@transloadit/convex 0.0.3 → 0.0.4

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 (81) hide show
  1. package/README.md +114 -134
  2. package/dist/client/index.d.ts +24 -13
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +14 -3
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/component/_generated/api.d.ts +2 -2
  7. package/dist/component/_generated/dataModel.d.ts +1 -1
  8. package/dist/component/_generated/server.d.ts +1 -1
  9. package/dist/component/apiUtils.d.ts +26 -6
  10. package/dist/component/apiUtils.d.ts.map +1 -1
  11. package/dist/component/apiUtils.js +48 -38
  12. package/dist/component/apiUtils.js.map +1 -1
  13. package/dist/component/lib.d.ts +7 -9
  14. package/dist/component/lib.d.ts.map +1 -1
  15. package/dist/component/lib.js +74 -18
  16. package/dist/component/lib.js.map +1 -1
  17. package/dist/component/schema.d.ts +4 -6
  18. package/dist/component/schema.d.ts.map +1 -1
  19. package/dist/component/schema.js +0 -7
  20. package/dist/component/schema.js.map +1 -1
  21. package/dist/debug/index.d.ts +19 -0
  22. package/dist/debug/index.d.ts.map +1 -0
  23. package/dist/debug/index.js +49 -0
  24. package/dist/debug/index.js.map +1 -0
  25. package/dist/react/index.d.ts +201 -3
  26. package/dist/react/index.d.ts.map +1 -1
  27. package/dist/react/index.js +674 -94
  28. package/dist/react/index.js.map +1 -1
  29. package/dist/shared/assemblyUrls.d.ts +10 -0
  30. package/dist/shared/assemblyUrls.d.ts.map +1 -0
  31. package/dist/shared/assemblyUrls.js +26 -0
  32. package/dist/shared/assemblyUrls.js.map +1 -0
  33. package/dist/shared/errors.d.ts +7 -0
  34. package/dist/shared/errors.d.ts.map +1 -0
  35. package/dist/shared/errors.js +10 -0
  36. package/dist/shared/errors.js.map +1 -0
  37. package/dist/shared/pollAssembly.d.ts +12 -0
  38. package/dist/shared/pollAssembly.d.ts.map +1 -0
  39. package/dist/shared/pollAssembly.js +50 -0
  40. package/dist/shared/pollAssembly.js.map +1 -0
  41. package/dist/shared/resultTypes.d.ts +37 -0
  42. package/dist/shared/resultTypes.d.ts.map +1 -0
  43. package/dist/shared/resultTypes.js +2 -0
  44. package/dist/shared/resultTypes.js.map +1 -0
  45. package/dist/shared/resultUtils.d.ts +4 -0
  46. package/dist/shared/resultUtils.d.ts.map +1 -0
  47. package/dist/shared/resultUtils.js +69 -0
  48. package/dist/shared/resultUtils.js.map +1 -0
  49. package/dist/shared/tusUpload.d.ts +13 -0
  50. package/dist/shared/tusUpload.d.ts.map +1 -0
  51. package/dist/shared/tusUpload.js +32 -0
  52. package/dist/shared/tusUpload.js.map +1 -0
  53. package/dist/test/index.d.ts +4 -4
  54. package/dist/test/nodeModules.d.ts +2 -0
  55. package/dist/test/nodeModules.d.ts.map +1 -0
  56. package/dist/test/nodeModules.js +19 -0
  57. package/dist/test/nodeModules.js.map +1 -0
  58. package/package.json +36 -6
  59. package/src/client/index.ts +73 -7
  60. package/src/component/_generated/api.ts +2 -2
  61. package/src/component/_generated/dataModel.ts +1 -1
  62. package/src/component/_generated/server.ts +1 -1
  63. package/src/component/apiUtils.test.ts +166 -2
  64. package/src/component/apiUtils.ts +96 -64
  65. package/src/component/lib.test.ts +170 -4
  66. package/src/component/lib.ts +113 -25
  67. package/src/component/schema.ts +0 -10
  68. package/src/debug/index.ts +84 -0
  69. package/src/react/index.test.tsx +340 -0
  70. package/src/react/index.tsx +1089 -179
  71. package/src/react/uploadWithTus.test.tsx +192 -0
  72. package/src/shared/assemblyUrls.test.ts +71 -0
  73. package/src/shared/assemblyUrls.ts +59 -0
  74. package/src/shared/errors.ts +23 -0
  75. package/src/shared/pollAssembly.ts +65 -0
  76. package/src/shared/resultTypes.ts +44 -0
  77. package/src/shared/resultUtils.test.ts +29 -0
  78. package/src/shared/resultUtils.ts +71 -0
  79. package/src/shared/tusUpload.ts +59 -0
  80. package/src/test/index.ts +1 -1
  81. package/src/test/nodeModules.ts +19 -0
@@ -0,0 +1,192 @@
1
+ /// <reference types="vite/client" />
2
+ // @vitest-environment jsdom
3
+
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { UploadState } from "./index.tsx";
6
+
7
+ vi.mock("tus-js-client", () => {
8
+ type UploadOptions = {
9
+ onUploadUrlAvailable?: () => void;
10
+ onProgress?: (bytesUploaded: number, bytesTotal: number) => void;
11
+ onSuccess?: () => void;
12
+ onError?: (error: Error) => void;
13
+ };
14
+
15
+ class Upload {
16
+ url: string;
17
+ options: UploadOptions;
18
+ file: File;
19
+ aborted = false;
20
+
21
+ constructor(file: File, options: UploadOptions) {
22
+ this.file = file;
23
+ this.options = options;
24
+ this.url = `https://upload.example.com/${encodeURIComponent(file.name)}`;
25
+ }
26
+
27
+ start() {
28
+ const shouldFail = this.file.name.includes("fail");
29
+ const delay = this.file.name.includes("slow") ? 25 : 0;
30
+ this.options.onUploadUrlAvailable?.();
31
+ this.options.onProgress?.(10, 10);
32
+ setTimeout(() => {
33
+ if (this.aborted) return;
34
+ if (shouldFail) {
35
+ this.options.onError?.(new Error("Upload failed"));
36
+ return;
37
+ }
38
+ this.options.onSuccess?.();
39
+ }, delay);
40
+ }
41
+
42
+ abort() {
43
+ this.aborted = true;
44
+ }
45
+ }
46
+
47
+ return { Upload };
48
+ });
49
+
50
+ import {
51
+ uploadFilesWithTransloaditTus,
52
+ uploadWithTransloaditTus,
53
+ } from "./index.tsx";
54
+
55
+ describe("uploadWithTransloaditTus", () => {
56
+ it("uploads with tus and emits progress", async () => {
57
+ const createAssembly = vi.fn(async () => ({
58
+ assemblyId: "asm_123",
59
+ data: {
60
+ tus_url: "https://tus.transloadit.com",
61
+ assembly_ssl_url: "https://transloadit.com/assembly",
62
+ },
63
+ }));
64
+ const file = new File(["hello"], "hello.txt", { type: "text/plain" });
65
+ const states: UploadState[] = [];
66
+ const progress: number[] = [];
67
+
68
+ const result = await uploadWithTransloaditTus(
69
+ createAssembly,
70
+ file,
71
+ {
72
+ numExpectedUploadFiles: 1,
73
+ onProgress: (value) => progress.push(value),
74
+ },
75
+ {
76
+ onStateChange: (state) => states.push(state),
77
+ },
78
+ );
79
+
80
+ expect(createAssembly).toHaveBeenCalledWith(
81
+ expect.objectContaining({ numExpectedUploadFiles: 1 }),
82
+ );
83
+ expect(result.assemblyId).toBe("asm_123");
84
+ expect(progress).toContain(100);
85
+ expect(states[0]).toEqual({ isUploading: true, progress: 0, error: null });
86
+ expect(states[states.length - 1]).toEqual({
87
+ isUploading: false,
88
+ progress: 100,
89
+ error: null,
90
+ });
91
+ });
92
+
93
+ it("fails when tus_url is missing", async () => {
94
+ const createAssembly = vi.fn(async () => ({
95
+ assemblyId: "asm_456",
96
+ data: {},
97
+ }));
98
+ const file = new File(["hello"], "hello.txt", { type: "text/plain" });
99
+ const states: UploadState[] = [];
100
+
101
+ await expect(
102
+ uploadWithTransloaditTus(
103
+ createAssembly,
104
+ file,
105
+ { numExpectedUploadFiles: 1 },
106
+ { onStateChange: (state) => states.push(state) },
107
+ ),
108
+ ).rejects.toThrow("tus_url");
109
+
110
+ const lastState = states[states.length - 1];
111
+ expect(lastState?.error).toBeInstanceOf(Error);
112
+ expect(lastState?.isUploading).toBe(false);
113
+ });
114
+ });
115
+
116
+ describe("uploadFilesWithTransloaditTus", () => {
117
+ const createAssembly = vi.fn(async () => ({
118
+ assemblyId: "asm_multi",
119
+ data: {
120
+ tus_url: "https://tus.transloadit.com",
121
+ assembly_ssl_url: "https://transloadit.com/assembly",
122
+ },
123
+ }));
124
+
125
+ it("uploads multiple files with overall progress", async () => {
126
+ const files = [
127
+ new File(["one"], "one.txt", { type: "text/plain" }),
128
+ new File(["two"], "two.txt", { type: "text/plain" }),
129
+ ];
130
+ const overall: number[] = [];
131
+ const perFile: Array<{ name: string; progress: number }> = [];
132
+
133
+ const controller = uploadFilesWithTransloaditTus(createAssembly, files, {
134
+ numExpectedUploadFiles: files.length,
135
+ onOverallProgress: (progress) => overall.push(progress),
136
+ onFileProgress: (file, progress) =>
137
+ perFile.push({ name: file.name, progress }),
138
+ });
139
+ const result = await controller.promise;
140
+
141
+ expect(result.assemblyId).toBe("asm_multi");
142
+ expect(result.files.every((file) => file.status === "success")).toBe(true);
143
+ expect(overall[overall.length - 1]).toBe(100);
144
+ expect(perFile.map((entry) => entry.name)).toEqual(
145
+ expect.arrayContaining(["one.txt", "two.txt"]),
146
+ );
147
+ });
148
+
149
+ it("does not reach 100% before all files start", async () => {
150
+ const files = [
151
+ new File(["slow"], "slow.txt", { type: "text/plain" }),
152
+ new File(["two"], "two.txt", { type: "text/plain" }),
153
+ ];
154
+ const overall: number[] = [];
155
+
156
+ const controller = uploadFilesWithTransloaditTus(createAssembly, files, {
157
+ numExpectedUploadFiles: files.length,
158
+ concurrency: 1,
159
+ onOverallProgress: (progress) => overall.push(progress),
160
+ });
161
+ await controller.promise;
162
+
163
+ expect(overall[0]).toBeLessThan(100);
164
+ });
165
+
166
+ it("returns results on partial failure when failFast is false", async () => {
167
+ const files = [
168
+ new File(["ok"], "ok.txt", { type: "text/plain" }),
169
+ new File(["bad"], "fail.txt", { type: "text/plain" }),
170
+ ];
171
+
172
+ await expect(
173
+ uploadFilesWithTransloaditTus(createAssembly, files, {
174
+ failFast: false,
175
+ }).promise,
176
+ ).rejects.toThrow("Failed to upload");
177
+ });
178
+
179
+ it("cancels uploads and surfaces results", async () => {
180
+ const files = [
181
+ new File(["slow"], "slow.txt", { type: "text/plain" }),
182
+ new File(["slow"], "slow-2.txt", { type: "text/plain" }),
183
+ ];
184
+ const controller = uploadFilesWithTransloaditTus(createAssembly, files, {
185
+ failFast: true,
186
+ });
187
+
188
+ controller.cancel();
189
+
190
+ await expect(controller.promise).rejects.toThrow("Upload canceled");
191
+ });
192
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ ASSEMBLY_STATUS_COMPLETED,
4
+ ASSEMBLY_STATUS_UPLOADING,
5
+ getAssemblyStage,
6
+ isAssemblyCompletedStatus,
7
+ isAssemblyUploadingStatus,
8
+ parseAssemblyFields,
9
+ parseAssemblyResults,
10
+ parseAssemblyStatus,
11
+ parseAssemblyUrls,
12
+ } from "./assemblyUrls.ts";
13
+
14
+ describe("assembly helpers", () => {
15
+ it("parses tus and assembly URLs with fallbacks", () => {
16
+ const parsed = parseAssemblyUrls({
17
+ tus_url: "https://tus.transloadit.com",
18
+ assembly_ssl_url: "https://ssl.transloadit.com/assembly",
19
+ assembly_url: "https://transloadit.com/assembly",
20
+ });
21
+
22
+ expect(parsed).toEqual({
23
+ tusUrl: "https://tus.transloadit.com",
24
+ assemblyUrl: "https://ssl.transloadit.com/assembly",
25
+ });
26
+
27
+ const fallback = parseAssemblyUrls({
28
+ tusUrl: "https://tus.example.com",
29
+ assemblyUrl: "https://assembly.example.com",
30
+ });
31
+
32
+ expect(fallback).toEqual({
33
+ tusUrl: "https://tus.example.com",
34
+ assemblyUrl: "https://assembly.example.com",
35
+ });
36
+ });
37
+
38
+ it("parses assembly status, fields, and results safely", () => {
39
+ const status = {
40
+ ok: "ASSEMBLY_COMPLETED",
41
+ fields: { album: "wedding-gallery" },
42
+ results: {
43
+ images_output: [
44
+ {
45
+ id: "result-1",
46
+ ssl_url: "https://cdn.example.com/image.jpg",
47
+ },
48
+ ],
49
+ },
50
+ };
51
+
52
+ expect(parseAssemblyStatus(status)?.ok).toBe("ASSEMBLY_COMPLETED");
53
+ expect(parseAssemblyFields(status)).toEqual({ album: "wedding-gallery" });
54
+ expect(Object.keys(parseAssemblyResults(status))).toEqual([
55
+ "images_output",
56
+ ]);
57
+
58
+ expect(parseAssemblyStatus("nope")).toBeNull();
59
+ expect(parseAssemblyFields("nope")).toEqual({});
60
+ expect(parseAssemblyResults("nope")).toEqual({});
61
+ });
62
+
63
+ it("exposes canonical status helpers", () => {
64
+ expect(isAssemblyCompletedStatus(ASSEMBLY_STATUS_COMPLETED)).toBe(true);
65
+ expect(isAssemblyCompletedStatus(ASSEMBLY_STATUS_UPLOADING)).toBe(false);
66
+ expect(isAssemblyUploadingStatus(ASSEMBLY_STATUS_UPLOADING)).toBe(true);
67
+ expect(getAssemblyStage({ ok: ASSEMBLY_STATUS_COMPLETED })).toBe(
68
+ "complete",
69
+ );
70
+ });
71
+ });
@@ -0,0 +1,59 @@
1
+ import {
2
+ ASSEMBLY_STATUS_COMPLETED,
3
+ ASSEMBLY_STATUS_UPLOADING,
4
+ type AssemblyStatus,
5
+ type AssemblyStatusResults,
6
+ assemblyStatusSchema,
7
+ getAssemblyStage,
8
+ isAssemblyCompletedStatus,
9
+ isAssemblyUploadingStatus,
10
+ normalizeAssemblyUploadUrls,
11
+ parseAssemblyUrls,
12
+ type AssemblyStage as ZodAssemblyStage,
13
+ type AssemblyUrls as ZodAssemblyUrls,
14
+ type NormalizedAssemblyUrls as ZodNormalizedAssemblyUrls,
15
+ } from "@transloadit/zod/v3";
16
+
17
+ export type AssemblyUrls = ZodAssemblyUrls;
18
+ export type NormalizedAssemblyUrls = ZodNormalizedAssemblyUrls;
19
+ export type AssemblyStage = ZodAssemblyStage;
20
+ export type TransloaditAssembly = AssemblyStatus;
21
+ export {
22
+ ASSEMBLY_STATUS_COMPLETED,
23
+ ASSEMBLY_STATUS_UPLOADING,
24
+ getAssemblyStage,
25
+ isAssemblyCompletedStatus,
26
+ isAssemblyUploadingStatus,
27
+ normalizeAssemblyUploadUrls,
28
+ parseAssemblyUrls,
29
+ };
30
+
31
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
32
+ value !== null && typeof value === "object" && !Array.isArray(value);
33
+
34
+ export const parseAssemblyStatus = (
35
+ data: unknown,
36
+ ): TransloaditAssembly | null => {
37
+ const parsed = assemblyStatusSchema.safeParse(data);
38
+ return parsed.success ? parsed.data : null;
39
+ };
40
+
41
+ export const parseAssemblyFields = (data: unknown): Record<string, unknown> => {
42
+ const status = parseAssemblyStatus(data);
43
+ const fields = status?.fields;
44
+ return isRecord(fields) ? fields : {};
45
+ };
46
+
47
+ export const parseAssemblyResults = (data: unknown): AssemblyStatusResults => {
48
+ const status = parseAssemblyStatus(data);
49
+ const results = status?.results;
50
+ if (!isRecord(results)) return {};
51
+
52
+ const output: AssemblyStatusResults = {};
53
+ for (const [key, value] of Object.entries(results)) {
54
+ if (Array.isArray(value)) {
55
+ output[key] = value as AssemblyStatusResults[string];
56
+ }
57
+ }
58
+ return output;
59
+ };
@@ -0,0 +1,23 @@
1
+ export type TransloaditErrorContext =
2
+ | "createAssembly"
3
+ | "upload"
4
+ | "polling"
5
+ | "status"
6
+ | "webhook"
7
+ | "payload"
8
+ | "config";
9
+
10
+ export class TransloaditError extends Error {
11
+ readonly context: TransloaditErrorContext;
12
+
13
+ constructor(context: TransloaditErrorContext, message: string) {
14
+ super(`Transloadit ${context}: ${message}`);
15
+ this.name = "TransloaditError";
16
+ this.context = context;
17
+ }
18
+ }
19
+
20
+ export const transloaditError = (
21
+ context: TransloaditErrorContext,
22
+ message: string,
23
+ ): TransloaditError => new TransloaditError(context, message);
@@ -0,0 +1,65 @@
1
+ import { transloaditError } from "./errors.ts";
2
+
3
+ export type PollAssemblyOptions = {
4
+ intervalMs: number;
5
+ refresh: () => Promise<void>;
6
+ isTerminal?: () => boolean;
7
+ shouldContinue?: () => boolean;
8
+ onError?: (error: Error) => void;
9
+ };
10
+
11
+ export type PollAssemblyController = {
12
+ stop: () => void;
13
+ };
14
+
15
+ export const pollAssembly = (
16
+ options: PollAssemblyOptions,
17
+ ): PollAssemblyController => {
18
+ const intervalMs = Math.max(0, options.intervalMs);
19
+ let cancelled = false;
20
+ let intervalId: ReturnType<typeof setInterval> | null = null;
21
+ let inFlight = false;
22
+
23
+ const shouldKeepPolling = () => {
24
+ if (!options.isTerminal?.()) return true;
25
+ return options.shouldContinue?.() ?? false;
26
+ };
27
+
28
+ const stop = () => {
29
+ cancelled = true;
30
+ if (intervalId) clearInterval(intervalId);
31
+ intervalId = null;
32
+ };
33
+
34
+ if (intervalMs <= 0) {
35
+ return { stop };
36
+ }
37
+
38
+ const poll = async () => {
39
+ if (cancelled) return;
40
+ if (!shouldKeepPolling()) {
41
+ stop();
42
+ return;
43
+ }
44
+ if (inFlight) return;
45
+ inFlight = true;
46
+ try {
47
+ await options.refresh();
48
+ } catch (error) {
49
+ const resolved =
50
+ error instanceof Error
51
+ ? error
52
+ : transloaditError("polling", "Refresh failed");
53
+ options.onError?.(resolved);
54
+ } finally {
55
+ inFlight = false;
56
+ }
57
+ };
58
+
59
+ void poll();
60
+ intervalId = setInterval(() => {
61
+ void poll();
62
+ }, intervalMs);
63
+
64
+ return { stop };
65
+ };
@@ -0,0 +1,44 @@
1
+ import type { AssemblyStatusResult } from "@transloadit/zod/v3/assemblyStatus";
2
+
3
+ export type TransloaditResult = AssemblyStatusResult;
4
+
5
+ export type ImageResizeResult = AssemblyStatusResult & {
6
+ ssl_url?: string | null;
7
+ url?: string | null;
8
+ width?: number | null;
9
+ height?: number | null;
10
+ mime?: string | null;
11
+ size?: number | null;
12
+ };
13
+
14
+ export type VideoEncodeResult = AssemblyStatusResult & {
15
+ ssl_url?: string | null;
16
+ url?: string | null;
17
+ duration?: number | null;
18
+ streaming_url?: string;
19
+ hls_url?: string;
20
+ };
21
+
22
+ export type VideoThumbsResult = AssemblyStatusResult & {
23
+ ssl_url?: string | null;
24
+ url?: string | null;
25
+ width?: number | null;
26
+ height?: number | null;
27
+ };
28
+
29
+ export type StoreResult = AssemblyStatusResult & {
30
+ ssl_url?: string | null;
31
+ url?: string | null;
32
+ storage_url?: string;
33
+ };
34
+
35
+ export type ResultByRobot = {
36
+ "/image/resize": ImageResizeResult;
37
+ "/video/encode": VideoEncodeResult;
38
+ "/video/thumbs": VideoThumbsResult;
39
+ "/r2/store": StoreResult;
40
+ "/s3/store": StoreResult;
41
+ };
42
+
43
+ export type ResultForRobot<Robot extends keyof ResultByRobot> =
44
+ ResultByRobot[Robot];
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getResultOriginalKey, getResultUrl } from "./resultUtils.ts";
3
+
4
+ describe("result utils", () => {
5
+ it("extracts result URLs with common fallbacks", () => {
6
+ expect(getResultUrl({ ssl_url: "https://cdn.example.com/file.jpg" })).toBe(
7
+ "https://cdn.example.com/file.jpg",
8
+ );
9
+ expect(
10
+ getResultUrl({
11
+ meta: { url: "https://cdn.example.com/meta.jpg" },
12
+ }),
13
+ ).toBe("https://cdn.example.com/meta.jpg");
14
+ });
15
+
16
+ it("derives original keys from raw metadata", () => {
17
+ expect(
18
+ getResultOriginalKey({
19
+ raw: { original_id: "orig_1" },
20
+ }),
21
+ ).toBe("orig_1");
22
+ expect(
23
+ getResultOriginalKey({
24
+ raw: { original_basename: "photo.jpg" },
25
+ }),
26
+ ).toBe("photo.jpg");
27
+ expect(getResultOriginalKey({ name: "fallback.jpg" })).toBe("fallback.jpg");
28
+ });
29
+ });
@@ -0,0 +1,71 @@
1
+ import type { TransloaditResult } from "./resultTypes.ts";
2
+
3
+ const extractUrlFromContainer = (container: Record<string, unknown>) => {
4
+ const candidates = [
5
+ container.ssl_url,
6
+ container.sslUrl,
7
+ container.url,
8
+ container.cdn_url,
9
+ container.cdnUrl,
10
+ container.storage_url,
11
+ container.storageUrl,
12
+ container.result_url,
13
+ container.resultUrl,
14
+ container.signed_url,
15
+ container.signedUrl,
16
+ ];
17
+ for (const candidate of candidates) {
18
+ if (typeof candidate === "string" && candidate.length > 0) {
19
+ return candidate;
20
+ }
21
+ }
22
+ return undefined;
23
+ };
24
+
25
+ const extractNestedUrl = (value: unknown) => {
26
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
27
+ return undefined;
28
+ }
29
+ return extractUrlFromContainer(value as Record<string, unknown>);
30
+ };
31
+
32
+ export const getResultUrl = (result: TransloaditResult) => {
33
+ const direct = extractUrlFromContainer(result as Record<string, unknown>);
34
+ if (direct) return direct;
35
+
36
+ const nestedKeys = ["meta", "metadata", "result", "results", "file", "data"];
37
+ for (const key of nestedKeys) {
38
+ const nested = extractNestedUrl((result as Record<string, unknown>)[key]);
39
+ if (nested) return nested;
40
+ }
41
+
42
+ const urlsValue = (result as Record<string, unknown>).urls;
43
+ const urlsNested = extractNestedUrl(urlsValue);
44
+ if (urlsNested) return urlsNested;
45
+
46
+ const urlValue = (result as Record<string, unknown>).url;
47
+ const urlNested = extractNestedUrl(urlValue);
48
+ if (urlNested) return urlNested;
49
+
50
+ return undefined;
51
+ };
52
+
53
+ export const getResultOriginalKey = (result: TransloaditResult) => {
54
+ const raw = (result as TransloaditResult & { raw?: unknown }).raw;
55
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
56
+ const rawRecord = raw as Record<string, unknown>;
57
+ const originalId = rawRecord.original_id;
58
+ if (typeof originalId === "string" && originalId.length > 0) {
59
+ return originalId;
60
+ }
61
+ const originalBase = rawRecord.original_basename;
62
+ if (typeof originalBase === "string" && originalBase.length > 0) {
63
+ return originalBase;
64
+ }
65
+ }
66
+
67
+ if (result.name) return result.name;
68
+ if (result.resultId) return result.resultId;
69
+ if (result._id) return result._id;
70
+ return null;
71
+ };
@@ -0,0 +1,59 @@
1
+ import { parseAssemblyUrls } from "./assemblyUrls.ts";
2
+ import { transloaditError } from "./errors.ts";
3
+
4
+ export type TusUploadConfig = {
5
+ endpoint: string;
6
+ metadata: Record<string, string>;
7
+ addRequestId: boolean;
8
+ tusUrl: string;
9
+ assemblyUrl: string;
10
+ };
11
+
12
+ export type TusMetadataOptions = {
13
+ fieldName?: string;
14
+ metadata?: Record<string, string>;
15
+ };
16
+
17
+ export const buildTusUploadConfig = (
18
+ assemblyData: unknown,
19
+ file: File,
20
+ options: TusMetadataOptions = {},
21
+ ): TusUploadConfig => {
22
+ const { tusUrl, assemblyUrl } = parseAssemblyUrls(assemblyData);
23
+
24
+ if (!tusUrl) {
25
+ throw transloaditError(
26
+ "upload",
27
+ "Transloadit response missing tus_url for resumable upload",
28
+ );
29
+ }
30
+
31
+ if (!assemblyUrl) {
32
+ throw transloaditError(
33
+ "upload",
34
+ "Transloadit response missing assembly_url for resumable upload",
35
+ );
36
+ }
37
+
38
+ const metadata: Record<string, string> = {
39
+ filename: file.name,
40
+ ...options.metadata,
41
+ };
42
+ if (file.type) {
43
+ metadata.filetype = file.type;
44
+ }
45
+ if (!metadata.fieldname) {
46
+ metadata.fieldname = options.fieldName ?? "file";
47
+ }
48
+ if (!metadata.assembly_url) {
49
+ metadata.assembly_url = assemblyUrl;
50
+ }
51
+
52
+ return {
53
+ endpoint: tusUrl,
54
+ metadata,
55
+ addRequestId: true,
56
+ tusUrl,
57
+ assemblyUrl,
58
+ };
59
+ };
package/src/test/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
3
  import { convexTest } from "convex-test";
4
- import schema from "../component/schema.js";
4
+ import schema from "../component/schema.ts";
5
5
 
6
6
  export const modules = import.meta.glob("../component/**/*.*s");
7
7
 
@@ -0,0 +1,19 @@
1
+ import * as apiModule from "../component/_generated/api.ts";
2
+ import * as componentModule from "../component/_generated/component.ts";
3
+ import * as dataModelModule from "../component/_generated/dataModel.ts";
4
+ import * as serverModule from "../component/_generated/server.ts";
5
+ import * as apiUtilsModule from "../component/apiUtils.ts";
6
+ import * as convexConfigModule from "../component/convex.config.ts";
7
+ import * as libModule from "../component/lib.ts";
8
+ import * as schemaModule from "../component/schema.ts";
9
+
10
+ export const modules: Record<string, () => Promise<unknown>> = {
11
+ "../component/apiUtils.ts": async () => apiUtilsModule,
12
+ "../component/lib.ts": async () => libModule,
13
+ "../component/convex.config.ts": async () => convexConfigModule,
14
+ "../component/schema.ts": async () => schemaModule,
15
+ "../component/_generated/api.ts": async () => apiModule,
16
+ "../component/_generated/component.ts": async () => componentModule,
17
+ "../component/_generated/dataModel.ts": async () => dataModelModule,
18
+ "../component/_generated/server.ts": async () => serverModule,
19
+ };