@transloadit/convex 0.0.3 → 0.0.5
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.
- package/README.md +154 -122
- package/dist/client/index.d.ts +54 -13
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +48 -5
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -2
- package/dist/component/_generated/component.d.ts +11 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/dataModel.d.ts +1 -1
- package/dist/component/_generated/server.d.ts +1 -1
- package/dist/component/apiUtils.d.ts +26 -6
- package/dist/component/apiUtils.d.ts.map +1 -1
- package/dist/component/apiUtils.js +48 -38
- package/dist/component/apiUtils.js.map +1 -1
- package/dist/component/lib.d.ts +37 -8
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +145 -18
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +9 -6
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +4 -8
- package/dist/component/schema.js.map +1 -1
- package/dist/debug/index.d.ts +19 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/index.js +49 -0
- package/dist/debug/index.js.map +1 -0
- package/dist/react/index.d.ts +201 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +674 -94
- package/dist/react/index.js.map +1 -1
- package/dist/shared/assemblyUrls.d.ts +10 -0
- package/dist/shared/assemblyUrls.d.ts.map +1 -0
- package/dist/shared/assemblyUrls.js +26 -0
- package/dist/shared/assemblyUrls.js.map +1 -0
- package/dist/shared/errors.d.ts +7 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +10 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/pollAssembly.d.ts +12 -0
- package/dist/shared/pollAssembly.d.ts.map +1 -0
- package/dist/shared/pollAssembly.js +50 -0
- package/dist/shared/pollAssembly.js.map +1 -0
- package/dist/shared/resultTypes.d.ts +37 -0
- package/dist/shared/resultTypes.d.ts.map +1 -0
- package/dist/shared/resultTypes.js +2 -0
- package/dist/shared/resultTypes.js.map +1 -0
- package/dist/shared/resultUtils.d.ts +4 -0
- package/dist/shared/resultUtils.d.ts.map +1 -0
- package/dist/shared/resultUtils.js +69 -0
- package/dist/shared/resultUtils.js.map +1 -0
- package/dist/shared/tusUpload.d.ts +13 -0
- package/dist/shared/tusUpload.d.ts.map +1 -0
- package/dist/shared/tusUpload.js +32 -0
- package/dist/shared/tusUpload.js.map +1 -0
- package/dist/test/index.d.ts +9 -4
- package/dist/test/index.d.ts.map +1 -1
- package/dist/test/nodeModules.d.ts +2 -0
- package/dist/test/nodeModules.d.ts.map +1 -0
- package/dist/test/nodeModules.js +19 -0
- package/dist/test/nodeModules.js.map +1 -0
- package/package.json +40 -7
- package/src/client/index.ts +111 -9
- package/src/component/_generated/api.ts +2 -2
- package/src/component/_generated/component.ts +14 -0
- package/src/component/_generated/dataModel.ts +1 -1
- package/src/component/_generated/server.ts +1 -1
- package/src/component/apiUtils.test.ts +166 -2
- package/src/component/apiUtils.ts +96 -64
- package/src/component/lib.test.ts +213 -4
- package/src/component/lib.ts +192 -25
- package/src/component/schema.ts +4 -11
- package/src/debug/index.ts +84 -0
- package/src/react/index.test.tsx +340 -0
- package/src/react/index.tsx +1089 -179
- package/src/react/uploadWithTus.test.tsx +192 -0
- package/src/shared/assemblyUrls.test.ts +71 -0
- package/src/shared/assemblyUrls.ts +59 -0
- package/src/shared/errors.ts +23 -0
- package/src/shared/pollAssembly.ts +65 -0
- package/src/shared/resultTypes.ts +44 -0
- package/src/shared/resultUtils.test.ts +29 -0
- package/src/shared/resultUtils.ts +71 -0
- package/src/shared/tusUpload.ts +59 -0
- package/src/test/index.ts +1 -1
- package/src/test/nodeModules.ts +19 -0
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
import { createHmac } from "node:crypto";
|
|
4
4
|
import { convexTest } from "convex-test";
|
|
5
5
|
import { describe, expect, test, vi } from "vitest";
|
|
6
|
-
import { api } from "./_generated/api.
|
|
7
|
-
import schema from "./schema.
|
|
8
|
-
import { modules } from "./setup.test.
|
|
6
|
+
import { api } from "./_generated/api.ts";
|
|
7
|
+
import schema from "./schema.ts";
|
|
8
|
+
import { modules } from "./setup.test.ts";
|
|
9
9
|
|
|
10
10
|
process.env.TRANSLOADIT_KEY = "test-key";
|
|
11
11
|
process.env.TRANSLOADIT_SECRET = "test-secret";
|
|
@@ -40,7 +40,6 @@ describe("Transloadit component lib", () => {
|
|
|
40
40
|
payload,
|
|
41
41
|
rawBody,
|
|
42
42
|
signature: `sha1:${signature}`,
|
|
43
|
-
verifySignature: true,
|
|
44
43
|
});
|
|
45
44
|
|
|
46
45
|
expect(result.assemblyId).toBe("asm_123");
|
|
@@ -61,6 +60,216 @@ describe("Transloadit component lib", () => {
|
|
|
61
60
|
expect(results[0]?.stepName).toBe("resized");
|
|
62
61
|
});
|
|
63
62
|
|
|
63
|
+
test("listAlbumResults returns album-scoped results", async () => {
|
|
64
|
+
const t = convexTest(schema, modules);
|
|
65
|
+
|
|
66
|
+
const payload = {
|
|
67
|
+
assembly_id: "asm_album",
|
|
68
|
+
ok: "ASSEMBLY_COMPLETED",
|
|
69
|
+
fields: {
|
|
70
|
+
album: "wedding-gallery",
|
|
71
|
+
userId: "user_123",
|
|
72
|
+
},
|
|
73
|
+
results: {
|
|
74
|
+
resized: [
|
|
75
|
+
{
|
|
76
|
+
id: "file_album",
|
|
77
|
+
ssl_url: "https://example.com/album.jpg",
|
|
78
|
+
name: "album.jpg",
|
|
79
|
+
size: 100,
|
|
80
|
+
mime: "image/jpeg",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const rawBody = JSON.stringify(payload);
|
|
87
|
+
const signature = createHmac("sha1", "test-secret")
|
|
88
|
+
.update(rawBody)
|
|
89
|
+
.digest("hex");
|
|
90
|
+
|
|
91
|
+
await t.action(api.lib.handleWebhook, {
|
|
92
|
+
payload,
|
|
93
|
+
rawBody,
|
|
94
|
+
signature: `sha1:${signature}`,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const results = await t.query(api.lib.listAlbumResults, {
|
|
98
|
+
album: "wedding-gallery",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(results).toHaveLength(1);
|
|
102
|
+
expect(results[0]?.album).toBe("wedding-gallery");
|
|
103
|
+
expect(results[0]?.userId).toBe("user_123");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("handleWebhook stores url when ssl_url missing", async () => {
|
|
107
|
+
const t = convexTest(schema, modules);
|
|
108
|
+
|
|
109
|
+
const payload = {
|
|
110
|
+
assembly_id: "asm_url",
|
|
111
|
+
ok: "ASSEMBLY_COMPLETED",
|
|
112
|
+
results: {
|
|
113
|
+
stored: [
|
|
114
|
+
{
|
|
115
|
+
id: "file_3",
|
|
116
|
+
url: "https://example.com/file-3.jpg",
|
|
117
|
+
name: "file-3.jpg",
|
|
118
|
+
size: 42,
|
|
119
|
+
mime: "image/jpeg",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const rawBody = JSON.stringify(payload);
|
|
126
|
+
const signature = createHmac("sha1", "test-secret")
|
|
127
|
+
.update(rawBody)
|
|
128
|
+
.digest("hex");
|
|
129
|
+
|
|
130
|
+
await t.action(api.lib.handleWebhook, {
|
|
131
|
+
payload,
|
|
132
|
+
rawBody,
|
|
133
|
+
signature: `sha1:${signature}`,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const results = await t.query(api.lib.listResults, {
|
|
137
|
+
assemblyId: "asm_url",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(results).toHaveLength(1);
|
|
141
|
+
expect(results[0]?.sslUrl).toBe("https://example.com/file-3.jpg");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("listResults exposes expected fields for common robot outputs", async () => {
|
|
145
|
+
const t = convexTest(schema, modules);
|
|
146
|
+
|
|
147
|
+
const payload = {
|
|
148
|
+
assembly_id: "asm_schema",
|
|
149
|
+
ok: "ASSEMBLY_COMPLETED",
|
|
150
|
+
results: {
|
|
151
|
+
images_resized: [
|
|
152
|
+
{
|
|
153
|
+
id: "img_1",
|
|
154
|
+
ssl_url: "https://example.com/img.jpg",
|
|
155
|
+
name: "img.jpg",
|
|
156
|
+
mime: "image/jpeg",
|
|
157
|
+
width: 1600,
|
|
158
|
+
height: 1200,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
videos_encoded: [
|
|
162
|
+
{
|
|
163
|
+
id: "vid_1",
|
|
164
|
+
ssl_url: "https://example.com/vid.mp4",
|
|
165
|
+
name: "vid.mp4",
|
|
166
|
+
mime: "video/mp4",
|
|
167
|
+
duration: 12.5,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
videos_thumbs_output: [
|
|
171
|
+
{
|
|
172
|
+
id: "thumb_1",
|
|
173
|
+
ssl_url: "https://example.com/thumb.jpg",
|
|
174
|
+
name: "thumb.jpg",
|
|
175
|
+
mime: "image/jpeg",
|
|
176
|
+
original_id: "vid_1",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const rawBody = JSON.stringify(payload);
|
|
183
|
+
const signature = createHmac("sha1", "test-secret")
|
|
184
|
+
.update(rawBody)
|
|
185
|
+
.digest("hex");
|
|
186
|
+
|
|
187
|
+
await t.action(api.lib.handleWebhook, {
|
|
188
|
+
payload,
|
|
189
|
+
rawBody,
|
|
190
|
+
signature: `sha1:${signature}`,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const results = await t.query(api.lib.listResults, {
|
|
194
|
+
assemblyId: "asm_schema",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(results).toHaveLength(3);
|
|
198
|
+
|
|
199
|
+
const byStep = new Map(results.map((result) => [result.stepName, result]));
|
|
200
|
+
const image = byStep.get("images_resized");
|
|
201
|
+
const video = byStep.get("videos_encoded");
|
|
202
|
+
const thumb = byStep.get("videos_thumbs_output");
|
|
203
|
+
|
|
204
|
+
expect(image?.sslUrl).toBe("https://example.com/img.jpg");
|
|
205
|
+
expect(image?.mime).toBe("image/jpeg");
|
|
206
|
+
expect(image?.raw?.width).toBe(1600);
|
|
207
|
+
expect(image?.raw?.height).toBe(1200);
|
|
208
|
+
|
|
209
|
+
expect(video?.sslUrl).toBe("https://example.com/vid.mp4");
|
|
210
|
+
expect(video?.mime).toBe("video/mp4");
|
|
211
|
+
expect(video?.raw?.duration).toBe(12.5);
|
|
212
|
+
|
|
213
|
+
expect(thumb?.sslUrl).toBe("https://example.com/thumb.jpg");
|
|
214
|
+
expect(thumb?.raw?.original_id).toBe("vid_1");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("handleWebhook requires rawBody when verifying signature", async () => {
|
|
218
|
+
const t = convexTest(schema, modules);
|
|
219
|
+
const payload = { assembly_id: "asm_missing" };
|
|
220
|
+
const signature = createHmac("sha1", "test-secret")
|
|
221
|
+
.update(JSON.stringify(payload))
|
|
222
|
+
.digest("hex");
|
|
223
|
+
|
|
224
|
+
await expect(
|
|
225
|
+
t.action(api.lib.handleWebhook, {
|
|
226
|
+
payload,
|
|
227
|
+
signature: `sha1:${signature}`,
|
|
228
|
+
}),
|
|
229
|
+
).rejects.toThrow("Missing rawBody for webhook verification");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("handleWebhook can skip verification when configured", async () => {
|
|
233
|
+
const t = convexTest(schema, modules);
|
|
234
|
+
const payload = {
|
|
235
|
+
assembly_id: "asm_skip",
|
|
236
|
+
ok: "ASSEMBLY_COMPLETED",
|
|
237
|
+
results: {
|
|
238
|
+
resized: [
|
|
239
|
+
{
|
|
240
|
+
id: "file_skip",
|
|
241
|
+
ssl_url: "https://example.com/skip.jpg",
|
|
242
|
+
name: "skip.jpg",
|
|
243
|
+
size: 123,
|
|
244
|
+
mime: "image/jpeg",
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = await t.action(api.lib.handleWebhook, {
|
|
251
|
+
payload,
|
|
252
|
+
verifySignature: false,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(result.assemblyId).toBe("asm_skip");
|
|
256
|
+
expect(result.resultCount).toBe(1);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("queueWebhook rejects invalid signature", async () => {
|
|
260
|
+
const t = convexTest(schema, modules);
|
|
261
|
+
const payload = { assembly_id: "asm_bad" };
|
|
262
|
+
const rawBody = JSON.stringify(payload);
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
t.action(api.lib.queueWebhook, {
|
|
266
|
+
payload,
|
|
267
|
+
rawBody,
|
|
268
|
+
signature: "sha1:bad",
|
|
269
|
+
}),
|
|
270
|
+
).rejects.toThrow("Invalid Transloadit webhook signature");
|
|
271
|
+
});
|
|
272
|
+
|
|
64
273
|
test("refreshAssembly fetches status and stores results", async () => {
|
|
65
274
|
const t = convexTest(schema, modules);
|
|
66
275
|
|
package/src/component/lib.ts
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import type { AssemblyStatus } from "@transloadit/
|
|
2
|
-
import type { AssemblyInstructionsInput } from "@transloadit/
|
|
1
|
+
import type { AssemblyStatus } from "@transloadit/zod/v3/assemblyStatus";
|
|
2
|
+
import type { AssemblyInstructionsInput } from "@transloadit/zod/v3/template";
|
|
3
3
|
import { anyApi, type FunctionReference } from "convex/server";
|
|
4
4
|
import { type Infer, v } from "convex/values";
|
|
5
|
+
import { parseAssemblyStatus } from "../shared/assemblyUrls.ts";
|
|
6
|
+
import { transloaditError } from "../shared/errors.ts";
|
|
7
|
+
import { getResultUrl } from "../shared/resultUtils.ts";
|
|
5
8
|
import {
|
|
6
9
|
action,
|
|
7
10
|
internalAction,
|
|
8
11
|
internalMutation,
|
|
9
12
|
mutation,
|
|
10
13
|
query,
|
|
11
|
-
} from "./_generated/server.
|
|
14
|
+
} from "./_generated/server.ts";
|
|
12
15
|
import {
|
|
13
16
|
buildTransloaditParams,
|
|
14
17
|
flattenResults,
|
|
15
18
|
signTransloaditParams,
|
|
16
19
|
verifyWebhookSignature,
|
|
17
|
-
} from "./apiUtils.
|
|
20
|
+
} from "./apiUtils.ts";
|
|
18
21
|
|
|
19
22
|
const TRANSLOADIT_ASSEMBLY_URL = "https://api2.transloadit.com/assemblies";
|
|
20
23
|
|
|
@@ -56,6 +59,32 @@ const resolveAssemblyId = (payload: AssemblyStatus): string => {
|
|
|
56
59
|
return "";
|
|
57
60
|
};
|
|
58
61
|
|
|
62
|
+
const getFieldString = (fields: unknown, key: string): string | undefined => {
|
|
63
|
+
if (!fields || typeof fields !== "object") return undefined;
|
|
64
|
+
const value = (fields as Record<string, unknown>)[key];
|
|
65
|
+
return typeof value === "string" ? value : undefined;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const parseAssemblyPayload = (payload: unknown): AssemblyStatus => {
|
|
69
|
+
const parsed = parseAssemblyStatus(payload);
|
|
70
|
+
if (!parsed) {
|
|
71
|
+
throw transloaditError("payload", "Invalid Transloadit payload");
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const resolveWebhookRawBody = (args: {
|
|
77
|
+
payload: unknown;
|
|
78
|
+
rawBody?: string;
|
|
79
|
+
verifySignature?: boolean;
|
|
80
|
+
}) => {
|
|
81
|
+
if (typeof args.rawBody === "string") return args.rawBody;
|
|
82
|
+
if (args.verifySignature === false) {
|
|
83
|
+
return JSON.stringify(args.payload ?? {});
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
|
|
59
88
|
const buildSignedAssemblyUrl = async (
|
|
60
89
|
assemblyId: string,
|
|
61
90
|
authKey: string,
|
|
@@ -75,12 +104,12 @@ const buildSignedAssemblyUrl = async (
|
|
|
75
104
|
};
|
|
76
105
|
|
|
77
106
|
const applyAssemblyStatus = async (
|
|
78
|
-
ctx: Pick<import("./_generated/server.
|
|
107
|
+
ctx: Pick<import("./_generated/server.ts").FunctionCtx, "runMutation">,
|
|
79
108
|
payload: AssemblyStatus,
|
|
80
109
|
) => {
|
|
81
110
|
const assemblyId = resolveAssemblyId(payload);
|
|
82
111
|
if (!assemblyId) {
|
|
83
|
-
throw
|
|
112
|
+
throw transloaditError("webhook", "Webhook payload missing assembly_id");
|
|
84
113
|
}
|
|
85
114
|
|
|
86
115
|
const results = flattenResults(payload.results ?? undefined);
|
|
@@ -94,10 +123,15 @@ const applyAssemblyStatus = async (
|
|
|
94
123
|
typeof payload.template_id === "string" ? payload.template_id : undefined,
|
|
95
124
|
notifyUrl:
|
|
96
125
|
typeof payload.notify_url === "string" ? payload.notify_url : undefined,
|
|
126
|
+
fields: payload.fields,
|
|
97
127
|
uploads: payload.uploads,
|
|
98
128
|
results: payload.results,
|
|
99
129
|
error: payload.error,
|
|
100
130
|
raw: payload,
|
|
131
|
+
userId:
|
|
132
|
+
typeof payload.user_id === "string"
|
|
133
|
+
? payload.user_id
|
|
134
|
+
: getFieldString(payload.fields, "userId"),
|
|
101
135
|
});
|
|
102
136
|
|
|
103
137
|
await ctx.runMutation(internal.lib.replaceResultsForAssembly, {
|
|
@@ -139,6 +173,8 @@ export const vAssemblyResult = v.object({
|
|
|
139
173
|
_id: v.id("results"),
|
|
140
174
|
_creationTime: v.number(),
|
|
141
175
|
assemblyId: v.string(),
|
|
176
|
+
album: v.optional(v.string()),
|
|
177
|
+
userId: v.optional(v.string()),
|
|
142
178
|
stepName: v.string(),
|
|
143
179
|
resultId: v.optional(v.string()),
|
|
144
180
|
sslUrl: v.optional(v.string()),
|
|
@@ -185,6 +221,9 @@ export const upsertAssembly = internalMutation({
|
|
|
185
221
|
},
|
|
186
222
|
returns: v.id("assemblies"),
|
|
187
223
|
handler: async (ctx, args) => {
|
|
224
|
+
// Note: we persist full `raw` + `results` for debugging/fidelity. Large
|
|
225
|
+
// assemblies can hit Convex document size limits; trim or externalize
|
|
226
|
+
// payloads if this becomes an issue for your workload.
|
|
188
227
|
const existing = await ctx.db
|
|
189
228
|
.query("assemblies")
|
|
190
229
|
.withIndex("by_assemblyId", (q) => q.eq("assemblyId", args.assemblyId))
|
|
@@ -244,6 +283,10 @@ export const replaceResultsForAssembly = internalMutation({
|
|
|
244
283
|
},
|
|
245
284
|
returns: v.null(),
|
|
246
285
|
handler: async (ctx, args) => {
|
|
286
|
+
// We store raw result payloads for fidelity. For very large assemblies,
|
|
287
|
+
// consider trimming or externalizing these fields to avoid size limits.
|
|
288
|
+
// This mutation replaces all results in one transaction; extremely large
|
|
289
|
+
// result sets may need batching or external storage to avoid Convex limits.
|
|
247
290
|
const existingResults = await ctx.db
|
|
248
291
|
.query("results")
|
|
249
292
|
.withIndex("by_assemblyId", (q) => q.eq("assemblyId", args.assemblyId))
|
|
@@ -253,14 +296,27 @@ export const replaceResultsForAssembly = internalMutation({
|
|
|
253
296
|
await ctx.db.delete(existing._id);
|
|
254
297
|
}
|
|
255
298
|
|
|
299
|
+
const assembly = await ctx.db
|
|
300
|
+
.query("assemblies")
|
|
301
|
+
.withIndex("by_assemblyId", (q) => q.eq("assemblyId", args.assemblyId))
|
|
302
|
+
.unique();
|
|
303
|
+
const album = getFieldString(assembly?.fields, "album");
|
|
304
|
+
const userId =
|
|
305
|
+
typeof assembly?.userId === "string"
|
|
306
|
+
? assembly.userId
|
|
307
|
+
: getFieldString(assembly?.fields, "userId");
|
|
308
|
+
|
|
256
309
|
const now = Date.now();
|
|
257
310
|
for (const entry of args.results) {
|
|
258
311
|
const raw = entry.result as Record<string, unknown>;
|
|
312
|
+
const sslUrl = getResultUrl(entry.result);
|
|
259
313
|
await ctx.db.insert("results", {
|
|
260
314
|
assemblyId: args.assemblyId,
|
|
315
|
+
album,
|
|
316
|
+
userId,
|
|
261
317
|
stepName: entry.stepName,
|
|
262
318
|
resultId: typeof raw.id === "string" ? raw.id : undefined,
|
|
263
|
-
sslUrl
|
|
319
|
+
sslUrl,
|
|
264
320
|
name: typeof raw.name === "string" ? raw.name : undefined,
|
|
265
321
|
size: typeof raw.size === "number" ? raw.size : undefined,
|
|
266
322
|
mime: typeof raw.mime === "string" ? raw.mime : undefined,
|
|
@@ -318,8 +374,9 @@ export const createAssembly = action({
|
|
|
318
374
|
|
|
319
375
|
const data = (await response.json()) as Record<string, unknown>;
|
|
320
376
|
if (!response.ok) {
|
|
321
|
-
throw
|
|
322
|
-
|
|
377
|
+
throw transloaditError(
|
|
378
|
+
"createAssembly",
|
|
379
|
+
`HTTP ${response.status}: ${JSON.stringify(data)}`,
|
|
323
380
|
);
|
|
324
381
|
}
|
|
325
382
|
|
|
@@ -331,7 +388,10 @@ export const createAssembly = action({
|
|
|
331
388
|
: "";
|
|
332
389
|
|
|
333
390
|
if (!assemblyId) {
|
|
334
|
-
throw
|
|
391
|
+
throw transloaditError(
|
|
392
|
+
"createAssembly",
|
|
393
|
+
"Transloadit response missing assembly_id",
|
|
394
|
+
);
|
|
335
395
|
}
|
|
336
396
|
|
|
337
397
|
await ctx.runMutation(internal.lib.upsertAssembly, {
|
|
@@ -362,6 +422,13 @@ const vWebhookArgs = {
|
|
|
362
422
|
authSecret: v.optional(v.string()),
|
|
363
423
|
};
|
|
364
424
|
|
|
425
|
+
const vPublicWebhookArgs = {
|
|
426
|
+
payload: v.any(),
|
|
427
|
+
rawBody: v.optional(v.string()),
|
|
428
|
+
signature: v.optional(v.string()),
|
|
429
|
+
verifySignature: v.optional(v.boolean()),
|
|
430
|
+
};
|
|
431
|
+
|
|
365
432
|
export const processWebhook = internalAction({
|
|
366
433
|
args: vWebhookArgs,
|
|
367
434
|
returns: v.object({
|
|
@@ -371,13 +438,22 @@ export const processWebhook = internalAction({
|
|
|
371
438
|
status: v.optional(v.string()),
|
|
372
439
|
}),
|
|
373
440
|
handler: async (ctx, args) => {
|
|
374
|
-
const rawBody =
|
|
441
|
+
const rawBody = resolveWebhookRawBody(args);
|
|
375
442
|
const shouldVerify = args.verifySignature ?? true;
|
|
376
443
|
const authSecret = args.authSecret ?? process.env.TRANSLOADIT_SECRET;
|
|
377
444
|
|
|
378
445
|
if (shouldVerify) {
|
|
446
|
+
if (!rawBody) {
|
|
447
|
+
throw transloaditError(
|
|
448
|
+
"webhook",
|
|
449
|
+
"Missing rawBody for webhook verification",
|
|
450
|
+
);
|
|
451
|
+
}
|
|
379
452
|
if (!authSecret) {
|
|
380
|
-
throw
|
|
453
|
+
throw transloaditError(
|
|
454
|
+
"webhook",
|
|
455
|
+
"Missing TRANSLOADIT_SECRET for webhook validation",
|
|
456
|
+
);
|
|
381
457
|
}
|
|
382
458
|
const verified = await verifyWebhookSignature({
|
|
383
459
|
rawBody,
|
|
@@ -385,17 +461,21 @@ export const processWebhook = internalAction({
|
|
|
385
461
|
authSecret,
|
|
386
462
|
});
|
|
387
463
|
if (!verified) {
|
|
388
|
-
throw
|
|
464
|
+
throw transloaditError(
|
|
465
|
+
"webhook",
|
|
466
|
+
"Invalid Transloadit webhook signature",
|
|
467
|
+
);
|
|
389
468
|
}
|
|
390
469
|
}
|
|
391
470
|
|
|
392
|
-
|
|
471
|
+
const parsed = parseAssemblyPayload(args.payload);
|
|
472
|
+
return applyAssemblyStatus(ctx, parsed);
|
|
393
473
|
},
|
|
394
474
|
});
|
|
395
475
|
|
|
396
476
|
export const handleWebhook = action({
|
|
397
477
|
args: {
|
|
398
|
-
...
|
|
478
|
+
...vPublicWebhookArgs,
|
|
399
479
|
config: v.optional(
|
|
400
480
|
v.object({
|
|
401
481
|
authSecret: v.string(),
|
|
@@ -409,11 +489,12 @@ export const handleWebhook = action({
|
|
|
409
489
|
status: v.optional(v.string()),
|
|
410
490
|
}),
|
|
411
491
|
handler: async (ctx, args) => {
|
|
492
|
+
const verifySignature = args.verifySignature ?? true;
|
|
412
493
|
return ctx.runAction(internal.lib.processWebhook, {
|
|
413
494
|
payload: args.payload,
|
|
414
495
|
rawBody: args.rawBody,
|
|
415
496
|
signature: args.signature,
|
|
416
|
-
verifySignature
|
|
497
|
+
verifySignature,
|
|
417
498
|
authSecret: args.config?.authSecret,
|
|
418
499
|
});
|
|
419
500
|
},
|
|
@@ -421,7 +502,7 @@ export const handleWebhook = action({
|
|
|
421
502
|
|
|
422
503
|
export const queueWebhook = action({
|
|
423
504
|
args: {
|
|
424
|
-
...
|
|
505
|
+
...vPublicWebhookArgs,
|
|
425
506
|
config: v.optional(
|
|
426
507
|
v.object({
|
|
427
508
|
authSecret: v.string(),
|
|
@@ -433,17 +514,48 @@ export const queueWebhook = action({
|
|
|
433
514
|
queued: v.boolean(),
|
|
434
515
|
}),
|
|
435
516
|
handler: async (ctx, args) => {
|
|
436
|
-
const
|
|
437
|
-
const
|
|
517
|
+
const rawBody = resolveWebhookRawBody(args);
|
|
518
|
+
const shouldVerify = args.verifySignature ?? true;
|
|
519
|
+
const authSecret =
|
|
520
|
+
args.config?.authSecret ?? process.env.TRANSLOADIT_SECRET;
|
|
521
|
+
|
|
522
|
+
if (shouldVerify) {
|
|
523
|
+
if (!rawBody) {
|
|
524
|
+
throw transloaditError(
|
|
525
|
+
"webhook",
|
|
526
|
+
"Missing rawBody for webhook verification",
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (!authSecret) {
|
|
530
|
+
throw transloaditError(
|
|
531
|
+
"webhook",
|
|
532
|
+
"Missing TRANSLOADIT_SECRET for webhook validation",
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
const verified = await verifyWebhookSignature({
|
|
536
|
+
rawBody,
|
|
537
|
+
signatureHeader: args.signature,
|
|
538
|
+
authSecret,
|
|
539
|
+
});
|
|
540
|
+
if (!verified) {
|
|
541
|
+
throw transloaditError(
|
|
542
|
+
"webhook",
|
|
543
|
+
"Invalid Transloadit webhook signature",
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const parsed = parseAssemblyPayload(args.payload);
|
|
549
|
+
const assemblyId = resolveAssemblyId(parsed);
|
|
438
550
|
if (!assemblyId) {
|
|
439
|
-
throw
|
|
551
|
+
throw transloaditError("webhook", "Webhook payload missing assembly_id");
|
|
440
552
|
}
|
|
441
553
|
|
|
442
554
|
await ctx.scheduler.runAfter(0, internal.lib.processWebhook, {
|
|
443
|
-
payload:
|
|
555
|
+
payload: parsed,
|
|
444
556
|
rawBody: args.rawBody,
|
|
445
557
|
signature: args.signature,
|
|
446
|
-
verifySignature:
|
|
558
|
+
verifySignature: true,
|
|
447
559
|
authSecret: args.config?.authSecret,
|
|
448
560
|
});
|
|
449
561
|
|
|
@@ -478,10 +590,11 @@ export const refreshAssembly = action({
|
|
|
478
590
|
: `${TRANSLOADIT_ASSEMBLY_URL}/${assemblyId}`;
|
|
479
591
|
|
|
480
592
|
const response = await fetch(url);
|
|
481
|
-
const payload = (await response.json())
|
|
593
|
+
const payload = parseAssemblyPayload(await response.json());
|
|
482
594
|
if (!response.ok) {
|
|
483
|
-
throw
|
|
484
|
-
|
|
595
|
+
throw transloaditError(
|
|
596
|
+
"status",
|
|
597
|
+
`HTTP ${response.status}: ${JSON.stringify(payload)}`,
|
|
485
598
|
);
|
|
486
599
|
}
|
|
487
600
|
|
|
@@ -557,6 +670,60 @@ export const listResults = query({
|
|
|
557
670
|
},
|
|
558
671
|
});
|
|
559
672
|
|
|
673
|
+
export const listAlbumResults = query({
|
|
674
|
+
args: {
|
|
675
|
+
album: v.string(),
|
|
676
|
+
limit: v.optional(v.number()),
|
|
677
|
+
},
|
|
678
|
+
returns: v.array(vAssemblyResult),
|
|
679
|
+
handler: async (ctx, args) => {
|
|
680
|
+
return ctx.db
|
|
681
|
+
.query("results")
|
|
682
|
+
.withIndex("by_album", (q) => q.eq("album", args.album))
|
|
683
|
+
.order("desc")
|
|
684
|
+
.take(args.limit ?? 200);
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
export const purgeAlbum = mutation({
|
|
689
|
+
args: {
|
|
690
|
+
album: v.string(),
|
|
691
|
+
deleteAssemblies: v.optional(v.boolean()),
|
|
692
|
+
},
|
|
693
|
+
returns: v.object({
|
|
694
|
+
deletedResults: v.number(),
|
|
695
|
+
deletedAssemblies: v.number(),
|
|
696
|
+
}),
|
|
697
|
+
handler: async (ctx, args) => {
|
|
698
|
+
const results = await ctx.db
|
|
699
|
+
.query("results")
|
|
700
|
+
.withIndex("by_album", (q) => q.eq("album", args.album))
|
|
701
|
+
.collect();
|
|
702
|
+
const assemblyIds = new Set<string>();
|
|
703
|
+
|
|
704
|
+
for (const result of results) {
|
|
705
|
+
assemblyIds.add(result.assemblyId);
|
|
706
|
+
await ctx.db.delete(result._id);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
let deletedAssemblies = 0;
|
|
710
|
+
if (args.deleteAssemblies ?? true) {
|
|
711
|
+
for (const assemblyId of assemblyIds) {
|
|
712
|
+
const assembly = await ctx.db
|
|
713
|
+
.query("assemblies")
|
|
714
|
+
.withIndex("by_assemblyId", (q) => q.eq("assemblyId", assemblyId))
|
|
715
|
+
.unique();
|
|
716
|
+
if (assembly) {
|
|
717
|
+
await ctx.db.delete(assembly._id);
|
|
718
|
+
deletedAssemblies += 1;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return { deletedResults: results.length, deletedAssemblies };
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
|
|
560
727
|
export const storeAssemblyMetadata = mutation({
|
|
561
728
|
args: {
|
|
562
729
|
assemblyId: v.string(),
|
package/src/component/schema.ts
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { defineSchema, defineTable } from "convex/server";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
3
|
|
|
4
|
-
export const assemblyStatusValues = [
|
|
5
|
-
"ASSEMBLY_UPLOADING",
|
|
6
|
-
"ASSEMBLY_EXECUTING",
|
|
7
|
-
"ASSEMBLY_COMPLETED",
|
|
8
|
-
"ASSEMBLY_CANCELED",
|
|
9
|
-
"ASSEMBLY_FAILED",
|
|
10
|
-
] as const;
|
|
11
|
-
|
|
12
|
-
export type AssemblyStatus = (typeof assemblyStatusValues)[number];
|
|
13
|
-
|
|
14
4
|
export default defineSchema({
|
|
15
5
|
assemblies: defineTable({
|
|
16
6
|
assemblyId: v.string(),
|
|
@@ -34,6 +24,8 @@ export default defineSchema({
|
|
|
34
24
|
.index("by_userId", ["userId"]),
|
|
35
25
|
results: defineTable({
|
|
36
26
|
assemblyId: v.string(),
|
|
27
|
+
album: v.optional(v.string()),
|
|
28
|
+
userId: v.optional(v.string()),
|
|
37
29
|
stepName: v.string(),
|
|
38
30
|
resultId: v.optional(v.string()),
|
|
39
31
|
sslUrl: v.optional(v.string()),
|
|
@@ -44,5 +36,6 @@ export default defineSchema({
|
|
|
44
36
|
createdAt: v.number(),
|
|
45
37
|
})
|
|
46
38
|
.index("by_assemblyId", ["assemblyId"])
|
|
47
|
-
.index("by_assemblyId_and_step", ["assemblyId", "stepName"])
|
|
39
|
+
.index("by_assemblyId_and_step", ["assemblyId", "stepName"])
|
|
40
|
+
.index("by_album", ["album"]),
|
|
48
41
|
});
|