@transloadit/convex 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +162 -79
  2. package/dist/client/index.d.ts +92 -63
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +57 -30
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/types.d.ts.map +1 -1
  7. package/dist/component/_generated/component.d.ts +35 -15
  8. package/dist/component/_generated/component.d.ts.map +1 -1
  9. package/dist/component/apiUtils.d.ts +13 -4
  10. package/dist/component/apiUtils.d.ts.map +1 -1
  11. package/dist/component/apiUtils.js +22 -12
  12. package/dist/component/apiUtils.js.map +1 -1
  13. package/dist/component/lib.d.ts +69 -45
  14. package/dist/component/lib.d.ts.map +1 -1
  15. package/dist/component/lib.js +154 -77
  16. package/dist/component/lib.js.map +1 -1
  17. package/dist/component/schema.d.ts +9 -9
  18. package/dist/component/schema.js +3 -3
  19. package/dist/component/schema.js.map +1 -1
  20. package/dist/react/index.d.ts +23 -25
  21. package/dist/react/index.d.ts.map +1 -1
  22. package/dist/react/index.js +129 -88
  23. package/dist/react/index.js.map +1 -1
  24. package/dist/test/index.d.ts +65 -0
  25. package/dist/test/index.d.ts.map +1 -0
  26. package/dist/test/index.js +8 -0
  27. package/dist/test/index.js.map +1 -0
  28. package/package.json +27 -15
  29. package/src/client/index.ts +72 -35
  30. package/src/client/types.ts +12 -11
  31. package/src/component/_generated/component.ts +44 -13
  32. package/src/component/apiUtils.test.ts +29 -0
  33. package/src/component/apiUtils.ts +52 -26
  34. package/src/component/lib.test.ts +73 -3
  35. package/src/component/lib.ts +220 -97
  36. package/src/component/schema.ts +3 -3
  37. package/src/react/index.tsx +193 -150
  38. package/src/test/index.ts +10 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@transloadit/convex",
3
3
  "description": "Transloadit component for Convex",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.5.0",
7
7
  "repository": {
@@ -25,16 +25,18 @@
25
25
  "typecheck": "tsc --noEmit",
26
26
  "test": "vitest run --typecheck",
27
27
  "test:watch": "vitest --typecheck",
28
+ "test:browser": "dotenv -q -- node scripts/run-browser.ts",
28
29
  "lint": "biome check .",
29
30
  "format": "biome format . --write",
31
+ "check": "yarn format && yarn lint && yarn typecheck && yarn test",
30
32
  "prepack": "yarn build",
31
- "smoke": "dotenv -q -- node scripts/smoke-test.ts",
32
33
  "template:ensure": "dotenv -q -- node scripts/ensure-template.ts",
33
- "tunnel": "node scripts/start-webhook-tunnel.ts",
34
- "qa:full": "dotenv -q -- node scripts/qa-full.ts",
35
- "qa:full:verbose": "dotenv -q -- node scripts/qa-full.ts --verbose"
34
+ "tunnel": "node scripts/start-webhook-tunnel.ts"
36
35
  },
37
- "files": ["dist", "src"],
36
+ "files": [
37
+ "dist",
38
+ "src"
39
+ ],
38
40
  "exports": {
39
41
  "./package.json": "./package.json",
40
42
  ".": {
@@ -52,6 +54,10 @@
52
54
  "types": "./dist/component/lib.d.ts",
53
55
  "default": "./dist/component/lib.js"
54
56
  },
57
+ "./test": {
58
+ "types": "./dist/test/index.d.ts",
59
+ "default": "./dist/test/index.js"
60
+ },
55
61
  "./convex.config": {
56
62
  "@convex-dev/component-source": "./src/component/convex.config.ts",
57
63
  "types": "./dist/component/convex.config.d.ts",
@@ -68,19 +74,25 @@
68
74
  }
69
75
  },
70
76
  "dependencies": {
71
- "tus-js-client": "^4.1.0"
77
+ "@transloadit/types": "^4.1.3",
78
+ "tus-js-client": "^4.3.1"
72
79
  },
73
80
  "devDependencies": {
74
- "@biomejs/biome": "^1.9.4",
81
+ "@biomejs/biome": "^2.3.11",
75
82
  "@edge-runtime/vm": "^5.0.0",
76
- "@types/node": "^20.19.0",
77
- "@types/react": "^19.1.10",
78
- "convex": "^1.24.8",
79
- "convex-test": "0.0.37",
80
- "dotenv-cli": "^8.0.0",
83
+ "@playwright/test": "^1.57.0",
84
+ "@types/node": "^25.0.8",
85
+ "@types/react": "^19.2.8",
86
+ "@types/react-dom": "^19",
87
+ "convex": "^1.31.4",
88
+ "convex-test": "^0.0.41",
89
+ "dotenv-cli": "^11.0.0",
90
+ "esbuild": "^0.27.2",
91
+ "react": "^19.2.3",
92
+ "react-dom": "^19.2.3",
81
93
  "typescript": "^5.9.3",
82
- "vitest": "3.2.4"
94
+ "vitest": "^4.0.17"
83
95
  },
84
96
  "types": "./dist/client/index.d.ts",
85
97
  "module": "./dist/client/index.js"
86
- }
98
+ }
@@ -1,8 +1,13 @@
1
+ import type { AssemblyStatus } from "@transloadit/types/assemblyStatus";
2
+ import type { AssemblyInstructionsInput } from "@transloadit/types/template";
1
3
  import { actionGeneric, mutationGeneric, queryGeneric } from "convex/server";
2
4
  import { type Infer, v } from "convex/values";
3
5
  import type { ComponentApi } from "../component/_generated/component.js";
4
6
  import type { RunActionCtx, RunMutationCtx, RunQueryCtx } from "./types.js";
5
7
 
8
+ export { parseTransloaditWebhook } from "../component/apiUtils.js";
9
+ export type { AssemblyStatus, AssemblyInstructionsInput };
10
+
6
11
  export interface TransloaditConfig {
7
12
  authKey: string;
8
13
  authSecret: string;
@@ -30,9 +35,9 @@ export const vAssemblyResponse = v.object({
30
35
  templateId: v.optional(v.string()),
31
36
  notifyUrl: v.optional(v.string()),
32
37
  numExpectedUploadFiles: v.optional(v.number()),
33
- fields: v.optional(v.any()),
34
- uploads: v.optional(v.any()),
35
- results: v.optional(v.any()),
38
+ fields: v.optional(v.record(v.string(), v.any())),
39
+ uploads: v.optional(v.array(v.any())),
40
+ results: v.optional(v.record(v.string(), v.array(v.any()))),
36
41
  error: v.optional(v.any()),
37
42
  raw: v.optional(v.any()),
38
43
  createdAt: v.number(),
@@ -60,12 +65,12 @@ export type AssemblyResultResponse = Infer<typeof vAssemblyResultResponse>;
60
65
 
61
66
  export const vCreateAssemblyArgs = v.object({
62
67
  templateId: v.optional(v.string()),
63
- steps: v.optional(v.any()),
64
- fields: v.optional(v.any()),
68
+ steps: v.optional(v.record(v.string(), v.any())),
69
+ fields: v.optional(v.record(v.string(), v.any())),
65
70
  notifyUrl: v.optional(v.string()),
66
71
  numExpectedUploadFiles: v.optional(v.number()),
67
72
  expires: v.optional(v.string()),
68
- additionalParams: v.optional(v.any()),
73
+ additionalParams: v.optional(v.record(v.string(), v.any())),
69
74
  userId: v.optional(v.string()),
70
75
  });
71
76
 
@@ -79,12 +84,8 @@ export class TransloaditClient {
79
84
  ) {
80
85
  this.component = component;
81
86
  this.config = {
82
- authKey:
83
- config?.authKey ??
84
- requireEnv(["TRANSLOADIT_AUTH_KEY", "TRANSLOADIT_KEY"]),
85
- authSecret:
86
- config?.authSecret ??
87
- requireEnv(["TRANSLOADIT_AUTH_SECRET", "TRANSLOADIT_SECRET"]),
87
+ authKey: config?.authKey ?? requireEnv(["TRANSLOADIT_KEY"]),
88
+ authSecret: config?.authSecret ?? requireEnv(["TRANSLOADIT_SECRET"]),
88
89
  };
89
90
  }
90
91
 
@@ -102,17 +103,22 @@ export class TransloaditClient {
102
103
  });
103
104
  }
104
105
 
105
- async generateUploadParams(
106
+ async handleWebhook(
106
107
  ctx: RunActionCtx,
107
- args: Infer<typeof vCreateAssemblyArgs>,
108
+ args: {
109
+ payload: unknown;
110
+ rawBody?: string;
111
+ signature?: string;
112
+ verifySignature?: boolean;
113
+ },
108
114
  ) {
109
- return ctx.runAction(this.component.lib.generateUploadParams, {
115
+ return ctx.runAction(this.component.lib.handleWebhook, {
110
116
  ...args,
111
- config: this.config,
117
+ config: { authSecret: this.config.authSecret },
112
118
  });
113
119
  }
114
120
 
115
- async handleWebhook(
121
+ async queueWebhook(
116
122
  ctx: RunActionCtx,
117
123
  args: {
118
124
  payload: unknown;
@@ -121,12 +127,19 @@ export class TransloaditClient {
121
127
  verifySignature?: boolean;
122
128
  },
123
129
  ) {
124
- return ctx.runAction(this.component.lib.handleWebhook, {
130
+ return ctx.runAction(this.component.lib.queueWebhook, {
125
131
  ...args,
126
132
  config: { authSecret: this.config.authSecret },
127
133
  });
128
134
  }
129
135
 
136
+ async refreshAssembly(ctx: RunActionCtx, assemblyId: string) {
137
+ return ctx.runAction(this.component.lib.refreshAssembly, {
138
+ assemblyId,
139
+ config: this.config,
140
+ });
141
+ }
142
+
130
143
  async getAssemblyStatus(ctx: RunQueryCtx, assemblyId: string) {
131
144
  return ctx.runQuery(this.component.lib.getAssemblyStatus, { assemblyId });
132
145
  }
@@ -157,17 +170,22 @@ export class TransloaditClient {
157
170
  }
158
171
  }
159
172
 
173
+ export class Transloadit extends TransloaditClient {}
174
+
175
+ export function createTransloadit(
176
+ component: TransloaditComponent,
177
+ config?: Partial<TransloaditConfig>,
178
+ ) {
179
+ return new Transloadit(component, config);
180
+ }
181
+
160
182
  export function makeTransloaditAPI(
161
183
  component: TransloaditComponent,
162
184
  config?: Partial<TransloaditConfig>,
163
185
  ) {
164
186
  const resolvedConfig: TransloaditConfig = {
165
- authKey:
166
- config?.authKey ??
167
- requireEnv(["TRANSLOADIT_AUTH_KEY", "TRANSLOADIT_KEY"]),
168
- authSecret:
169
- config?.authSecret ??
170
- requireEnv(["TRANSLOADIT_AUTH_SECRET", "TRANSLOADIT_SECRET"]),
187
+ authKey: config?.authKey ?? requireEnv(["TRANSLOADIT_KEY"]),
188
+ authSecret: config?.authSecret ?? requireEnv(["TRANSLOADIT_SECRET"]),
171
189
  };
172
190
 
173
191
  return {
@@ -184,21 +202,25 @@ export function makeTransloaditAPI(
184
202
  });
185
203
  },
186
204
  }),
187
- generateUploadParams: actionGeneric({
188
- args: vCreateAssemblyArgs,
205
+ handleWebhook: actionGeneric({
206
+ args: {
207
+ payload: v.any(),
208
+ rawBody: v.optional(v.string()),
209
+ signature: v.optional(v.string()),
210
+ verifySignature: v.optional(v.boolean()),
211
+ },
189
212
  returns: v.object({
190
- params: v.string(),
191
- signature: v.string(),
192
- url: v.string(),
213
+ assemblyId: v.string(),
214
+ resultCount: v.number(),
193
215
  }),
194
216
  handler: async (ctx, args) => {
195
- return ctx.runAction(component.lib.generateUploadParams, {
217
+ return ctx.runAction(component.lib.handleWebhook, {
196
218
  ...args,
197
- config: resolvedConfig,
219
+ config: { authSecret: resolvedConfig.authSecret },
198
220
  });
199
221
  },
200
222
  }),
201
- handleWebhook: actionGeneric({
223
+ queueWebhook: actionGeneric({
202
224
  args: {
203
225
  payload: v.any(),
204
226
  rawBody: v.optional(v.string()),
@@ -207,15 +229,30 @@ export function makeTransloaditAPI(
207
229
  },
208
230
  returns: v.object({
209
231
  assemblyId: v.string(),
210
- resultCount: v.number(),
232
+ queued: v.boolean(),
211
233
  }),
212
234
  handler: async (ctx, args) => {
213
- return ctx.runAction(component.lib.handleWebhook, {
235
+ return ctx.runAction(component.lib.queueWebhook, {
214
236
  ...args,
215
237
  config: { authSecret: resolvedConfig.authSecret },
216
238
  });
217
239
  },
218
240
  }),
241
+ refreshAssembly: actionGeneric({
242
+ args: { assemblyId: v.string() },
243
+ returns: v.object({
244
+ assemblyId: v.string(),
245
+ resultCount: v.number(),
246
+ ok: v.optional(v.string()),
247
+ status: v.optional(v.string()),
248
+ }),
249
+ handler: async (ctx, args) => {
250
+ return ctx.runAction(component.lib.refreshAssembly, {
251
+ ...args,
252
+ config: resolvedConfig,
253
+ });
254
+ },
255
+ }),
219
256
  getAssemblyStatus: queryGeneric({
220
257
  args: { assemblyId: v.string() },
221
258
  returns: v.union(vAssemblyResponse, v.null()),
@@ -249,7 +286,7 @@ export function makeTransloaditAPI(
249
286
  args: {
250
287
  assemblyId: v.string(),
251
288
  userId: v.optional(v.string()),
252
- fields: v.optional(v.any()),
289
+ fields: v.optional(v.record(v.string(), v.any())),
253
290
  },
254
291
  returns: v.union(vAssemblyResponse, v.null()),
255
292
  handler: async (ctx, args) => {
@@ -33,17 +33,18 @@ export type QueryCtx = RunQueryCtx & {
33
33
  storage: StorageReader;
34
34
  };
35
35
 
36
- export type OpaqueIds<T> = T extends GenericId<infer _T>
37
- ? string
38
- : T extends (infer U)[]
39
- ? OpaqueIds<U>[]
40
- : T extends ArrayBuffer
41
- ? ArrayBuffer
42
- : T extends object
43
- ? {
44
- [K in keyof T]: OpaqueIds<T[K]>;
45
- }
46
- : T;
36
+ export type OpaqueIds<T> =
37
+ T extends GenericId<infer _T>
38
+ ? string
39
+ : T extends (infer U)[]
40
+ ? OpaqueIds<U>[]
41
+ : T extends ArrayBuffer
42
+ ? ArrayBuffer
43
+ : T extends object
44
+ ? {
45
+ [K in keyof T]: OpaqueIds<T[K]>;
46
+ }
47
+ : T;
47
48
 
48
49
  export type UseApi<API> = Expand<{
49
50
  [mod in keyof API]: API[mod] extends FunctionReference<
@@ -60,24 +60,43 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
60
60
  { assemblyId: string; data: any },
61
61
  Name
62
62
  >;
63
- generateUploadParams: FunctionReference<
63
+ handleWebhook: FunctionReference<
64
64
  "action",
65
65
  "internal",
66
66
  {
67
- config: { authKey: string; authSecret: string };
68
- templateId?: string;
69
- steps?: any;
70
- fields?: any;
71
- notifyUrl?: string;
72
- numExpectedUploadFiles?: number;
73
- expires?: string;
74
- additionalParams?: any;
75
- userId?: string;
67
+ payload: any;
68
+ rawBody?: string;
69
+ signature?: string;
70
+ verifySignature?: boolean;
71
+ config?: { authSecret: string };
72
+ },
73
+ {
74
+ assemblyId: string;
75
+ resultCount: number;
76
+ ok?: string;
77
+ status?: string;
76
78
  },
77
- { params: string; signature: string; url: string },
78
79
  Name
79
80
  >;
80
- handleWebhook: FunctionReference<
81
+ processWebhook: FunctionReference<
82
+ "action",
83
+ "internal",
84
+ {
85
+ payload: any;
86
+ rawBody?: string;
87
+ signature?: string;
88
+ verifySignature?: boolean;
89
+ authSecret?: string;
90
+ },
91
+ {
92
+ assemblyId: string;
93
+ resultCount: number;
94
+ ok?: string;
95
+ status?: string;
96
+ },
97
+ Name
98
+ >;
99
+ queueWebhook: FunctionReference<
81
100
  "action",
82
101
  "internal",
83
102
  {
@@ -87,7 +106,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
87
106
  verifySignature?: boolean;
88
107
  config?: { authSecret: string };
89
108
  },
90
- { assemblyId: string; resultCount: number },
109
+ { assemblyId: string; queued: boolean },
110
+ Name
111
+ >;
112
+ refreshAssembly: FunctionReference<
113
+ "action",
114
+ "internal",
115
+ { assemblyId: string; config?: { authKey: string; authSecret: string } },
116
+ {
117
+ assemblyId: string;
118
+ resultCount: number;
119
+ ok?: string;
120
+ status?: string;
121
+ },
91
122
  Name
92
123
  >;
93
124
  getAssemblyStatus: FunctionReference<
@@ -2,6 +2,7 @@ import { createHmac } from "node:crypto";
2
2
  import { describe, expect, test } from "vitest";
3
3
  import {
4
4
  buildTransloaditParams,
5
+ parseTransloaditWebhook,
5
6
  signTransloaditParams,
6
7
  verifyWebhookSignature,
7
8
  } from "./apiUtils.js";
@@ -45,4 +46,32 @@ describe("apiUtils", () => {
45
46
 
46
47
  expect(verified).toBe(true);
47
48
  });
49
+
50
+ test("parseTransloaditWebhook returns payload and signature", async () => {
51
+ const payload = { ok: "ASSEMBLY_COMPLETED", assembly_id: "asm_123" };
52
+ const formData = new FormData();
53
+ formData.append("transloadit", JSON.stringify(payload));
54
+ formData.append("signature", "sha384:abc");
55
+
56
+ const request = new Request("http://localhost", {
57
+ method: "POST",
58
+ body: formData,
59
+ });
60
+
61
+ const result = await parseTransloaditWebhook(request);
62
+ expect(result.payload).toEqual(payload);
63
+ expect(result.rawBody).toBe(JSON.stringify(payload));
64
+ expect(result.signature).toBe("sha384:abc");
65
+ });
66
+
67
+ test("parseTransloaditWebhook throws on missing payload", async () => {
68
+ const request = new Request("http://localhost", {
69
+ method: "POST",
70
+ body: new FormData(),
71
+ });
72
+
73
+ await expect(parseTransloaditWebhook(request)).rejects.toThrow(
74
+ "Missing transloadit payload",
75
+ );
76
+ });
48
77
  });
@@ -1,3 +1,6 @@
1
+ import type { AssemblyStatusResults } from "@transloadit/types/assemblyStatus";
2
+ import type { AssemblyInstructionsInput } from "@transloadit/types/template";
3
+
1
4
  export interface TransloaditAuthConfig {
2
5
  authKey: string;
3
6
  authSecret: string;
@@ -6,8 +9,8 @@ export interface TransloaditAuthConfig {
6
9
  export interface BuildParamsOptions {
7
10
  authKey: string;
8
11
  templateId?: string;
9
- steps?: Record<string, unknown>;
10
- fields?: Record<string, unknown>;
12
+ steps?: AssemblyInstructionsInput["steps"];
13
+ fields?: AssemblyInstructionsInput["fields"];
11
14
  notifyUrl?: string;
12
15
  numExpectedUploadFiles?: number;
13
16
  expires?: string;
@@ -64,30 +67,27 @@ async function hmacHex(
64
67
  key: string,
65
68
  data: string,
66
69
  ): Promise<string> {
67
- if (globalThis.crypto?.subtle) {
68
- const encoder = new TextEncoder();
69
- const cryptoKey = await globalThis.crypto.subtle.importKey(
70
- "raw",
71
- encoder.encode(key),
72
- { name: "HMAC", hash: { name: algorithm } },
73
- false,
74
- ["sign"],
75
- );
76
- const signature = await globalThis.crypto.subtle.sign(
77
- "HMAC",
78
- cryptoKey,
79
- encoder.encode(data),
80
- );
81
- const bytes = new Uint8Array(signature);
82
- return Array.from(bytes)
83
- .map((byte) => byte.toString(16).padStart(2, "0"))
84
- .join("");
70
+ if (!globalThis.crypto?.subtle) {
71
+ throw new Error("Web Crypto is required to sign Transloadit payloads");
85
72
  }
86
73
 
87
- const { createHmac } = await import("node:crypto");
88
- return createHmac(algorithm.replace("-", "").toLowerCase(), key)
89
- .update(data)
90
- .digest("hex");
74
+ const encoder = new TextEncoder();
75
+ const cryptoKey = await globalThis.crypto.subtle.importKey(
76
+ "raw",
77
+ encoder.encode(key),
78
+ { name: "HMAC", hash: { name: algorithm } },
79
+ false,
80
+ ["sign"],
81
+ );
82
+ const signature = await globalThis.crypto.subtle.sign(
83
+ "HMAC",
84
+ cryptoKey,
85
+ encoder.encode(data),
86
+ );
87
+ const bytes = new Uint8Array(signature);
88
+ return Array.from(bytes)
89
+ .map((byte) => byte.toString(16).padStart(2, "0"))
90
+ .join("");
91
91
  }
92
92
 
93
93
  export async function signTransloaditParams(
@@ -98,6 +98,30 @@ export async function signTransloaditParams(
98
98
  return `sha384:${signature}`;
99
99
  }
100
100
 
101
+ export type ParsedWebhookRequest = {
102
+ payload: unknown;
103
+ rawBody: string;
104
+ signature?: string;
105
+ };
106
+
107
+ export async function parseTransloaditWebhook(
108
+ request: Request,
109
+ ): Promise<ParsedWebhookRequest> {
110
+ const formData = await request.formData();
111
+ const rawPayload = formData.get("transloadit");
112
+ const signature = formData.get("signature");
113
+
114
+ if (typeof rawPayload !== "string") {
115
+ throw new Error("Missing transloadit payload");
116
+ }
117
+
118
+ return {
119
+ payload: JSON.parse(rawPayload),
120
+ rawBody: rawPayload,
121
+ signature: typeof signature === "string" ? signature : undefined,
122
+ };
123
+ }
124
+
101
125
  function safeCompare(a: string, b: string): boolean {
102
126
  if (a.length !== b.length) return false;
103
127
  let mismatch = 0;
@@ -136,13 +160,15 @@ export async function verifyWebhookSignature(options: {
136
160
  return safeCompare(expected, sig);
137
161
  }
138
162
 
163
+ export type AssemblyResult = AssemblyStatusResults[string][number];
164
+
139
165
  export type AssemblyResultRecord = {
140
166
  stepName: string;
141
- result: Record<string, unknown>;
167
+ result: AssemblyResult;
142
168
  };
143
169
 
144
170
  export function flattenResults(
145
- results: Record<string, Array<Record<string, unknown>>> | undefined,
171
+ results: AssemblyStatusResults | undefined,
146
172
  ): AssemblyResultRecord[] {
147
173
  if (!results) return [];
148
174
  const output: AssemblyResultRecord[] = [];
@@ -2,13 +2,13 @@
2
2
 
3
3
  import { createHmac } from "node:crypto";
4
4
  import { convexTest } from "convex-test";
5
- import { describe, expect, test } from "vitest";
5
+ import { describe, expect, test, vi } from "vitest";
6
6
  import { api } from "./_generated/api.js";
7
7
  import schema from "./schema.js";
8
8
  import { modules } from "./setup.test.js";
9
9
 
10
- process.env.TRANSLOADIT_AUTH_KEY = "test-key";
11
- process.env.TRANSLOADIT_AUTH_SECRET = "test-secret";
10
+ process.env.TRANSLOADIT_KEY = "test-key";
11
+ process.env.TRANSLOADIT_SECRET = "test-secret";
12
12
 
13
13
  describe("Transloadit component lib", () => {
14
14
  test("handleWebhook stores assembly and results", async () => {
@@ -60,4 +60,74 @@ describe("Transloadit component lib", () => {
60
60
  expect(results).toHaveLength(1);
61
61
  expect(results[0]?.stepName).toBe("resized");
62
62
  });
63
+
64
+ test("refreshAssembly fetches status and stores results", async () => {
65
+ const t = convexTest(schema, modules);
66
+
67
+ const payload = {
68
+ assembly_id: "asm_456",
69
+ ok: "ASSEMBLY_COMPLETED",
70
+ message: "Assembly complete",
71
+ results: {
72
+ resized: [
73
+ {
74
+ id: "file_2",
75
+ ssl_url: "https://example.com/file-2.jpg",
76
+ name: "file-2.jpg",
77
+ size: 54321,
78
+ mime: "image/jpeg",
79
+ },
80
+ ],
81
+ },
82
+ };
83
+
84
+ const fetchMock = vi.fn<typeof fetch>(async () => {
85
+ return new Response(JSON.stringify(payload), {
86
+ status: 200,
87
+ headers: { "content-type": "application/json" },
88
+ });
89
+ });
90
+
91
+ vi.stubGlobal("fetch", fetchMock);
92
+
93
+ try {
94
+ const result = await t.action(api.lib.refreshAssembly, {
95
+ assemblyId: "asm_456",
96
+ config: { authKey: "test-key", authSecret: "test-secret" },
97
+ });
98
+
99
+ expect(result.assemblyId).toBe("asm_456");
100
+ expect(result.ok).toBe("ASSEMBLY_COMPLETED");
101
+
102
+ const requestInfo = fetchMock.mock.calls[0]?.[0];
103
+ const requestUrl =
104
+ typeof requestInfo === "string"
105
+ ? requestInfo
106
+ : requestInfo instanceof URL
107
+ ? requestInfo.toString()
108
+ : requestInfo instanceof Request
109
+ ? requestInfo.url
110
+ : "";
111
+ if (!requestUrl) {
112
+ throw new Error("Expected fetch to be called with a URL string");
113
+ }
114
+ const url = new URL(requestUrl);
115
+ expect(url.origin).toBe("https://api2.transloadit.com");
116
+ expect(url.searchParams.get("signature")).toBeTruthy();
117
+ expect(url.searchParams.get("params")).toBeTruthy();
118
+
119
+ const assembly = await t.query(api.lib.getAssemblyStatus, {
120
+ assemblyId: "asm_456",
121
+ });
122
+ expect(assembly?.ok).toBe("ASSEMBLY_COMPLETED");
123
+
124
+ const results = await t.query(api.lib.listResults, {
125
+ assemblyId: "asm_456",
126
+ });
127
+ expect(results).toHaveLength(1);
128
+ expect(results[0]?.stepName).toBe("resized");
129
+ } finally {
130
+ vi.unstubAllGlobals();
131
+ }
132
+ });
63
133
  });