@transloadit/convex 0.0.2 → 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.
@@ -1,7 +1,10 @@
1
+ import type { AssemblyStatus } from "@transloadit/types/assemblyStatus";
2
+ import type { AssemblyInstructionsInput } from "@transloadit/types/template";
3
+ import { anyApi, type FunctionReference } from "convex/server";
1
4
  import { type Infer, v } from "convex/values";
2
- import { internal } from "./_generated/api.js";
3
5
  import {
4
6
  action,
7
+ internalAction,
5
8
  internalMutation,
6
9
  mutation,
7
10
  query,
@@ -15,6 +18,101 @@ import {
15
18
 
16
19
  const TRANSLOADIT_ASSEMBLY_URL = "https://api2.transloadit.com/assemblies";
17
20
 
21
+ type ProcessWebhookResult = {
22
+ assemblyId: string;
23
+ resultCount: number;
24
+ ok?: string;
25
+ status?: string;
26
+ };
27
+
28
+ type InternalApi = {
29
+ lib: {
30
+ upsertAssembly: FunctionReference<
31
+ "mutation",
32
+ "internal",
33
+ Record<string, unknown>,
34
+ unknown
35
+ >;
36
+ replaceResultsForAssembly: FunctionReference<
37
+ "mutation",
38
+ "internal",
39
+ Record<string, unknown>,
40
+ unknown
41
+ >;
42
+ processWebhook: FunctionReference<
43
+ "action",
44
+ "internal",
45
+ Record<string, unknown>,
46
+ ProcessWebhookResult
47
+ >;
48
+ };
49
+ };
50
+
51
+ const internal = anyApi as unknown as InternalApi;
52
+
53
+ const resolveAssemblyId = (payload: AssemblyStatus): string => {
54
+ if (typeof payload.assembly_id === "string") return payload.assembly_id;
55
+ if (typeof payload.assemblyId === "string") return payload.assemblyId;
56
+ return "";
57
+ };
58
+
59
+ const buildSignedAssemblyUrl = async (
60
+ assemblyId: string,
61
+ authKey: string,
62
+ authSecret: string,
63
+ ): Promise<string> => {
64
+ const params = JSON.stringify({
65
+ auth: {
66
+ key: authKey,
67
+ expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
68
+ },
69
+ });
70
+ const signature = await signTransloaditParams(params, authSecret);
71
+ const url = new URL(`${TRANSLOADIT_ASSEMBLY_URL}/${assemblyId}`);
72
+ url.searchParams.set("signature", signature);
73
+ url.searchParams.set("params", params);
74
+ return url.toString();
75
+ };
76
+
77
+ const applyAssemblyStatus = async (
78
+ ctx: Pick<import("./_generated/server.js").FunctionCtx, "runMutation">,
79
+ payload: AssemblyStatus,
80
+ ) => {
81
+ const assemblyId = resolveAssemblyId(payload);
82
+ if (!assemblyId) {
83
+ throw new Error("Webhook payload missing assembly_id");
84
+ }
85
+
86
+ const results = flattenResults(payload.results ?? undefined);
87
+
88
+ await ctx.runMutation(internal.lib.upsertAssembly, {
89
+ assemblyId,
90
+ status: typeof payload.ok === "string" ? payload.ok : undefined,
91
+ ok: typeof payload.ok === "string" ? payload.ok : undefined,
92
+ message: typeof payload.message === "string" ? payload.message : undefined,
93
+ templateId:
94
+ typeof payload.template_id === "string" ? payload.template_id : undefined,
95
+ notifyUrl:
96
+ typeof payload.notify_url === "string" ? payload.notify_url : undefined,
97
+ uploads: payload.uploads,
98
+ results: payload.results,
99
+ error: payload.error,
100
+ raw: payload,
101
+ });
102
+
103
+ await ctx.runMutation(internal.lib.replaceResultsForAssembly, {
104
+ assemblyId,
105
+ results,
106
+ });
107
+
108
+ return {
109
+ assemblyId,
110
+ resultCount: results.length,
111
+ ok: typeof payload.ok === "string" ? payload.ok : undefined,
112
+ status: typeof payload.ok === "string" ? payload.ok : undefined,
113
+ };
114
+ };
115
+
18
116
  export const vAssembly = v.object({
19
117
  _id: v.id("assemblies"),
20
118
  _creationTime: v.number(),
@@ -25,9 +123,9 @@ export const vAssembly = v.object({
25
123
  templateId: v.optional(v.string()),
26
124
  notifyUrl: v.optional(v.string()),
27
125
  numExpectedUploadFiles: v.optional(v.number()),
28
- fields: v.optional(v.any()),
29
- uploads: v.optional(v.any()),
30
- results: v.optional(v.any()),
126
+ fields: v.optional(v.record(v.string(), v.any())),
127
+ uploads: v.optional(v.array(v.any())),
128
+ results: v.optional(v.record(v.string(), v.array(v.any()))),
31
129
  error: v.optional(v.any()),
32
130
  raw: v.optional(v.any()),
33
131
  createdAt: v.number(),
@@ -60,12 +158,12 @@ export const vTransloaditConfig = v.object({
60
158
 
61
159
  const vAssemblyBaseArgs = {
62
160
  templateId: v.optional(v.string()),
63
- steps: v.optional(v.any()),
64
- fields: v.optional(v.any()),
161
+ steps: v.optional(v.record(v.string(), v.any())),
162
+ fields: v.optional(v.record(v.string(), v.any())),
65
163
  notifyUrl: v.optional(v.string()),
66
164
  numExpectedUploadFiles: v.optional(v.number()),
67
165
  expires: v.optional(v.string()),
68
- additionalParams: v.optional(v.any()),
166
+ additionalParams: v.optional(v.record(v.string(), v.any())),
69
167
  userId: v.optional(v.string()),
70
168
  };
71
169
 
@@ -78,9 +176,9 @@ export const upsertAssembly = internalMutation({
78
176
  templateId: v.optional(v.string()),
79
177
  notifyUrl: v.optional(v.string()),
80
178
  numExpectedUploadFiles: v.optional(v.number()),
81
- fields: v.optional(v.any()),
82
- uploads: v.optional(v.any()),
83
- results: v.optional(v.any()),
179
+ fields: v.optional(v.record(v.string(), v.any())),
180
+ uploads: v.optional(v.array(v.any())),
181
+ results: v.optional(v.record(v.string(), v.array(v.any()))),
84
182
  error: v.optional(v.any()),
85
183
  raw: v.optional(v.any()),
86
184
  userId: v.optional(v.string()),
@@ -188,10 +286,10 @@ export const createAssembly = action({
188
286
  const { paramsString, params } = buildTransloaditParams({
189
287
  authKey: args.config.authKey,
190
288
  templateId: args.templateId,
191
- steps: args.steps as Record<string, unknown> | undefined,
192
- fields: args.fields as Record<string, unknown> | undefined,
289
+ steps: args.steps as AssemblyInstructionsInput["steps"],
290
+ fields: args.fields as AssemblyInstructionsInput["fields"],
193
291
  notifyUrl: args.notifyUrl,
194
- numExpectedUploadFiles: args.numExpectedUploadFiles,
292
+ numExpectedUploadFiles: undefined,
195
293
  expires: args.expires,
196
294
  additionalParams: args.additionalParams as
197
295
  | Record<string, unknown>
@@ -206,6 +304,12 @@ export const createAssembly = action({
206
304
  const formData = new FormData();
207
305
  formData.append("params", paramsString);
208
306
  formData.append("signature", signature);
307
+ if (typeof args.numExpectedUploadFiles === "number") {
308
+ formData.append(
309
+ "tus_num_expected_upload_files",
310
+ String(args.numExpectedUploadFiles),
311
+ );
312
+ }
209
313
 
210
314
  const response = await fetch(TRANSLOADIT_ASSEMBLY_URL, {
211
315
  method: "POST",
@@ -250,72 +354,30 @@ export const createAssembly = action({
250
354
  },
251
355
  });
252
356
 
253
- export const generateUploadParams = action({
254
- args: {
255
- config: vTransloaditConfig,
256
- ...vAssemblyBaseArgs,
257
- },
258
- returns: v.object({
259
- params: v.string(),
260
- signature: v.string(),
261
- url: v.string(),
262
- }),
263
- handler: async (_ctx, args) => {
264
- const { paramsString } = buildTransloaditParams({
265
- authKey: args.config.authKey,
266
- templateId: args.templateId,
267
- steps: args.steps as Record<string, unknown> | undefined,
268
- fields: args.fields as Record<string, unknown> | undefined,
269
- notifyUrl: args.notifyUrl,
270
- numExpectedUploadFiles: args.numExpectedUploadFiles,
271
- expires: args.expires,
272
- additionalParams: args.additionalParams as
273
- | Record<string, unknown>
274
- | undefined,
275
- });
276
-
277
- const signature = await signTransloaditParams(
278
- paramsString,
279
- args.config.authSecret,
280
- );
281
-
282
- return {
283
- params: paramsString,
284
- signature,
285
- url: TRANSLOADIT_ASSEMBLY_URL,
286
- };
287
- },
288
- });
357
+ const vWebhookArgs = {
358
+ payload: v.any(),
359
+ rawBody: v.optional(v.string()),
360
+ signature: v.optional(v.string()),
361
+ verifySignature: v.optional(v.boolean()),
362
+ authSecret: v.optional(v.string()),
363
+ };
289
364
 
290
- export const handleWebhook = action({
291
- args: {
292
- payload: v.any(),
293
- rawBody: v.optional(v.string()),
294
- signature: v.optional(v.string()),
295
- verifySignature: v.optional(v.boolean()),
296
- config: v.optional(
297
- v.object({
298
- authSecret: v.string(),
299
- }),
300
- ),
301
- },
365
+ export const processWebhook = internalAction({
366
+ args: vWebhookArgs,
302
367
  returns: v.object({
303
368
  assemblyId: v.string(),
304
369
  resultCount: v.number(),
370
+ ok: v.optional(v.string()),
371
+ status: v.optional(v.string()),
305
372
  }),
306
373
  handler: async (ctx, args) => {
307
374
  const rawBody = args.rawBody ?? JSON.stringify(args.payload ?? {});
308
375
  const shouldVerify = args.verifySignature ?? true;
309
- const authSecret =
310
- args.config?.authSecret ??
311
- process.env.TRANSLOADIT_AUTH_SECRET ??
312
- process.env.TRANSLOADIT_SECRET;
376
+ const authSecret = args.authSecret ?? process.env.TRANSLOADIT_SECRET;
313
377
 
314
378
  if (shouldVerify) {
315
379
  if (!authSecret) {
316
- throw new Error(
317
- "Missing TRANSLOADIT_AUTH_SECRET for webhook validation",
318
- );
380
+ throw new Error("Missing TRANSLOADIT_SECRET for webhook validation");
319
381
  }
320
382
  const verified = await verifyWebhookSignature({
321
383
  rawBody,
@@ -327,41 +389,103 @@ export const handleWebhook = action({
327
389
  }
328
390
  }
329
391
 
330
- const payload = args.payload as Record<string, unknown>;
331
- const assemblyId =
332
- typeof payload.assembly_id === "string"
333
- ? payload.assembly_id
334
- : typeof payload.assemblyId === "string"
335
- ? payload.assemblyId
336
- : "";
392
+ return applyAssemblyStatus(ctx, args.payload as AssemblyStatus);
393
+ },
394
+ });
395
+
396
+ export const handleWebhook = action({
397
+ args: {
398
+ ...vWebhookArgs,
399
+ config: v.optional(
400
+ v.object({
401
+ authSecret: v.string(),
402
+ }),
403
+ ),
404
+ },
405
+ returns: v.object({
406
+ assemblyId: v.string(),
407
+ resultCount: v.number(),
408
+ ok: v.optional(v.string()),
409
+ status: v.optional(v.string()),
410
+ }),
411
+ handler: async (ctx, args) => {
412
+ return ctx.runAction(internal.lib.processWebhook, {
413
+ payload: args.payload,
414
+ rawBody: args.rawBody,
415
+ signature: args.signature,
416
+ verifySignature: args.verifySignature,
417
+ authSecret: args.config?.authSecret,
418
+ });
419
+ },
420
+ });
337
421
 
422
+ export const queueWebhook = action({
423
+ args: {
424
+ ...vWebhookArgs,
425
+ config: v.optional(
426
+ v.object({
427
+ authSecret: v.string(),
428
+ }),
429
+ ),
430
+ },
431
+ returns: v.object({
432
+ assemblyId: v.string(),
433
+ queued: v.boolean(),
434
+ }),
435
+ handler: async (ctx, args) => {
436
+ const payload = args.payload as AssemblyStatus;
437
+ const assemblyId = resolveAssemblyId(payload);
338
438
  if (!assemblyId) {
339
439
  throw new Error("Webhook payload missing assembly_id");
340
440
  }
341
441
 
342
- const results = flattenResults(
343
- (payload.results as Record<string, Array<Record<string, unknown>>>) ??
344
- undefined,
345
- );
346
-
347
- await ctx.runMutation(internal.lib.upsertAssembly, {
348
- assemblyId,
349
- status: typeof payload.ok === "string" ? payload.ok : undefined,
350
- ok: typeof payload.ok === "string" ? payload.ok : undefined,
351
- message:
352
- typeof payload.message === "string" ? payload.message : undefined,
353
- uploads: payload.uploads,
354
- results: payload.results,
355
- error: payload.error,
356
- raw: payload,
442
+ await ctx.scheduler.runAfter(0, internal.lib.processWebhook, {
443
+ payload: args.payload,
444
+ rawBody: args.rawBody,
445
+ signature: args.signature,
446
+ verifySignature: args.verifySignature,
447
+ authSecret: args.config?.authSecret,
357
448
  });
358
449
 
359
- await ctx.runMutation(internal.lib.replaceResultsForAssembly, {
360
- assemblyId,
361
- results,
362
- });
450
+ return { assemblyId, queued: true };
451
+ },
452
+ });
453
+
454
+ export const refreshAssembly = action({
455
+ args: {
456
+ assemblyId: v.string(),
457
+ config: v.optional(
458
+ v.object({
459
+ authKey: v.string(),
460
+ authSecret: v.string(),
461
+ }),
462
+ ),
463
+ },
464
+ returns: v.object({
465
+ assemblyId: v.string(),
466
+ resultCount: v.number(),
467
+ ok: v.optional(v.string()),
468
+ status: v.optional(v.string()),
469
+ }),
470
+ handler: async (ctx, args) => {
471
+ const { assemblyId } = args;
472
+ const authKey = args.config?.authKey ?? process.env.TRANSLOADIT_KEY;
473
+ const authSecret =
474
+ args.config?.authSecret ?? process.env.TRANSLOADIT_SECRET;
475
+ const url =
476
+ authKey && authSecret
477
+ ? await buildSignedAssemblyUrl(assemblyId, authKey, authSecret)
478
+ : `${TRANSLOADIT_ASSEMBLY_URL}/${assemblyId}`;
479
+
480
+ const response = await fetch(url);
481
+ const payload = (await response.json()) as AssemblyStatus;
482
+ if (!response.ok) {
483
+ throw new Error(
484
+ `Transloadit status error ${response.status}: ${JSON.stringify(payload)}`,
485
+ );
486
+ }
363
487
 
364
- return { assemblyId, resultCount: results.length };
488
+ return applyAssemblyStatus(ctx, payload);
365
489
  },
366
490
  });
367
491
 
@@ -437,7 +561,7 @@ export const storeAssemblyMetadata = mutation({
437
561
  args: {
438
562
  assemblyId: v.string(),
439
563
  userId: v.optional(v.string()),
440
- fields: v.optional(v.any()),
564
+ fields: v.optional(v.record(v.string(), v.any())),
441
565
  },
442
566
  returns: v.union(vAssembly, v.null()),
443
567
  handler: async (ctx, args) => {
@@ -20,9 +20,9 @@ export default defineSchema({
20
20
  templateId: v.optional(v.string()),
21
21
  notifyUrl: v.optional(v.string()),
22
22
  numExpectedUploadFiles: v.optional(v.number()),
23
- fields: v.optional(v.any()),
24
- uploads: v.optional(v.any()),
25
- results: v.optional(v.any()),
23
+ fields: v.optional(v.record(v.string(), v.any())),
24
+ uploads: v.optional(v.array(v.any())),
25
+ results: v.optional(v.record(v.string(), v.array(v.any()))),
26
26
  error: v.optional(v.any()),
27
27
  raw: v.optional(v.any()),
28
28
  createdAt: v.number(),