@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.
Files changed (85) hide show
  1. package/README.md +154 -122
  2. package/dist/client/index.d.ts +54 -13
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +48 -5
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +2 -2
  7. package/dist/component/_generated/component.d.ts +11 -0
  8. package/dist/component/_generated/component.d.ts.map +1 -1
  9. package/dist/component/_generated/dataModel.d.ts +1 -1
  10. package/dist/component/_generated/server.d.ts +1 -1
  11. package/dist/component/apiUtils.d.ts +26 -6
  12. package/dist/component/apiUtils.d.ts.map +1 -1
  13. package/dist/component/apiUtils.js +48 -38
  14. package/dist/component/apiUtils.js.map +1 -1
  15. package/dist/component/lib.d.ts +37 -8
  16. package/dist/component/lib.d.ts.map +1 -1
  17. package/dist/component/lib.js +145 -18
  18. package/dist/component/lib.js.map +1 -1
  19. package/dist/component/schema.d.ts +9 -6
  20. package/dist/component/schema.d.ts.map +1 -1
  21. package/dist/component/schema.js +4 -8
  22. package/dist/component/schema.js.map +1 -1
  23. package/dist/debug/index.d.ts +19 -0
  24. package/dist/debug/index.d.ts.map +1 -0
  25. package/dist/debug/index.js +49 -0
  26. package/dist/debug/index.js.map +1 -0
  27. package/dist/react/index.d.ts +201 -3
  28. package/dist/react/index.d.ts.map +1 -1
  29. package/dist/react/index.js +674 -94
  30. package/dist/react/index.js.map +1 -1
  31. package/dist/shared/assemblyUrls.d.ts +10 -0
  32. package/dist/shared/assemblyUrls.d.ts.map +1 -0
  33. package/dist/shared/assemblyUrls.js +26 -0
  34. package/dist/shared/assemblyUrls.js.map +1 -0
  35. package/dist/shared/errors.d.ts +7 -0
  36. package/dist/shared/errors.d.ts.map +1 -0
  37. package/dist/shared/errors.js +10 -0
  38. package/dist/shared/errors.js.map +1 -0
  39. package/dist/shared/pollAssembly.d.ts +12 -0
  40. package/dist/shared/pollAssembly.d.ts.map +1 -0
  41. package/dist/shared/pollAssembly.js +50 -0
  42. package/dist/shared/pollAssembly.js.map +1 -0
  43. package/dist/shared/resultTypes.d.ts +37 -0
  44. package/dist/shared/resultTypes.d.ts.map +1 -0
  45. package/dist/shared/resultTypes.js +2 -0
  46. package/dist/shared/resultTypes.js.map +1 -0
  47. package/dist/shared/resultUtils.d.ts +4 -0
  48. package/dist/shared/resultUtils.d.ts.map +1 -0
  49. package/dist/shared/resultUtils.js +69 -0
  50. package/dist/shared/resultUtils.js.map +1 -0
  51. package/dist/shared/tusUpload.d.ts +13 -0
  52. package/dist/shared/tusUpload.d.ts.map +1 -0
  53. package/dist/shared/tusUpload.js +32 -0
  54. package/dist/shared/tusUpload.js.map +1 -0
  55. package/dist/test/index.d.ts +9 -4
  56. package/dist/test/index.d.ts.map +1 -1
  57. package/dist/test/nodeModules.d.ts +2 -0
  58. package/dist/test/nodeModules.d.ts.map +1 -0
  59. package/dist/test/nodeModules.js +19 -0
  60. package/dist/test/nodeModules.js.map +1 -0
  61. package/package.json +40 -7
  62. package/src/client/index.ts +111 -9
  63. package/src/component/_generated/api.ts +2 -2
  64. package/src/component/_generated/component.ts +14 -0
  65. package/src/component/_generated/dataModel.ts +1 -1
  66. package/src/component/_generated/server.ts +1 -1
  67. package/src/component/apiUtils.test.ts +166 -2
  68. package/src/component/apiUtils.ts +96 -64
  69. package/src/component/lib.test.ts +213 -4
  70. package/src/component/lib.ts +192 -25
  71. package/src/component/schema.ts +4 -11
  72. package/src/debug/index.ts +84 -0
  73. package/src/react/index.test.tsx +340 -0
  74. package/src/react/index.tsx +1089 -179
  75. package/src/react/uploadWithTus.test.tsx +192 -0
  76. package/src/shared/assemblyUrls.test.ts +71 -0
  77. package/src/shared/assemblyUrls.ts +59 -0
  78. package/src/shared/errors.ts +23 -0
  79. package/src/shared/pollAssembly.ts +65 -0
  80. package/src/shared/resultTypes.ts +44 -0
  81. package/src/shared/resultUtils.test.ts +29 -0
  82. package/src/shared/resultUtils.ts +71 -0
  83. package/src/shared/tusUpload.ts +59 -0
  84. package/src/test/index.ts +1 -1
  85. package/src/test/nodeModules.ts +19 -0
@@ -0,0 +1,84 @@
1
+ type ConsoleSink = Pick<Console, "log" | "info" | "warn" | "error">;
2
+
3
+ export type DebugLogger = {
4
+ enabled: boolean;
5
+ log: (message: string, meta?: Record<string, unknown>) => void;
6
+ info: (message: string, meta?: Record<string, unknown>) => void;
7
+ warn: (message: string, meta?: Record<string, unknown>) => void;
8
+ error: (message: string, meta?: Record<string, unknown>) => void;
9
+ event: (name: string, meta?: Record<string, unknown>) => void;
10
+ child: (namespace: string) => DebugLogger;
11
+ };
12
+
13
+ export type DebugLoggerOptions = {
14
+ namespace?: string;
15
+ enabled?: boolean;
16
+ sink?: ConsoleSink;
17
+ clock?: () => Date;
18
+ };
19
+
20
+ const resolveEnv = (): Record<string, string | undefined> => {
21
+ if (typeof process !== "undefined" && process.env) {
22
+ return process.env;
23
+ }
24
+ return {};
25
+ };
26
+
27
+ const parseEnabled = (value: string | undefined) => {
28
+ if (!value) return false;
29
+ const normalized = value.trim().toLowerCase();
30
+ return normalized === "1" || normalized === "true" || normalized === "yes";
31
+ };
32
+
33
+ const formatLine = (
34
+ timestamp: string,
35
+ prefix: string,
36
+ message: string,
37
+ meta?: Record<string, unknown>,
38
+ ) => {
39
+ if (!meta || Object.keys(meta).length === 0) {
40
+ return `${timestamp} ${prefix} ${message}`;
41
+ }
42
+ return `${timestamp} ${prefix} ${message} ${JSON.stringify(meta)}`;
43
+ };
44
+
45
+ export const createDebugLogger = (
46
+ options: DebugLoggerOptions = {},
47
+ ): DebugLogger => {
48
+ const env = resolveEnv();
49
+ const enabled =
50
+ options.enabled ??
51
+ (parseEnabled(env.TRANSLOADIT_DEBUG) ||
52
+ parseEnabled(env.CONVEX_TRANSLOADIT_DEBUG));
53
+ const namespace = options.namespace ?? "convex";
54
+ const prefix = `[transloadit:${namespace}]`;
55
+ const sink: ConsoleSink = options.sink ?? console;
56
+ const clock = options.clock ?? (() => new Date());
57
+
58
+ const emit = (
59
+ level: "log" | "info" | "warn" | "error",
60
+ message: string,
61
+ meta?: Record<string, unknown>,
62
+ ) => {
63
+ if (!enabled) return;
64
+ const line = formatLine(clock().toISOString(), prefix, message, meta);
65
+ sink[level](line);
66
+ };
67
+
68
+ const logger: DebugLogger = {
69
+ enabled,
70
+ log: (message, meta) => emit("log", message, meta),
71
+ info: (message, meta) => emit("info", message, meta),
72
+ warn: (message, meta) => emit("warn", message, meta),
73
+ error: (message, meta) => emit("error", message, meta),
74
+ event: (name, meta) => emit("info", `event:${name}`, meta),
75
+ child: (childNamespace) =>
76
+ createDebugLogger({
77
+ ...options,
78
+ namespace: `${namespace}:${childNamespace}`,
79
+ enabled,
80
+ }),
81
+ };
82
+
83
+ return logger;
84
+ };
@@ -0,0 +1,340 @@
1
+ /// <reference types="vite/client" />
2
+ // @vitest-environment jsdom
3
+
4
+ import { renderHook } from "@testing-library/react";
5
+ import type { FunctionReference } from "convex/server";
6
+ import { act } from "react";
7
+ import { afterEach, describe, expect, test, vi } from "vitest";
8
+ import type {
9
+ CreateAssemblyFn,
10
+ GetAssemblyStatusFn,
11
+ ListResultsFn,
12
+ RefreshAssemblyFn,
13
+ UppyLike,
14
+ } from "./index.tsx";
15
+ import {
16
+ useAssemblyStatusWithPolling,
17
+ useTransloaditUpload,
18
+ useTransloaditUppy,
19
+ } from "./index.tsx";
20
+
21
+ (
22
+ globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
23
+ ).IS_REACT_ACT_ENVIRONMENT = true;
24
+
25
+ let currentStatus: unknown = null;
26
+ let currentResults: unknown = null;
27
+ let queryHandler: (fn: unknown, args: unknown) => unknown = () => currentStatus;
28
+ const refreshMock = vi.hoisted(() => vi.fn(() => Promise.resolve()));
29
+ const actionMock = vi.hoisted(() => vi.fn((fn: unknown) => fn));
30
+ const queryMock = vi.hoisted(() => vi.fn());
31
+
32
+ vi.mock("convex/react", () => ({
33
+ useQuery: queryMock,
34
+ useAction: actionMock,
35
+ }));
36
+
37
+ vi.mock("tus-js-client", () => {
38
+ type UploadOptions = {
39
+ endpoint?: string;
40
+ onUploadUrlAvailable?: () => void;
41
+ onProgress?: (bytesUploaded: number, bytesTotal: number) => void;
42
+ onSuccess?: () => void;
43
+ };
44
+
45
+ return {
46
+ Upload: class MockUpload {
47
+ url?: string;
48
+ private options: UploadOptions;
49
+ constructor(_file: File, options: UploadOptions) {
50
+ this.options = options;
51
+ this.url = options?.endpoint
52
+ ? `${options.endpoint}/upload`
53
+ : "https://tus.mock/upload";
54
+ }
55
+ start() {
56
+ this.options?.onUploadUrlAvailable?.();
57
+ this.options?.onProgress?.(1, 1);
58
+ this.options?.onSuccess?.();
59
+ }
60
+ abort() {
61
+ // no-op for tests
62
+ }
63
+ },
64
+ };
65
+ });
66
+
67
+ const noopGetStatus = (() => null) as unknown as Parameters<
68
+ typeof useAssemblyStatusWithPolling
69
+ >[0];
70
+ const noopRefresh = refreshMock as unknown as RefreshAssemblyFn;
71
+
72
+ queryMock.mockImplementation((fn, args) => queryHandler(fn, args));
73
+
74
+ describe("useAssemblyStatusWithPolling", () => {
75
+ afterEach(() => {
76
+ vi.useRealTimers();
77
+ refreshMock.mockClear();
78
+ actionMock.mockClear();
79
+ queryMock.mockClear();
80
+ currentResults = null;
81
+ currentStatus = null;
82
+ queryHandler = () => currentStatus;
83
+ queryMock.mockImplementation((fn, args) => queryHandler(fn, args));
84
+ });
85
+
86
+ test("does not trigger immediate refresh on status change", async () => {
87
+ vi.useFakeTimers();
88
+ currentStatus = { ok: "ASSEMBLY_UPLOADING" };
89
+
90
+ const { rerender, unmount } = renderHook(
91
+ ({ assemblyId }: { assemblyId: string }) =>
92
+ useAssemblyStatusWithPolling(noopGetStatus, noopRefresh, assemblyId, {
93
+ pollIntervalMs: 1000,
94
+ }),
95
+ { initialProps: { assemblyId: "asm_1" } },
96
+ );
97
+
98
+ await act(async () => {
99
+ await Promise.resolve();
100
+ });
101
+
102
+ expect(refreshMock).toHaveBeenCalledTimes(1);
103
+
104
+ await act(async () => {
105
+ vi.advanceTimersByTime(1000);
106
+ await Promise.resolve();
107
+ });
108
+
109
+ expect(refreshMock).toHaveBeenCalledTimes(2);
110
+
111
+ currentStatus = { ok: "ASSEMBLY_COMPLETED" };
112
+ rerender({ assemblyId: "asm_1" });
113
+
114
+ await act(async () => {
115
+ await Promise.resolve();
116
+ });
117
+
118
+ expect(refreshMock).toHaveBeenCalledTimes(2);
119
+
120
+ unmount();
121
+ });
122
+
123
+ test("does not overlap refresh calls", async () => {
124
+ vi.useFakeTimers();
125
+ currentStatus = { ok: "ASSEMBLY_UPLOADING" };
126
+ let resolveRefresh: (() => void) | null = null;
127
+ refreshMock.mockImplementation(
128
+ () =>
129
+ new Promise<void>((resolve) => {
130
+ resolveRefresh = resolve;
131
+ }),
132
+ );
133
+
134
+ const { unmount } = renderHook(() =>
135
+ useAssemblyStatusWithPolling(noopGetStatus, noopRefresh, "asm_overlap", {
136
+ pollIntervalMs: 1000,
137
+ }),
138
+ );
139
+
140
+ await act(async () => {
141
+ await Promise.resolve();
142
+ });
143
+
144
+ expect(refreshMock).toHaveBeenCalledTimes(1);
145
+
146
+ await act(async () => {
147
+ vi.advanceTimersByTime(3000);
148
+ await Promise.resolve();
149
+ });
150
+
151
+ expect(refreshMock).toHaveBeenCalledTimes(1);
152
+
153
+ await act(async () => {
154
+ resolveRefresh?.();
155
+ await Promise.resolve();
156
+ });
157
+
158
+ await act(async () => {
159
+ vi.advanceTimersByTime(1000);
160
+ await Promise.resolve();
161
+ });
162
+
163
+ expect(refreshMock).toHaveBeenCalledTimes(2);
164
+
165
+ unmount();
166
+ });
167
+ });
168
+
169
+ describe("useTransloaditUpload", () => {
170
+ afterEach(() => {
171
+ actionMock.mockClear();
172
+ queryMock.mockClear();
173
+ currentResults = null;
174
+ currentStatus = null;
175
+ queryHandler = () => currentStatus;
176
+ });
177
+
178
+ test("uploads files and exposes status/results", async () => {
179
+ const createAssembly = vi.fn(async () => ({
180
+ assemblyId: "asm_123",
181
+ data: {
182
+ tus_url: "https://tus.example.com",
183
+ assembly_ssl_url: "https://api2.transloadit.com/assemblies/asm_123",
184
+ },
185
+ }));
186
+
187
+ const getStatus = {} as GetAssemblyStatusFn;
188
+ const listResults = {} as ListResultsFn;
189
+ const refreshAssembly = refreshMock as unknown as RefreshAssemblyFn;
190
+ currentStatus = { raw: { ok: "ASSEMBLY_UPLOADING" } };
191
+ currentResults = [{ stepName: "resize", raw: { ssl_url: "https://file" } }];
192
+ queryHandler = (fn) => {
193
+ if (fn === getStatus) return currentStatus;
194
+ if (fn === listResults) return currentResults;
195
+ return null;
196
+ };
197
+ queryMock.mockImplementation((fn, args) => queryHandler(fn, args));
198
+
199
+ const { result } = renderHook(() =>
200
+ useTransloaditUpload({
201
+ createAssembly: createAssembly as unknown as CreateAssemblyFn,
202
+ getStatus,
203
+ listResults,
204
+ refreshAssembly,
205
+ }),
206
+ );
207
+
208
+ const file = new File(["hello"], "hello.txt", { type: "text/plain" });
209
+
210
+ await act(async () => {
211
+ await result.current.upload([file], {
212
+ steps: { resize: { robot: "/image/resize" } },
213
+ });
214
+ });
215
+
216
+ expect(createAssembly).toHaveBeenCalled();
217
+ expect(result.current.assemblyId).toBe("asm_123");
218
+ expect(result.current.results).toEqual(currentResults);
219
+ expect(result.current.status?.ok).toBe("ASSEMBLY_UPLOADING");
220
+ });
221
+
222
+ test("reset clears upload state", async () => {
223
+ const createAssembly = vi.fn(async () => ({
224
+ assemblyId: "asm_reset",
225
+ data: {
226
+ tus_url: "https://tus.example.com",
227
+ assembly_ssl_url: "https://api2.transloadit.com/assemblies/asm_reset",
228
+ },
229
+ }));
230
+
231
+ const getStatus = {} as GetAssemblyStatusFn;
232
+ const listResults = {} as ListResultsFn;
233
+ const refreshAssembly = refreshMock as unknown as RefreshAssemblyFn;
234
+ currentStatus = { raw: { ok: "ASSEMBLY_UPLOADING" } };
235
+ currentResults = [{ stepName: "resize", raw: { ssl_url: "https://file" } }];
236
+ queryHandler = (fn) => {
237
+ if (fn === getStatus) return currentStatus;
238
+ if (fn === listResults) return currentResults;
239
+ return null;
240
+ };
241
+ queryMock.mockImplementation((fn, args) => queryHandler(fn, args));
242
+
243
+ const { result } = renderHook(() =>
244
+ useTransloaditUpload({
245
+ createAssembly: createAssembly as unknown as CreateAssemblyFn,
246
+ getStatus,
247
+ listResults,
248
+ refreshAssembly,
249
+ }),
250
+ );
251
+
252
+ const file = new File(["hello"], "hello.txt", { type: "text/plain" });
253
+
254
+ await act(async () => {
255
+ await result.current.upload([file], {
256
+ steps: { resize: { robot: "/image/resize" } },
257
+ });
258
+ });
259
+
260
+ expect(result.current.assemblyId).toBe("asm_reset");
261
+
262
+ act(() => {
263
+ result.current.reset();
264
+ });
265
+
266
+ expect(result.current.assemblyId).toBeNull();
267
+ expect(result.current.error).toBeNull();
268
+ });
269
+ });
270
+
271
+ describe("useTransloaditUppy", () => {
272
+ afterEach(() => {
273
+ actionMock.mockClear();
274
+ queryMock.mockClear();
275
+ currentResults = null;
276
+ currentStatus = null;
277
+ queryHandler = () => currentStatus;
278
+ queryMock.mockImplementation((fn, args) => queryHandler(fn, args));
279
+ });
280
+
281
+ test("uploads via uppy and exposes status/results", async () => {
282
+ const createAssembly = vi.fn(async () => ({
283
+ assemblyId: "asm_uppy",
284
+ data: {
285
+ tus_url: "https://tus.example.com",
286
+ assembly_ssl_url: "https://api2.transloadit.com/assemblies/asm_uppy",
287
+ },
288
+ }));
289
+
290
+ const uppy = {
291
+ getFiles: () => [
292
+ {
293
+ id: "file-1",
294
+ data: new File(["hello"], "hello.jpg", { type: "image/jpeg" }),
295
+ },
296
+ ],
297
+ setFileMeta: vi.fn(),
298
+ setFileState: vi.fn(),
299
+ getPlugin: vi.fn(() => ({ setOptions: vi.fn() })),
300
+ upload: vi.fn(async () => ({ successful: [{ id: "file-1" }] })),
301
+ } as unknown as UppyLike;
302
+
303
+ const getStatus = {} as GetAssemblyStatusFn;
304
+ const listResults = {} as ListResultsFn;
305
+ const refreshAssembly = refreshMock as unknown as RefreshAssemblyFn;
306
+ currentStatus = { raw: { ok: "ASSEMBLY_UPLOADING" } };
307
+ currentResults = [{ stepName: "resize", raw: { ssl_url: "https://file" } }];
308
+ queryHandler = (fn) => {
309
+ if (fn === getStatus) return currentStatus;
310
+ if (fn === listResults) return currentResults;
311
+ return null;
312
+ };
313
+ queryMock.mockImplementation((fn, args) => queryHandler(fn, args));
314
+
315
+ const { result } = renderHook(() =>
316
+ useTransloaditUppy({
317
+ uppy,
318
+ createAssembly: createAssembly as unknown as FunctionReference<
319
+ "action",
320
+ "public",
321
+ { fileCount: number },
322
+ { assemblyId: string; data: Record<string, unknown> }
323
+ >,
324
+ getStatus,
325
+ listResults,
326
+ refreshAssembly,
327
+ }),
328
+ );
329
+
330
+ await act(async () => {
331
+ await result.current.startUpload();
332
+ });
333
+
334
+ expect(createAssembly).toHaveBeenCalled();
335
+ expect(uppy.upload).toHaveBeenCalled();
336
+ expect(result.current.assemblyId).toBe("asm_uppy");
337
+ expect(result.current.results).toEqual(currentResults);
338
+ expect(result.current.status?.ok).toBe("ASSEMBLY_UPLOADING");
339
+ });
340
+ });