diff --git a/docs/prompts/2026-01-26-refactoring.md b/docs/prompts/2026-01-26-refactoring.md index 41ac424..4ced654 100644 --- a/docs/prompts/2026-01-26-refactoring.md +++ b/docs/prompts/2026-01-26-refactoring.md @@ -3,7 +3,7 @@ This proposal responds to Merlijn's review and issue #20 (type duplication). It focuses on: - Removing custom Uppy + React hooks in favor of official Uppy patterns. -- Reusing existing Transloadit SDKs where it makes sense. +- Reusing existing Transloadit SDKs where it makes sense, without adding `@transloadit/node`. - Reducing type duplication by leaning on Zod schemas and convex-helpers. - Keeping the current component stable while introducing a cleaner, long-term path. @@ -19,8 +19,8 @@ This proposal responds to Merlijn's review and issue #20 (type duplication). It - Uppy 5.0 ships official React hooks and recommends using them via `UppyContextProvider`. - `@uppy/transloadit` is the canonical plugin; it uses Tus internally and supports `assemblyOptions` as a function that can call a backend for signatures. -- Convex allows marking external packages for Node actions, so a heavier SDK like `transloadit` - can be used server-side without client bundle impact. +- Convex allows marking external packages for Node actions, but we will avoid adding + `@transloadit/node` to keep setup minimal. - `convex-helpers` provides Zod-based function argument validation and optional `zodToConvex` helpers for schema definitions, with caveats. @@ -43,20 +43,11 @@ This lets us remove: We can keep a small helper that wires `assemblyOptions()` to a Convex HTTP endpoint or action. -### 2) Reuse the `transloadit` Node SDK server-side +### 2) Keep the lightweight API client (no `@transloadit/node`) -Likely feasible, but optional. - -Convex Node actions can import external packages. That means we could: -- Use the `transloadit` SDK inside `createAssembly` and `refreshAssembly` actions. -- Avoid re-implementing API logic and edge-case handling. - -Tradeoffs: -- Adds dependency weight. -- Need to verify ESM/CJS interop. -- Might require a Convex `convex.json` config change or explicit guidance for adopters. - -Alternative: keep the lightweight `fetch` + `@transloadit/utils` approach to avoid dependency risk. +We will keep the current `fetch` + `@transloadit/utils` approach. It avoids extra setup +(`convex.json` externalPackages) and keeps the component light. If we later discover +edge cases the SDK handles significantly better, we can revisit with evidence. ### 3) Type duplication (issue #20) @@ -71,6 +62,10 @@ We can reduce duplication without fully replacing Convex validators: This aligns with Convex guidance: Zod is great for args; use `zodToConvex` for DB schemas only when the tradeoffs are acceptable. +**Decision for now:** keep a single Convex schema source in `src/shared/schemas.ts`. We can revisit +convex-helpers later for *function args only* if we want richer validation, but it does not replace +DB schemas and adds another dependency. + ## Proposed direction (phased) ### Phase 0: Remove custom hooks + expose the official path (short) @@ -91,11 +86,9 @@ when the tradeoffs are acceptable. - Remove remaining Uppy-specific helpers from `@transloadit/convex/react`. - Keep only status/results helpers (or remove the React entry entirely if unnecessary). -### Phase 3: Optional server SDK adoption (medium) +### Phase 3: Optional server SDK adoption (not planned) -Likely **not** needed. Keep `fetch` + `@transloadit/utils` unless we can demonstrate: -- clear edge-case improvements over the current implementation, or -- significant DX improvements without extra complexity. +No server SDK adoption planned. Stick with `fetch` + `@transloadit/utils`. ### Phase 4: Type cleanup (medium) @@ -116,8 +109,7 @@ Likely **not** needed. Keep `fetch` + `@transloadit/utils` unless we can demonst 1. Should `@transloadit/convex/react` remain as a small status/results hook library, or be removed entirely in favor of Uppy React hooks? 2. `createAssemblyOptions` should support both templates and inline steps. -3. Avoid requiring `convex.json` changes unless we adopt the `transloadit` SDK for - clear edge-case wins. +3. Avoid requiring `convex.json` changes by staying off `@transloadit/node`. ## Success criteria diff --git a/src/client/index.ts b/src/client/index.ts index 9958a06..602e519 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -1,10 +1,31 @@ import type { AssemblyStatus } from "@transloadit/zod/v3/assemblyStatus"; import type { AssemblyInstructionsInput } from "@transloadit/zod/v3/template"; import { actionGeneric, mutationGeneric, queryGeneric } from "convex/server"; -import { type Infer, v } from "convex/values"; +import { v } from "convex/values"; import type { ComponentApi } from "../component/_generated/component.ts"; +import { + type AssemblyResponse, + type AssemblyResultResponse, + type CreateAssemblyArgs, + vAssemblyIdArgs, + vAssemblyResponse, + vAssemblyResultResponse, + vCreateAssemblyArgs, + vCreateAssemblyReturn, + vListAlbumResultsArgs, + vListAssembliesArgs, + vListResultsArgs, + vPurgeAlbumArgs, + vPurgeAlbumResponse, + vQueueWebhookResponse, + vStoreAssemblyMetadataArgs, + vWebhookActionArgs, + vWebhookResponse, +} from "../shared/schemas.ts"; import type { RunActionCtx, RunMutationCtx, RunQueryCtx } from "./types.ts"; +export { vAssemblyResponse, vAssemblyResultResponse, vCreateAssemblyArgs }; + export { assemblyStatusErrCodeSchema, assemblyStatusOkCodeSchema, @@ -20,11 +41,6 @@ export { isAssemblyTerminalOk, isAssemblyTerminalOkStatus, } from "@transloadit/zod/v3/assemblyStatus"; -export type { - ParsedWebhookRequest, - VerifiedWebhookRequest, - WebhookActionArgs, -} from "../component/apiUtils.ts"; export { buildWebhookQueueArgs, handleWebhookRequest, @@ -61,6 +77,11 @@ export { getResultOriginalKey, getResultUrl, } from "../shared/resultUtils.ts"; +export type { + ParsedWebhookRequest, + VerifiedWebhookRequest, + WebhookActionArgs, +} from "../shared/schemas.ts"; export type { TusMetadataOptions, TusUploadConfig, @@ -85,56 +106,7 @@ function requireEnv(names: string[]): string { throw new Error(`Missing ${names.join(" or ")} environment variable`); } -export const vAssemblyResponse = v.object({ - _id: v.string(), - _creationTime: v.number(), - assemblyId: v.string(), - status: v.optional(v.string()), - ok: v.optional(v.string()), - message: v.optional(v.string()), - templateId: v.optional(v.string()), - notifyUrl: v.optional(v.string()), - numExpectedUploadFiles: v.optional(v.number()), - fields: v.optional(v.record(v.string(), v.any())), - uploads: v.optional(v.array(v.any())), - results: v.optional(v.record(v.string(), v.array(v.any()))), - error: v.optional(v.any()), - raw: v.optional(v.any()), - createdAt: v.number(), - updatedAt: v.number(), - userId: v.optional(v.string()), -}); - -export type AssemblyResponse = Infer; - -export const vAssemblyResultResponse = v.object({ - _id: v.string(), - _creationTime: v.number(), - assemblyId: v.string(), - album: v.optional(v.string()), - userId: v.optional(v.string()), - stepName: v.string(), - resultId: v.optional(v.string()), - sslUrl: v.optional(v.string()), - name: v.optional(v.string()), - size: v.optional(v.number()), - mime: v.optional(v.string()), - raw: v.any(), - createdAt: v.number(), -}); - -export type AssemblyResultResponse = Infer; - -export const vCreateAssemblyArgs = v.object({ - templateId: v.optional(v.string()), - steps: v.optional(v.record(v.string(), v.any())), - fields: v.optional(v.record(v.string(), v.any())), - notifyUrl: v.optional(v.string()), - numExpectedUploadFiles: v.optional(v.number()), - expires: v.optional(v.string()), - additionalParams: v.optional(v.record(v.string(), v.any())), - userId: v.optional(v.string()), -}); +export type { AssemblyResponse, AssemblyResultResponse, CreateAssemblyArgs }; /** * @deprecated Prefer `makeTransloaditAPI` or `Transloadit` for new code. @@ -158,10 +130,7 @@ export class TransloaditClient { return new TransloaditClient(component, config); } - async createAssembly( - ctx: RunActionCtx, - args: Infer, - ) { + async createAssembly(ctx: RunActionCtx, args: CreateAssemblyArgs) { return ctx.runAction(this.component.lib.createAssembly, { ...args, config: this.config, @@ -266,10 +235,7 @@ export function makeTransloaditAPI( return { createAssembly: actionGeneric({ args: vCreateAssemblyArgs, - returns: v.object({ - assemblyId: v.string(), - data: v.any(), - }), + returns: vCreateAssemblyReturn, handler: async (ctx, args) => { const resolvedConfig = resolveConfig(); return ctx.runAction(component.lib.createAssembly, { @@ -279,17 +245,8 @@ export function makeTransloaditAPI( }, }), handleWebhook: actionGeneric({ - args: { - payload: v.any(), - rawBody: v.optional(v.string()), - signature: v.optional(v.string()), - }, - returns: v.object({ - assemblyId: v.string(), - resultCount: v.number(), - ok: v.optional(v.string()), - status: v.optional(v.string()), - }), + args: vWebhookActionArgs, + returns: vWebhookResponse, handler: async (ctx, args) => { const resolvedConfig = resolveConfig(); return ctx.runAction(component.lib.handleWebhook, { @@ -299,15 +256,8 @@ export function makeTransloaditAPI( }, }), queueWebhook: actionGeneric({ - args: { - payload: v.any(), - rawBody: v.optional(v.string()), - signature: v.optional(v.string()), - }, - returns: v.object({ - assemblyId: v.string(), - queued: v.boolean(), - }), + args: vWebhookActionArgs, + returns: vQueueWebhookResponse, handler: async (ctx, args) => { const resolvedConfig = resolveConfig(); return ctx.runAction(component.lib.queueWebhook, { @@ -317,13 +267,8 @@ export function makeTransloaditAPI( }, }), refreshAssembly: actionGeneric({ - args: { assemblyId: v.string() }, - returns: v.object({ - assemblyId: v.string(), - resultCount: v.number(), - ok: v.optional(v.string()), - status: v.optional(v.string()), - }), + args: vAssemblyIdArgs, + returns: vWebhookResponse, handler: async (ctx, args) => { const resolvedConfig = resolveConfig(); return ctx.runAction(component.lib.refreshAssembly, { @@ -333,63 +278,42 @@ export function makeTransloaditAPI( }, }), getAssemblyStatus: queryGeneric({ - args: { assemblyId: v.string() }, + args: vAssemblyIdArgs, returns: v.union(vAssemblyResponse, v.null()), handler: async (ctx, args) => { return ctx.runQuery(component.lib.getAssemblyStatus, args); }, }), listAssemblies: queryGeneric({ - args: { - status: v.optional(v.string()), - userId: v.optional(v.string()), - limit: v.optional(v.number()), - }, + args: vListAssembliesArgs, returns: v.array(vAssemblyResponse), handler: async (ctx, args) => { return ctx.runQuery(component.lib.listAssemblies, args); }, }), listResults: queryGeneric({ - args: { - assemblyId: v.string(), - stepName: v.optional(v.string()), - limit: v.optional(v.number()), - }, + args: vListResultsArgs, returns: v.array(vAssemblyResultResponse), handler: async (ctx, args) => { return ctx.runQuery(component.lib.listResults, args); }, }), listAlbumResults: queryGeneric({ - args: { - album: v.string(), - limit: v.optional(v.number()), - }, + args: vListAlbumResultsArgs, returns: v.array(vAssemblyResultResponse), handler: async (ctx, args) => { return ctx.runQuery(component.lib.listAlbumResults, args); }, }), purgeAlbum: mutationGeneric({ - args: { - album: v.string(), - deleteAssemblies: v.optional(v.boolean()), - }, - returns: v.object({ - deletedResults: v.number(), - deletedAssemblies: v.number(), - }), + args: vPurgeAlbumArgs, + returns: vPurgeAlbumResponse, handler: async (ctx, args) => { return ctx.runMutation(component.lib.purgeAlbum, args); }, }), storeAssemblyMetadata: mutationGeneric({ - args: { - assemblyId: v.string(), - userId: v.optional(v.string()), - fields: v.optional(v.record(v.string(), v.any())), - }, + args: vStoreAssemblyMetadataArgs, returns: v.union(vAssemblyResponse, v.null()), handler: async (ctx, args) => { return ctx.runMutation(component.lib.storeAssemblyMetadata, args); diff --git a/src/component/apiUtils.ts b/src/component/apiUtils.ts index 8fde703..0e08f66 100644 --- a/src/component/apiUtils.ts +++ b/src/component/apiUtils.ts @@ -1,28 +1,13 @@ import { signParams, verifyWebhookSignature } from "@transloadit/utils"; import type { AssemblyStatusResults } from "@transloadit/zod/v3/assemblyStatus"; -import type { AssemblyInstructionsInput } from "@transloadit/zod/v3/template"; import { transloaditError } from "../shared/errors.ts"; - -export interface TransloaditAuthConfig { - authKey: string; - authSecret: string; -} - -export interface BuildParamsOptions { - authKey: string; - templateId?: string; - steps?: AssemblyInstructionsInput["steps"]; - fields?: AssemblyInstructionsInput["fields"]; - notifyUrl?: string; - numExpectedUploadFiles?: number; - expires?: string; - additionalParams?: Record; -} - -export interface BuildParamsResult { - params: Record; - paramsString: string; -} +import type { + BuildParamsOptions, + BuildParamsResult, + ParsedWebhookRequest, + VerifiedWebhookRequest, + WebhookActionArgs, +} from "../shared/schemas.ts"; export function buildTransloaditParams( options: BuildParamsOptions, @@ -74,16 +59,6 @@ export async function signTransloaditParams( return signParams(paramsString, authSecret, "sha384"); } -export type ParsedWebhookRequest = { - payload: unknown; - rawBody: string; - signature?: string; -}; - -export type VerifiedWebhookRequest = ParsedWebhookRequest & { - verified: boolean; -}; - export async function parseTransloaditWebhook( request: Request, ): Promise { @@ -154,12 +129,6 @@ export async function buildWebhookQueueArgs( }; } -export type WebhookActionArgs = { - payload: unknown; - rawBody?: string; - signature?: string; -}; - export async function handleWebhookRequest( request: Request, options: { diff --git a/src/component/lib.ts b/src/component/lib.ts index 452f32e..68fea08 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -1,10 +1,32 @@ import type { AssemblyStatus } from "@transloadit/zod/v3/assemblyStatus"; import type { AssemblyInstructionsInput } from "@transloadit/zod/v3/template"; import { anyApi, type FunctionReference } from "convex/server"; -import { type Infer, v } from "convex/values"; +import { v } from "convex/values"; import { parseAssemblyStatus } from "../shared/assemblyUrls.ts"; import { transloaditError } from "../shared/errors.ts"; import { getResultUrl } from "../shared/resultUtils.ts"; +import { + type ProcessWebhookResult, + vAssembly, + vAssemblyBaseArgs, + vAssemblyIdArgs, + vAssemblyResult, + vCreateAssemblyReturn, + vHandleWebhookArgs, + vListAlbumResultsArgs, + vListAssembliesArgs, + vListResultsArgs, + vPurgeAlbumArgs, + vPurgeAlbumResponse, + vQueueWebhookResponse, + vRefreshAssemblyArgs, + vReplaceResultsArgs, + vStoreAssemblyMetadataArgs, + vTransloaditConfig, + vUpsertAssemblyArgs, + vWebhookArgs, + vWebhookResponse, +} from "../shared/schemas.ts"; import { action, internalAction, @@ -21,12 +43,8 @@ import { const TRANSLOADIT_ASSEMBLY_URL = "https://api2.transloadit.com/assemblies"; -type ProcessWebhookResult = { - assemblyId: string; - resultCount: number; - ok?: string; - status?: string; -}; +export { vAssembly, vAssemblyResult, vTransloaditConfig }; +export type { Assembly, AssemblyResult } from "../shared/schemas.ts"; type InternalApi = { lib: { @@ -147,78 +165,8 @@ const applyAssemblyStatus = async ( }; }; -export const vAssembly = v.object({ - _id: v.id("assemblies"), - _creationTime: v.number(), - assemblyId: v.string(), - status: v.optional(v.string()), - ok: v.optional(v.string()), - message: v.optional(v.string()), - templateId: v.optional(v.string()), - notifyUrl: v.optional(v.string()), - numExpectedUploadFiles: v.optional(v.number()), - fields: v.optional(v.record(v.string(), v.any())), - uploads: v.optional(v.array(v.any())), - results: v.optional(v.record(v.string(), v.array(v.any()))), - error: v.optional(v.any()), - raw: v.optional(v.any()), - createdAt: v.number(), - updatedAt: v.number(), - userId: v.optional(v.string()), -}); - -export type Assembly = Infer; - -export const vAssemblyResult = v.object({ - _id: v.id("results"), - _creationTime: v.number(), - assemblyId: v.string(), - album: v.optional(v.string()), - userId: v.optional(v.string()), - stepName: v.string(), - resultId: v.optional(v.string()), - sslUrl: v.optional(v.string()), - name: v.optional(v.string()), - size: v.optional(v.number()), - mime: v.optional(v.string()), - raw: v.any(), - createdAt: v.number(), -}); - -export type AssemblyResult = Infer; - -export const vTransloaditConfig = v.object({ - authKey: v.string(), - authSecret: v.string(), -}); - -const vAssemblyBaseArgs = { - templateId: v.optional(v.string()), - steps: v.optional(v.record(v.string(), v.any())), - fields: v.optional(v.record(v.string(), v.any())), - notifyUrl: v.optional(v.string()), - numExpectedUploadFiles: v.optional(v.number()), - expires: v.optional(v.string()), - additionalParams: v.optional(v.record(v.string(), v.any())), - userId: v.optional(v.string()), -}; - export const upsertAssembly = internalMutation({ - args: { - assemblyId: v.string(), - status: v.optional(v.string()), - ok: v.optional(v.string()), - message: v.optional(v.string()), - templateId: v.optional(v.string()), - notifyUrl: v.optional(v.string()), - numExpectedUploadFiles: v.optional(v.number()), - fields: v.optional(v.record(v.string(), v.any())), - uploads: v.optional(v.array(v.any())), - results: v.optional(v.record(v.string(), v.array(v.any()))), - error: v.optional(v.any()), - raw: v.optional(v.any()), - userId: v.optional(v.string()), - }, + args: vUpsertAssemblyArgs, returns: v.id("assemblies"), handler: async (ctx, args) => { // Note: we persist full `raw` + `results` for debugging/fidelity. Large @@ -272,15 +220,7 @@ export const upsertAssembly = internalMutation({ }); export const replaceResultsForAssembly = internalMutation({ - args: { - assemblyId: v.string(), - results: v.array( - v.object({ - stepName: v.string(), - result: v.any(), - }), - ), - }, + args: vReplaceResultsArgs, returns: v.null(), handler: async (ctx, args) => { // We store raw result payloads for fidelity. For very large assemblies, @@ -334,10 +274,7 @@ export const createAssembly = action({ config: vTransloaditConfig, ...vAssemblyBaseArgs, }, - returns: v.object({ - assemblyId: v.string(), - data: v.any(), - }), + returns: vCreateAssemblyReturn, handler: async (ctx, args) => { const { paramsString, params } = buildTransloaditParams({ authKey: args.config.authKey, @@ -414,29 +351,9 @@ export const createAssembly = action({ }, }); -const vWebhookArgs = { - payload: v.any(), - rawBody: v.optional(v.string()), - signature: v.optional(v.string()), - verifySignature: v.optional(v.boolean()), - authSecret: v.optional(v.string()), -}; - -const vPublicWebhookArgs = { - payload: v.any(), - rawBody: v.optional(v.string()), - signature: v.optional(v.string()), - verifySignature: v.optional(v.boolean()), -}; - export const processWebhook = internalAction({ args: vWebhookArgs, - returns: v.object({ - assemblyId: v.string(), - resultCount: v.number(), - ok: v.optional(v.string()), - status: v.optional(v.string()), - }), + returns: vWebhookResponse, handler: async (ctx, args) => { const rawBody = resolveWebhookRawBody(args); const shouldVerify = args.verifySignature ?? true; @@ -474,20 +391,8 @@ export const processWebhook = internalAction({ }); export const handleWebhook = action({ - args: { - ...vPublicWebhookArgs, - config: v.optional( - v.object({ - authSecret: v.string(), - }), - ), - }, - returns: v.object({ - assemblyId: v.string(), - resultCount: v.number(), - ok: v.optional(v.string()), - status: v.optional(v.string()), - }), + args: vHandleWebhookArgs, + returns: vWebhookResponse, handler: async (ctx, args) => { const verifySignature = args.verifySignature ?? true; return ctx.runAction(internal.lib.processWebhook, { @@ -501,18 +406,8 @@ export const handleWebhook = action({ }); export const queueWebhook = action({ - args: { - ...vPublicWebhookArgs, - config: v.optional( - v.object({ - authSecret: v.string(), - }), - ), - }, - returns: v.object({ - assemblyId: v.string(), - queued: v.boolean(), - }), + args: vHandleWebhookArgs, + returns: vQueueWebhookResponse, handler: async (ctx, args) => { const rawBody = resolveWebhookRawBody(args); const shouldVerify = args.verifySignature ?? true; @@ -564,21 +459,8 @@ export const queueWebhook = action({ }); export const refreshAssembly = action({ - args: { - assemblyId: v.string(), - config: v.optional( - v.object({ - authKey: v.string(), - authSecret: v.string(), - }), - ), - }, - returns: v.object({ - assemblyId: v.string(), - resultCount: v.number(), - ok: v.optional(v.string()), - status: v.optional(v.string()), - }), + args: vRefreshAssemblyArgs, + returns: vWebhookResponse, handler: async (ctx, args) => { const { assemblyId } = args; const authKey = args.config?.authKey ?? process.env.TRANSLOADIT_KEY; @@ -603,7 +485,7 @@ export const refreshAssembly = action({ }); export const getAssemblyStatus = query({ - args: { assemblyId: v.string() }, + args: vAssemblyIdArgs, returns: v.union(vAssembly, v.null()), handler: async (ctx, args) => { return await ctx.db @@ -614,11 +496,7 @@ export const getAssemblyStatus = query({ }); export const listAssemblies = query({ - args: { - status: v.optional(v.string()), - userId: v.optional(v.string()), - limit: v.optional(v.number()), - }, + args: vListAssembliesArgs, returns: v.array(vAssembly), handler: async (ctx, args) => { if (args.userId) { @@ -644,11 +522,7 @@ export const listAssemblies = query({ }); export const listResults = query({ - args: { - assemblyId: v.string(), - stepName: v.optional(v.string()), - limit: v.optional(v.number()), - }, + args: vListResultsArgs, returns: v.array(vAssemblyResult), handler: async (ctx, args) => { if (args.stepName) { @@ -671,10 +545,7 @@ export const listResults = query({ }); export const listAlbumResults = query({ - args: { - album: v.string(), - limit: v.optional(v.number()), - }, + args: vListAlbumResultsArgs, returns: v.array(vAssemblyResult), handler: async (ctx, args) => { return ctx.db @@ -686,14 +557,8 @@ export const listAlbumResults = query({ }); export const purgeAlbum = mutation({ - args: { - album: v.string(), - deleteAssemblies: v.optional(v.boolean()), - }, - returns: v.object({ - deletedResults: v.number(), - deletedAssemblies: v.number(), - }), + args: vPurgeAlbumArgs, + returns: vPurgeAlbumResponse, handler: async (ctx, args) => { const results = await ctx.db .query("results") @@ -725,11 +590,7 @@ export const purgeAlbum = mutation({ }); export const storeAssemblyMetadata = mutation({ - args: { - assemblyId: v.string(), - userId: v.optional(v.string()), - fields: v.optional(v.record(v.string(), v.any())), - }, + args: vStoreAssemblyMetadataArgs, returns: v.union(vAssembly, v.null()), handler: async (ctx, args) => { const existing = await ctx.db diff --git a/src/component/schema.ts b/src/component/schema.ts index 3f7e3bf..11cf758 100644 --- a/src/component/schema.ts +++ b/src/component/schema.ts @@ -1,40 +1,12 @@ import { defineSchema, defineTable } from "convex/server"; -import { v } from "convex/values"; +import { vAssemblyFields, vAssemblyResultFields } from "../shared/schemas.ts"; export default defineSchema({ - assemblies: defineTable({ - assemblyId: v.string(), - status: v.optional(v.string()), - ok: v.optional(v.string()), - message: v.optional(v.string()), - templateId: v.optional(v.string()), - notifyUrl: v.optional(v.string()), - numExpectedUploadFiles: v.optional(v.number()), - fields: v.optional(v.record(v.string(), v.any())), - uploads: v.optional(v.array(v.any())), - results: v.optional(v.record(v.string(), v.array(v.any()))), - error: v.optional(v.any()), - raw: v.optional(v.any()), - createdAt: v.number(), - updatedAt: v.number(), - userId: v.optional(v.string()), - }) + assemblies: defineTable(vAssemblyFields) .index("by_assemblyId", ["assemblyId"]) .index("by_status", ["status"]) .index("by_userId", ["userId"]), - results: defineTable({ - assemblyId: v.string(), - album: v.optional(v.string()), - userId: v.optional(v.string()), - stepName: v.string(), - resultId: v.optional(v.string()), - sslUrl: v.optional(v.string()), - name: v.optional(v.string()), - size: v.optional(v.number()), - mime: v.optional(v.string()), - raw: v.any(), - createdAt: v.number(), - }) + results: defineTable(vAssemblyResultFields) .index("by_assemblyId", ["assemblyId"]) .index("by_assemblyId_and_step", ["assemblyId", "stepName"]) .index("by_album", ["album"]), diff --git a/src/shared/schemas.ts b/src/shared/schemas.ts new file mode 100644 index 0000000..3270d7c --- /dev/null +++ b/src/shared/schemas.ts @@ -0,0 +1,271 @@ +import type { AssemblyInstructionsInput } from "@transloadit/zod/v3/template"; +import { type Infer, v } from "convex/values"; + +export const vAssemblyFields = { + assemblyId: v.string(), + status: v.optional(v.string()), + ok: v.optional(v.string()), + message: v.optional(v.string()), + templateId: v.optional(v.string()), + notifyUrl: v.optional(v.string()), + numExpectedUploadFiles: v.optional(v.number()), + fields: v.optional(v.record(v.string(), v.any())), + uploads: v.optional(v.array(v.any())), + results: v.optional(v.record(v.string(), v.array(v.any()))), + error: v.optional(v.any()), + raw: v.optional(v.any()), + createdAt: v.number(), + updatedAt: v.number(), + userId: v.optional(v.string()), +}; + +export const vAssemblyResultFields = { + assemblyId: v.string(), + album: v.optional(v.string()), + userId: v.optional(v.string()), + stepName: v.string(), + resultId: v.optional(v.string()), + sslUrl: v.optional(v.string()), + name: v.optional(v.string()), + size: v.optional(v.number()), + mime: v.optional(v.string()), + raw: v.any(), + createdAt: v.number(), +}; + +export const vAssembly = v.object({ + _id: v.id("assemblies"), + _creationTime: v.number(), + ...vAssemblyFields, +}); + +export type Assembly = Infer; + +export const vAssemblyResponse = v.object({ + _id: v.string(), + _creationTime: v.number(), + ...vAssemblyFields, +}); + +export type AssemblyResponse = Infer; + +export const vAssemblyResult = v.object({ + _id: v.id("results"), + _creationTime: v.number(), + ...vAssemblyResultFields, +}); + +export type AssemblyResult = Infer; + +export const vAssemblyResultResponse = v.object({ + _id: v.string(), + _creationTime: v.number(), + ...vAssemblyResultFields, +}); + +export type AssemblyResultResponse = Infer; + +export const vTransloaditConfig = v.object({ + authKey: v.string(), + authSecret: v.string(), +}); + +export type TransloaditConfig = Infer; + +export const vAssemblyBaseArgs = { + templateId: v.optional(v.string()), + steps: v.optional(v.record(v.string(), v.any())), + fields: v.optional(v.record(v.string(), v.any())), + notifyUrl: v.optional(v.string()), + numExpectedUploadFiles: v.optional(v.number()), + expires: v.optional(v.string()), + additionalParams: v.optional(v.record(v.string(), v.any())), + userId: v.optional(v.string()), +}; + +export const vCreateAssemblyArgs = v.object(vAssemblyBaseArgs); + +export type CreateAssemblyArgs = Omit< + Infer, + "steps" | "fields" +> & { + steps?: AssemblyInstructionsInput["steps"]; + fields?: AssemblyInstructionsInput["fields"]; +}; + +export const vCreateAssemblyReturn = v.object({ + assemblyId: v.string(), + data: v.any(), +}); + +export type CreateAssemblyReturn = Infer; + +export const vWebhookArgs = { + payload: v.any(), + rawBody: v.optional(v.string()), + signature: v.optional(v.string()), + verifySignature: v.optional(v.boolean()), + authSecret: v.optional(v.string()), +}; + +export const vPublicWebhookArgs = { + payload: v.any(), + rawBody: v.optional(v.string()), + signature: v.optional(v.string()), + verifySignature: v.optional(v.boolean()), +}; + +export const vWebhookActionArgs = v.object({ + payload: v.any(), + rawBody: v.optional(v.string()), + signature: v.optional(v.string()), +}); + +export type WebhookActionArgs = Infer; + +export const vWebhookResponse = v.object({ + assemblyId: v.string(), + resultCount: v.number(), + ok: v.optional(v.string()), + status: v.optional(v.string()), +}); + +export type WebhookResponse = Infer; + +export const vQueueWebhookResponse = v.object({ + assemblyId: v.string(), + queued: v.boolean(), +}); + +export type QueueWebhookResponse = Infer; + +export const vAssemblyIdArgs = { + assemblyId: v.string(), +}; + +export const vListAssembliesArgs = { + status: v.optional(v.string()), + userId: v.optional(v.string()), + limit: v.optional(v.number()), +}; + +export const vListResultsArgs = { + assemblyId: v.string(), + stepName: v.optional(v.string()), + limit: v.optional(v.number()), +}; + +export const vListAlbumResultsArgs = { + album: v.string(), + limit: v.optional(v.number()), +}; + +export const vPurgeAlbumArgs = { + album: v.string(), + deleteAssemblies: v.optional(v.boolean()), +}; + +export const vPurgeAlbumResponse = v.object({ + deletedResults: v.number(), + deletedAssemblies: v.number(), +}); + +export type PurgeAlbumResponse = Infer; + +export const vStoreAssemblyMetadataArgs = { + assemblyId: v.string(), + userId: v.optional(v.string()), + fields: v.optional(v.record(v.string(), v.any())), +}; + +export const vRefreshAssemblyArgs = { + assemblyId: v.string(), + config: v.optional( + v.object({ + authKey: v.string(), + authSecret: v.string(), + }), + ), +}; + +export const vHandleWebhookArgs = { + ...vPublicWebhookArgs, + config: v.optional( + v.object({ + authSecret: v.string(), + }), + ), +}; + +export const vProcessWebhookResult = vWebhookResponse; + +export type ProcessWebhookResult = Infer; + +export const vReplaceResultsArgs = { + assemblyId: v.string(), + results: v.array( + v.object({ + stepName: v.string(), + result: v.any(), + }), + ), +}; + +export const vUpsertAssemblyArgs = { + assemblyId: v.string(), + status: v.optional(v.string()), + ok: v.optional(v.string()), + message: v.optional(v.string()), + templateId: v.optional(v.string()), + notifyUrl: v.optional(v.string()), + numExpectedUploadFiles: v.optional(v.number()), + fields: v.optional(v.record(v.string(), v.any())), + uploads: v.optional(v.array(v.any())), + results: v.optional(v.record(v.string(), v.array(v.any()))), + error: v.optional(v.any()), + raw: v.optional(v.any()), + userId: v.optional(v.string()), +}; + +export const vBuildParamsOptions = v.object({ + authKey: v.string(), + templateId: v.optional(v.string()), + steps: v.optional(v.any()), + fields: v.optional(v.any()), + notifyUrl: v.optional(v.string()), + numExpectedUploadFiles: v.optional(v.number()), + expires: v.optional(v.string()), + additionalParams: v.optional(v.record(v.string(), v.any())), +}); + +export type BuildParamsOptions = Omit< + Infer, + "steps" | "fields" +> & { + steps?: AssemblyInstructionsInput["steps"]; + fields?: AssemblyInstructionsInput["fields"]; +}; + +export const vBuildParamsResult = v.object({ + params: v.record(v.string(), v.any()), + paramsString: v.string(), +}); + +export type BuildParamsResult = Infer; + +const vParsedWebhookFields = { + payload: v.any(), + rawBody: v.string(), + signature: v.optional(v.string()), +}; + +export const vParsedWebhookRequest = v.object(vParsedWebhookFields); + +export type ParsedWebhookRequest = Infer; + +export const vVerifiedWebhookRequest = v.object({ + ...vParsedWebhookFields, + verified: v.boolean(), +}); + +export type VerifiedWebhookRequest = Infer;