@devultur/convex 1.0.3 → 1.1.0
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 +126 -0
- package/dist/bin/init.js +1 -0
- package/dist/{chunk-FHWT3GD6.js → chunk-GKLFRSGI.js} +2 -0
- package/dist/component/_generated/api.d.ts +12 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +49 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/lock.d.ts +33 -0
- package/dist/component/lock.d.ts.map +1 -0
- package/dist/component/lock.js +47 -0
- package/dist/component/lock.js.map +1 -0
- package/dist/component/lockValidators.d.ts +25 -0
- package/dist/component/lockValidators.d.ts.map +1 -0
- package/dist/component/lockValidators.js +14 -0
- package/dist/component/lockValidators.js.map +1 -0
- package/dist/component/mediaClient.d.ts +3 -0
- package/dist/component/mediaClient.d.ts.map +1 -0
- package/dist/component/mediaClient.js +7 -0
- package/dist/component/mediaClient.js.map +1 -0
- package/dist/component/public.d.ts +72 -0
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +48 -13
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +10 -1
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +10 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/uploadKeys.d.ts +14 -0
- package/dist/component/uploadKeys.d.ts.map +1 -0
- package/dist/component/uploadKeys.js +23 -0
- package/dist/component/uploadKeys.js.map +1 -0
- package/dist/component/uploads.d.ts +119 -0
- package/dist/component/uploads.d.ts.map +1 -0
- package/dist/component/uploads.js +106 -0
- package/dist/component/uploads.js.map +1 -0
- package/dist/component/workerErrors.d.ts +17 -0
- package/dist/component/workerErrors.d.ts.map +1 -0
- package/dist/component/workerErrors.js +41 -0
- package/dist/component/workerErrors.js.map +1 -0
- package/dist/index.d.ts +138 -4
- package/dist/index.js +102 -4
- package/dist/react.d.ts +18 -5
- package/dist/react.js +35 -13
- package/dist/validators.d.ts +4 -2
- package/dist/validators.js +1 -1
- package/package.json +7 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Worker errors across the component boundary. A plain `Error` thrown in a component function reaches the app as a
|
|
2
|
+
// message only; a `ConvexError` keeps its `data`. Every component action is built with `workerAction`, which rethrows
|
|
3
|
+
// the Worker's `DevulturApiError` as `ConvexError<DevulturErrorData>`, so the app reads `error.data.code`
|
|
4
|
+
// (`retention_active`, `lock_conflict`, `compliance_extend_only`, ...) and `error.data.details`.
|
|
5
|
+
import { DevulturApiError } from "@devultur/core";
|
|
6
|
+
import { ConvexError } from "convex/values";
|
|
7
|
+
import { action } from "./_generated/server";
|
|
8
|
+
function isApiError(error) {
|
|
9
|
+
if (error instanceof DevulturApiError)
|
|
10
|
+
return true;
|
|
11
|
+
// A second copy of `@devultur/core` (bundling) fails `instanceof`; its shape doesn't.
|
|
12
|
+
return (error instanceof Error &&
|
|
13
|
+
error.name === "DevulturApiError" &&
|
|
14
|
+
typeof error.status === "number");
|
|
15
|
+
}
|
|
16
|
+
/** The error to rethrow: a Worker refusal becomes a `ConvexError` with its code; anything else passes through. */
|
|
17
|
+
export function toComponentError(error) {
|
|
18
|
+
if (!isApiError(error))
|
|
19
|
+
return error;
|
|
20
|
+
const data = {
|
|
21
|
+
code: error.code ?? "worker_error",
|
|
22
|
+
message: error.message,
|
|
23
|
+
status: error.status,
|
|
24
|
+
// JSON from the Worker: valid Convex values once `undefined` is gone.
|
|
25
|
+
...(error.details ? { details: JSON.parse(JSON.stringify(error.details)) } : {}),
|
|
26
|
+
};
|
|
27
|
+
return new ConvexError(data);
|
|
28
|
+
}
|
|
29
|
+
/** `action` whose handler rethrows Worker refusals as `ConvexError<DevulturErrorData>`. */
|
|
30
|
+
export const workerAction = ((definition) => action({
|
|
31
|
+
...definition,
|
|
32
|
+
handler: async (ctx, args) => {
|
|
33
|
+
try {
|
|
34
|
+
return await definition.handler(ctx, args);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw toComponentError(error);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
//# sourceMappingURL=workerErrors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workerErrors.js","sourceRoot":"","sources":["../../src/component/workerErrors.ts"],"names":[],"mappings":"AAAA,mHAAmH;AACnH,sHAAsH;AACtH,0GAA0G;AAC1G,iGAAiG;AACjG,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAc,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAa7C,SAAS,UAAU,CAAC,KAAc;IACjC,IAAI,KAAK,YAAY,gBAAgB;QAAE,OAAO,IAAI,CAAC;IACnD,sFAAsF;IACtF,OAAO,CACN,KAAK,YAAY,KAAK;QACtB,KAAK,CAAC,IAAI,KAAK,kBAAkB;QACjC,OAAQ,KAA8B,CAAC,MAAM,KAAK,QAAQ,CAC1D,CAAC;AACH,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC9C,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACrC,MAAM,IAAI,GAAsB;QAC/B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,cAAc;QAClC,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,sEAAsE;QACtE,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAA0B,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACzG,CAAC;IACF,OAAO,IAAI,WAAW,CAAC,IAAwB,CAAC,CAAC;AAClD,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,UAA0E,EAAE,EAAE,CAC3G,MAAM,CAAC;IACN,GAAG,UAAU;IACb,OAAO,EAAE,KAAK,EAAE,GAAY,EAAE,IAAa,EAAE,EAAE;QAC9C,IAAI,CAAC;YACJ,OAAO,MAAM,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;IACF,CAAC;CACQ,CAAC,CAA6B,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { VideoModuleConfig } from '@devultur/core';
|
|
1
|
+
import { VideoModuleConfig, ObjectLock, LockView, SetLockOptions, HoldPage, DevulturMedia } from '@devultur/core';
|
|
2
2
|
export { CaptionLocale, DevulturConfig, DevulturKind, DevulturMedia, ImageFit, ImageFormat, SecurityLevel, SpeechProvider, StorageProvider, TranscodePreset, TranscodeProvider, TranslationProvider, VideoModuleConfig, Visibility } from '@devultur/core';
|
|
3
3
|
import * as convex_server from 'convex/server';
|
|
4
|
-
import { FunctionReference, GenericQueryCtx, GenericDataModel, GenericMutationCtx } from 'convex/server';
|
|
4
|
+
import { FunctionReference, GenericQueryCtx, GenericDataModel, GenericMutationCtx, GenericActionCtx, PaginationOptions, PaginationResult } from 'convex/server';
|
|
5
|
+
import { Value, ConvexError } from 'convex/values';
|
|
5
6
|
export { dv } from './validators.js';
|
|
6
|
-
import 'convex/values';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Generated `ComponentApi` utility.
|
|
@@ -26,6 +26,26 @@ import 'convex/values';
|
|
|
26
26
|
* ```
|
|
27
27
|
*/
|
|
28
28
|
type ComponentApi<Name extends string | undefined = string | undefined> = {
|
|
29
|
+
lock: {
|
|
30
|
+
getLock: FunctionReference<"action", "internal", {
|
|
31
|
+
key: string;
|
|
32
|
+
}, any, Name>;
|
|
33
|
+
releaseHold: FunctionReference<"action", "internal", {
|
|
34
|
+
keys: Array<string>;
|
|
35
|
+
}, any, Name>;
|
|
36
|
+
setHold: FunctionReference<"action", "internal", {
|
|
37
|
+
keys: Array<string>;
|
|
38
|
+
}, any, Name>;
|
|
39
|
+
setLock: FunctionReference<"action", "internal", {
|
|
40
|
+
bypassGovernance?: boolean;
|
|
41
|
+
derivatives?: "inherit" | "original-only";
|
|
42
|
+
key: string;
|
|
43
|
+
mode?: "governance" | "compliance";
|
|
44
|
+
period?: number;
|
|
45
|
+
remove?: true;
|
|
46
|
+
until?: string;
|
|
47
|
+
}, any, Name>;
|
|
48
|
+
};
|
|
29
49
|
public: {
|
|
30
50
|
createMedia: FunctionReference<"mutation", "internal", {
|
|
31
51
|
contentType: string;
|
|
@@ -37,6 +57,16 @@ type ComponentApi<Name extends string | undefined = string | undefined> = {
|
|
|
37
57
|
createUploadUrl: FunctionReference<"action", "internal", {
|
|
38
58
|
contentType: string;
|
|
39
59
|
filename: string;
|
|
60
|
+
hold?: boolean;
|
|
61
|
+
lock?: {
|
|
62
|
+
hold?: boolean;
|
|
63
|
+
retention?: {
|
|
64
|
+
derivatives?: "inherit" | "original-only";
|
|
65
|
+
mode: "governance" | "compliance";
|
|
66
|
+
period?: number;
|
|
67
|
+
until?: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
40
70
|
visibility?: "public" | "private";
|
|
41
71
|
}, any, Name>;
|
|
42
72
|
deleteMediaAction: FunctionReference<"action", "internal", {
|
|
@@ -62,6 +92,17 @@ type ComponentApi<Name extends string | undefined = string | undefined> = {
|
|
|
62
92
|
listMedia: FunctionReference<"query", "internal", {
|
|
63
93
|
type?: "video" | "image" | "file";
|
|
64
94
|
}, any, Name>;
|
|
95
|
+
listMediaPage: FunctionReference<"query", "internal", {
|
|
96
|
+
paginationOpts: {
|
|
97
|
+
cursor: string | null;
|
|
98
|
+
endCursor?: string | null;
|
|
99
|
+
id?: number;
|
|
100
|
+
maximumBytesRead?: number;
|
|
101
|
+
maximumRowsRead?: number;
|
|
102
|
+
numItems: number;
|
|
103
|
+
};
|
|
104
|
+
type?: "video" | "image" | "file";
|
|
105
|
+
}, any, Name>;
|
|
65
106
|
loadProgress: FunctionReference<"query", "internal", {
|
|
66
107
|
userId: string;
|
|
67
108
|
videoId: string;
|
|
@@ -108,17 +149,40 @@ type ComponentApi<Name extends string | undefined = string | undefined> = {
|
|
|
108
149
|
vttUrl: string;
|
|
109
150
|
}, any, Name>;
|
|
110
151
|
};
|
|
152
|
+
uploads: {
|
|
153
|
+
confirmUpload: FunctionReference<"action", "internal", {
|
|
154
|
+
filename?: string;
|
|
155
|
+
height?: number;
|
|
156
|
+
key: string;
|
|
157
|
+
width?: number;
|
|
158
|
+
}, any, Name>;
|
|
159
|
+
};
|
|
111
160
|
};
|
|
112
161
|
|
|
113
162
|
interface DevulturComponentConfig {
|
|
114
163
|
video?: VideoModuleConfig;
|
|
164
|
+
/**
|
|
165
|
+
* Lock every upload through the exposed `createUploadUrl` gets (retention and/or hold). Set here, on your server:
|
|
166
|
+
* the browser never chooses a lock. To lock per upload, call `devultur.createUploadUrl(ctx, { lock })` from your
|
|
167
|
+
* own function.
|
|
168
|
+
*/
|
|
169
|
+
upload?: {
|
|
170
|
+
lock?: ObjectLock;
|
|
171
|
+
};
|
|
115
172
|
}
|
|
116
173
|
interface MediaRecord {
|
|
174
|
+
/** The media id (the `id` of its `DevulturMedia`), for every kind. */
|
|
117
175
|
key: string;
|
|
118
176
|
type: "video" | "image" | "file";
|
|
119
177
|
filename: string;
|
|
120
178
|
contentType: string;
|
|
121
179
|
sizeBytes: number;
|
|
180
|
+
/** Set by a confirmed upload; unset on records from before 1.1.0. */
|
|
181
|
+
visibility?: "public" | "private";
|
|
182
|
+
/** Key of the uploaded object (a video's raw file). Set by a confirmed upload. */
|
|
183
|
+
objectKey?: string;
|
|
184
|
+
width?: number;
|
|
185
|
+
height?: number;
|
|
122
186
|
transcodeStatus?: "queued" | "transcoding" | "captioning" | "ready" | "failed";
|
|
123
187
|
transcodeProgress?: {
|
|
124
188
|
current: number;
|
|
@@ -147,6 +211,23 @@ interface ProgressRecord {
|
|
|
147
211
|
type DevulturComponent = ComponentApi;
|
|
148
212
|
type QueryCtx = GenericQueryCtx<GenericDataModel>;
|
|
149
213
|
type MutationCtx = GenericMutationCtx<GenericDataModel>;
|
|
214
|
+
type ActionCtx = GenericActionCtx<GenericDataModel>;
|
|
215
|
+
type UploadUrlArgs = {
|
|
216
|
+
filename: string;
|
|
217
|
+
contentType: string;
|
|
218
|
+
visibility?: "public" | "private";
|
|
219
|
+
};
|
|
220
|
+
type MediaType = "video" | "image" | "file";
|
|
221
|
+
type MediaPageArgs = {
|
|
222
|
+
type?: MediaType;
|
|
223
|
+
paginationOpts: PaginationOptions;
|
|
224
|
+
};
|
|
225
|
+
type ConfirmUploadArgs = {
|
|
226
|
+
key: string;
|
|
227
|
+
filename?: string;
|
|
228
|
+
width?: number;
|
|
229
|
+
height?: number;
|
|
230
|
+
};
|
|
150
231
|
declare class Devultur {
|
|
151
232
|
private component;
|
|
152
233
|
constructor(component: DevulturComponent);
|
|
@@ -160,6 +241,34 @@ declare class Devultur {
|
|
|
160
241
|
userId: string;
|
|
161
242
|
videoId: string;
|
|
162
243
|
}): Promise<any>;
|
|
244
|
+
/** An upload URL from your server, optionally locked (retention and/or hold, written with the bytes). */
|
|
245
|
+
createUploadUrl(ctx: ActionCtx, args: UploadUrlArgs & {
|
|
246
|
+
lock?: ObjectLock;
|
|
247
|
+
hold?: boolean;
|
|
248
|
+
}): Promise<any>;
|
|
249
|
+
/**
|
|
250
|
+
* Records an upload in the component once its bytes are stored (after the PUT): it then shows in `listMedia`.
|
|
251
|
+
* Returns the record, or `null` when nothing is stored at `key`. Uploads nobody confirms are recorded a day later.
|
|
252
|
+
*/
|
|
253
|
+
confirmUpload(ctx: ActionCtx, args: ConfirmUploadArgs): Promise<MediaRecord | null>;
|
|
254
|
+
/**
|
|
255
|
+
* The recorded media (every confirmed upload, and every video sent to `processVideo`), optionally of one type.
|
|
256
|
+
* `mediaFromRecord(record)` turns a row into the reference `<Image>` and `dv.media()` take. Unbounded: it reads every
|
|
257
|
+
* record, so a media library pages with `listMediaPage`.
|
|
258
|
+
*/
|
|
259
|
+
listMedia(ctx: QueryCtx, args?: {
|
|
260
|
+
type?: MediaType;
|
|
261
|
+
}): Promise<MediaRecord[]>;
|
|
262
|
+
/** One page of the recorded media, newest first, optionally of one type. Takes Convex's `paginationOpts`. */
|
|
263
|
+
listMediaPage(ctx: QueryCtx, args: MediaPageArgs): Promise<PaginationResult<MediaRecord>>;
|
|
264
|
+
/** The lock in force on `key`. */
|
|
265
|
+
getLock(ctx: ActionCtx, key: string): Promise<LockView>;
|
|
266
|
+
/** Sets or extends `key`'s retention (shortening a governance one needs `bypassGovernance` and the Worker's hook). */
|
|
267
|
+
setLock(ctx: ActionCtx, key: string, options: SetLockOptions): Promise<LockView>;
|
|
268
|
+
/** Holds `keys` (any number: sent in pages). A bulk hold by prefix uses `@devultur/core`'s client on your server. */
|
|
269
|
+
setHold(ctx: ActionCtx, keys: string[]): Promise<Pick<HoldPage, "updated" | "skipped">>;
|
|
270
|
+
/** Releases `keys`' holds: a retention `period` starts now; without a retention they're free. */
|
|
271
|
+
releaseHold(ctx: ActionCtx, keys: string[]): Promise<Pick<HoldPage, "updated" | "skipped">>;
|
|
163
272
|
static exposeApi(component: DevulturComponent, config?: DevulturComponentConfig): {
|
|
164
273
|
issueViewerToken: convex_server.RegisteredAction<"public", {
|
|
165
274
|
sessionSecret?: string;
|
|
@@ -169,6 +278,7 @@ declare class Devultur {
|
|
|
169
278
|
contentType: string;
|
|
170
279
|
visibility?: "public" | "private";
|
|
171
280
|
}, Promise<any>>;
|
|
281
|
+
confirmUpload: convex_server.RegisteredAction<"public", ConfirmUploadArgs, Promise<any>>;
|
|
172
282
|
deleteMedia: convex_server.RegisteredAction<"public", {
|
|
173
283
|
key: string;
|
|
174
284
|
}, Promise<any>>;
|
|
@@ -182,6 +292,7 @@ declare class Devultur {
|
|
|
182
292
|
getMedia: convex_server.RegisteredQuery<"public", {
|
|
183
293
|
key: string;
|
|
184
294
|
}, Promise<any>>;
|
|
295
|
+
listMediaPage: convex_server.RegisteredQuery<"public", MediaPageArgs, Promise<any>>;
|
|
185
296
|
saveProgress: convex_server.RegisteredMutation<"public", {
|
|
186
297
|
userId: string;
|
|
187
298
|
videoId: string;
|
|
@@ -202,4 +313,27 @@ declare class Devultur {
|
|
|
202
313
|
};
|
|
203
314
|
}
|
|
204
315
|
|
|
205
|
-
|
|
316
|
+
/** `data` of the `ConvexError` a component action throws when the Worker refuses a request. */
|
|
317
|
+
interface DevulturErrorData {
|
|
318
|
+
/** The Worker's stable code (`retention_active`, `file_too_large`, ...), or `worker_error` when it sent none. */
|
|
319
|
+
code: string;
|
|
320
|
+
message: string;
|
|
321
|
+
/** HTTP status of the Worker's answer. */
|
|
322
|
+
status: number;
|
|
323
|
+
/** The rest of the Worker's error body, e.g. `{ key, mode, until, hold }` on `retention_active`. */
|
|
324
|
+
details?: Record<string, Value>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** True when `error` is a Worker refusal from a component action. */
|
|
328
|
+
declare function isDevulturError(error: unknown): error is ConvexError<DevulturErrorData & Record<string, never>>;
|
|
329
|
+
/** The refusal's `{ code, message, status, details? }`, or `null` when `error` isn't one. */
|
|
330
|
+
declare function devulturErrorOf(error: unknown): DevulturErrorData | null;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The `DevulturMedia` reference of a component record (a `listMedia` row), ready for `<Image>`, `<DownloadLink>` or a
|
|
334
|
+
* `dv.media()` field. `null` for a record without a visibility: one from before 1.1.0, or a video only `processVideo`
|
|
335
|
+
* recorded.
|
|
336
|
+
*/
|
|
337
|
+
declare function mediaFromRecord(record: MediaRecord): DevulturMedia | null;
|
|
338
|
+
|
|
339
|
+
export { Devultur, type DevulturComponentConfig, type DevulturErrorData, type MediaRecord, type ProgressRecord, devulturErrorOf, isDevulturError, mediaFromRecord };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
dv
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GKLFRSGI.js";
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
6
|
import {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
// src/client.ts
|
|
19
19
|
import { extractId } from "@devultur/core";
|
|
20
|
-
import { actionGeneric, mutationGeneric, queryGeneric } from "convex/server";
|
|
20
|
+
import { actionGeneric, mutationGeneric, paginationOptsValidator, queryGeneric } from "convex/server";
|
|
21
21
|
import { v } from "convex/values";
|
|
22
22
|
var Devultur = class {
|
|
23
23
|
component;
|
|
@@ -33,10 +33,50 @@ var Devultur = class {
|
|
|
33
33
|
async loadProgress(ctx, args) {
|
|
34
34
|
return ctx.runQuery(this.component.public.loadProgress, args);
|
|
35
35
|
}
|
|
36
|
+
/** An upload URL from your server, optionally locked (retention and/or hold, written with the bytes). */
|
|
37
|
+
async createUploadUrl(ctx, args) {
|
|
38
|
+
return ctx.runAction(this.component.public.createUploadUrl, args);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Records an upload in the component once its bytes are stored (after the PUT): it then shows in `listMedia`.
|
|
42
|
+
* Returns the record, or `null` when nothing is stored at `key`. Uploads nobody confirms are recorded a day later.
|
|
43
|
+
*/
|
|
44
|
+
async confirmUpload(ctx, args) {
|
|
45
|
+
return ctx.runAction(this.component.uploads.confirmUpload, args);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The recorded media (every confirmed upload, and every video sent to `processVideo`), optionally of one type.
|
|
49
|
+
* `mediaFromRecord(record)` turns a row into the reference `<Image>` and `dv.media()` take. Unbounded: it reads every
|
|
50
|
+
* record, so a media library pages with `listMediaPage`.
|
|
51
|
+
*/
|
|
52
|
+
async listMedia(ctx, args) {
|
|
53
|
+
return ctx.runQuery(this.component.public.listMedia, args ?? {});
|
|
54
|
+
}
|
|
55
|
+
/** One page of the recorded media, newest first, optionally of one type. Takes Convex's `paginationOpts`. */
|
|
56
|
+
async listMediaPage(ctx, args) {
|
|
57
|
+
return ctx.runQuery(this.component.public.listMediaPage, args);
|
|
58
|
+
}
|
|
59
|
+
/** The lock in force on `key`. */
|
|
60
|
+
async getLock(ctx, key) {
|
|
61
|
+
return ctx.runAction(this.component.lock.getLock, { key });
|
|
62
|
+
}
|
|
63
|
+
/** Sets or extends `key`'s retention (shortening a governance one needs `bypassGovernance` and the Worker's hook). */
|
|
64
|
+
async setLock(ctx, key, options) {
|
|
65
|
+
return ctx.runAction(this.component.lock.setLock, { key, ...options });
|
|
66
|
+
}
|
|
67
|
+
/** Holds `keys` (any number: sent in pages). A bulk hold by prefix uses `@devultur/core`'s client on your server. */
|
|
68
|
+
async setHold(ctx, keys) {
|
|
69
|
+
return ctx.runAction(this.component.lock.setHold, { keys });
|
|
70
|
+
}
|
|
71
|
+
/** Releases `keys`' holds: a retention `period` starts now; without a retention they're free. */
|
|
72
|
+
async releaseHold(ctx, keys) {
|
|
73
|
+
return ctx.runAction(this.component.lock.releaseHold, { keys });
|
|
74
|
+
}
|
|
36
75
|
static exposeApi(component, config) {
|
|
37
76
|
const preset = config?.video?.transcodePreset;
|
|
38
77
|
const locales = config?.video?.captionLocales ?? [];
|
|
39
78
|
const visibilityArg = v.optional(v.union(v.literal("public"), v.literal("private")));
|
|
79
|
+
const uploadLock = config?.upload?.lock;
|
|
40
80
|
return {
|
|
41
81
|
issueViewerToken: actionGeneric({
|
|
42
82
|
args: { sessionSecret: v.optional(v.string()) },
|
|
@@ -47,7 +87,23 @@ var Devultur = class {
|
|
|
47
87
|
createUploadUrl: actionGeneric({
|
|
48
88
|
args: { filename: v.string(), contentType: v.string(), visibility: visibilityArg },
|
|
49
89
|
handler: async (ctx, args) => {
|
|
50
|
-
return ctx.runAction(component.public.createUploadUrl,
|
|
90
|
+
return ctx.runAction(component.public.createUploadUrl, {
|
|
91
|
+
...args,
|
|
92
|
+
...uploadLock ? { lock: uploadLock } : {}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
// Records an upload once its bytes are stored (the Worker HEADs the key): a browser can't list anything
|
|
97
|
+
// that isn't in your bucket. `useDevultur().upload()` and `useMediaUpload` call it after the PUT.
|
|
98
|
+
confirmUpload: actionGeneric({
|
|
99
|
+
args: {
|
|
100
|
+
key: v.string(),
|
|
101
|
+
filename: v.optional(v.string()),
|
|
102
|
+
width: v.optional(v.number()),
|
|
103
|
+
height: v.optional(v.number())
|
|
104
|
+
},
|
|
105
|
+
handler: async (ctx, args) => {
|
|
106
|
+
return ctx.runAction(component.uploads.confirmUpload, args);
|
|
51
107
|
}
|
|
52
108
|
}),
|
|
53
109
|
deleteMedia: actionGeneric({
|
|
@@ -79,6 +135,17 @@ var Devultur = class {
|
|
|
79
135
|
return ctx.runQuery(component.public.getMediaByKey, { key: mediaKey });
|
|
80
136
|
}
|
|
81
137
|
}),
|
|
138
|
+
// The media library, a page at a time (for `usePaginatedQuery`). It lists every record, of every user: export
|
|
139
|
+
// it only when every caller may see the whole library, otherwise wrap `devultur.listMediaPage` with your checks.
|
|
140
|
+
listMediaPage: queryGeneric({
|
|
141
|
+
args: {
|
|
142
|
+
type: v.optional(v.union(v.literal("video"), v.literal("image"), v.literal("file"))),
|
|
143
|
+
paginationOpts: paginationOptsValidator
|
|
144
|
+
},
|
|
145
|
+
handler: async (ctx, args) => {
|
|
146
|
+
return ctx.runQuery(component.public.listMediaPage, args);
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
82
149
|
saveProgress: mutationGeneric({
|
|
83
150
|
args: { userId: v.string(), videoId: v.string(), position: v.number() },
|
|
84
151
|
handler: async (ctx, args) => {
|
|
@@ -106,6 +173,34 @@ var Devultur = class {
|
|
|
106
173
|
};
|
|
107
174
|
}
|
|
108
175
|
};
|
|
176
|
+
|
|
177
|
+
// src/errors.ts
|
|
178
|
+
import { ConvexError } from "convex/values";
|
|
179
|
+
function isErrorData(data) {
|
|
180
|
+
if (typeof data !== "object" || data === null) return false;
|
|
181
|
+
const fields = data;
|
|
182
|
+
return typeof fields.code === "string" && typeof fields.status === "number" && typeof fields.message === "string";
|
|
183
|
+
}
|
|
184
|
+
function isDevulturError(error) {
|
|
185
|
+
return error instanceof ConvexError && isErrorData(error.data);
|
|
186
|
+
}
|
|
187
|
+
function devulturErrorOf(error) {
|
|
188
|
+
return isDevulturError(error) ? error.data : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/media-record.ts
|
|
192
|
+
function mediaFromRecord(record) {
|
|
193
|
+
if (!record.visibility) return null;
|
|
194
|
+
return {
|
|
195
|
+
id: record.key,
|
|
196
|
+
kind: record.type,
|
|
197
|
+
visibility: record.visibility,
|
|
198
|
+
filename: record.filename,
|
|
199
|
+
...record.objectKey ? { key: record.objectKey } : {},
|
|
200
|
+
...record.width !== void 0 ? { width: record.width } : {},
|
|
201
|
+
...record.height !== void 0 ? { height: record.height } : {}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
109
204
|
export {
|
|
110
205
|
CaptionLocale,
|
|
111
206
|
Devultur,
|
|
@@ -117,5 +212,8 @@ export {
|
|
|
117
212
|
TranscodePreset,
|
|
118
213
|
TranscodeProvider,
|
|
119
214
|
TranslationProvider,
|
|
120
|
-
|
|
215
|
+
devulturErrorOf,
|
|
216
|
+
dv,
|
|
217
|
+
isDevulturError,
|
|
218
|
+
mediaFromRecord
|
|
121
219
|
};
|
package/dist/react.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Visibility, DevulturMedia, MediaUrlOptions, ProcessingPhase, FailureReason, MediaStatusValue } from '@devultur/core';
|
|
3
|
-
import { BakeOutputFormat, MediaUploadState, MediaUploadError } from '@devultur/react';
|
|
4
|
-
export { MediaUploadError, MediaUploadState } from '@devultur/react';
|
|
3
|
+
import { BakeOutputFormat, MediaUploadState, MediaUploadError, UploadProgress } from '@devultur/react';
|
|
4
|
+
export { MediaUploadError, MediaUploadState, UploadProgress } from '@devultur/react';
|
|
5
5
|
import { FunctionReference } from 'convex/server';
|
|
6
6
|
|
|
7
7
|
interface CropUploadRenderProps {
|
|
@@ -59,6 +59,8 @@ type AnyQuery = FunctionReference<"query">;
|
|
|
59
59
|
interface DevulturApi {
|
|
60
60
|
issueViewerToken: AnyAction;
|
|
61
61
|
createUploadUrl: AnyAction;
|
|
62
|
+
/** Records an upload in the component after its PUT, so it shows in `listMedia`. Optional (uploads still work). */
|
|
63
|
+
confirmUpload?: AnyAction;
|
|
62
64
|
deleteMedia: AnyAction;
|
|
63
65
|
/** Deletes the full folder of a video (raw/hls/thumbnails/captions). Required to delete videos. */
|
|
64
66
|
deleteVideo?: AnyAction;
|
|
@@ -73,7 +75,18 @@ interface DevulturApi {
|
|
|
73
75
|
interface UploadOptions {
|
|
74
76
|
/** Visibility override for this resource. Omitted → inherits the router's default by type. */
|
|
75
77
|
visibility?: Visibility;
|
|
78
|
+
/** Progress of the PUT: bytes sent, total bytes and their fraction (0 to 1). */
|
|
79
|
+
onProgress?: (progress: UploadProgress) => void;
|
|
80
|
+
/** Aborts the PUT → `upload()` rejects with an `AbortError`. */
|
|
81
|
+
signal?: AbortSignal;
|
|
76
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The reference `upload()` resolves to: a `DevulturMedia` whose `key` is always set (the uploaded object's key; a
|
|
85
|
+
* video's raw file, what `processVideo` takes). It stores as-is in a `dv.media()` field.
|
|
86
|
+
*/
|
|
87
|
+
type UploadedMedia = DevulturMedia & {
|
|
88
|
+
readonly key: string;
|
|
89
|
+
};
|
|
77
90
|
interface DevulturContextValue {
|
|
78
91
|
token: string | null;
|
|
79
92
|
baseUrl: string | null;
|
|
@@ -83,8 +96,8 @@ interface DevulturContextValue {
|
|
|
83
96
|
url: string;
|
|
84
97
|
key: string;
|
|
85
98
|
}>;
|
|
86
|
-
/** Uploads a file and returns the
|
|
87
|
-
upload: (file: File, options?: UploadOptions) => Promise<
|
|
99
|
+
/** Uploads a file (reporting progress), records it in the component, and returns the reference ready to save. */
|
|
100
|
+
upload: (file: File, options?: UploadOptions) => Promise<UploadedMedia>;
|
|
88
101
|
/** Delivery URL from a reference (non-hook → usable in handlers and `.map`). Token-transparent. */
|
|
89
102
|
url: (media: DevulturMedia, options?: MediaRenderOptions) => string;
|
|
90
103
|
/** Downloads a video's subtitle text for a locale (auth-transparent). */
|
|
@@ -278,4 +291,4 @@ interface UseMediaUploadReturn {
|
|
|
278
291
|
*/
|
|
279
292
|
declare function useMediaUpload(options?: UseMediaUploadOptions): UseMediaUploadReturn;
|
|
280
293
|
|
|
281
|
-
export { type CaptionTrack, CropUpload, type CropUploadLabels, type CropUploadProps, type CropUploadRenderProps, DevulturProvider, DownloadLink, Image, type UseMediaUploadOptions, type UseMediaUploadReturn, type VideoProps, type VideoState, useDevultur, useDevulturMedia, useDevulturProgress, useMediaUpload, useMediaUrl, useVideoProps, useVideoState };
|
|
294
|
+
export { type CaptionTrack, CropUpload, type CropUploadLabels, type CropUploadProps, type CropUploadRenderProps, DevulturProvider, DownloadLink, Image, type UploadedMedia, type UseMediaUploadOptions, type UseMediaUploadReturn, type VideoProps, type VideoState, useDevultur, useDevulturMedia, useDevulturProgress, useMediaUpload, useMediaUrl, useVideoProps, useVideoState };
|
package/dist/react.js
CHANGED
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
SESSION_HEADER,
|
|
14
14
|
visibilityFromKey
|
|
15
15
|
} from "@devultur/core";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
UploadHttpError,
|
|
18
|
+
uploadFileWithProgress
|
|
19
|
+
} from "@devultur/react";
|
|
17
20
|
import { useAction, useConvex, useMutation, useQuery } from "convex/react";
|
|
18
21
|
|
|
19
22
|
// src/image-recovery.ts
|
|
@@ -175,6 +178,7 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
175
178
|
const issueViewerToken = useAction(api.issueViewerToken);
|
|
176
179
|
const createUploadUrlAction = useAction(api.createUploadUrl);
|
|
177
180
|
const deleteMediaAction = useAction(api.deleteMedia);
|
|
181
|
+
const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
|
|
178
182
|
const deleteVideoAction = useAction(api.deleteVideo ?? api.deleteMedia);
|
|
179
183
|
const refreshTimerRef = React2.useRef(null);
|
|
180
184
|
const reissueInFlightRef = React2.useRef(null);
|
|
@@ -230,26 +234,23 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
230
234
|
const upload = React2.useCallback(
|
|
231
235
|
async (file, options) => {
|
|
232
236
|
const dimensions = await readImageDimensions(file);
|
|
233
|
-
const
|
|
237
|
+
const target = await createUploadUrlAction({
|
|
234
238
|
filename: file.name,
|
|
235
239
|
contentType: file.type,
|
|
236
240
|
visibility: options?.visibility
|
|
237
241
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
body: file,
|
|
241
|
-
headers: { "Content-Type": file.type }
|
|
242
|
-
});
|
|
243
|
-
if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.status}`);
|
|
242
|
+
await uploadFileWithProgress(target.url, file, { onBytes: options?.onProgress, signal: options?.signal });
|
|
243
|
+
if (api.confirmUpload) await confirmUploadBestEffort(confirmUploadAction, target.key, file, dimensions);
|
|
244
244
|
return {
|
|
245
|
-
id:
|
|
246
|
-
kind:
|
|
247
|
-
visibility:
|
|
245
|
+
id: target.id,
|
|
246
|
+
kind: target.kind,
|
|
247
|
+
visibility: target.visibility,
|
|
248
248
|
filename: file.name,
|
|
249
|
+
key: target.key,
|
|
249
250
|
...dimensions
|
|
250
251
|
};
|
|
251
252
|
},
|
|
252
|
-
[createUploadUrlAction]
|
|
253
|
+
[createUploadUrlAction, confirmUploadAction, api.confirmUpload]
|
|
253
254
|
);
|
|
254
255
|
const url = React2.useCallback(
|
|
255
256
|
(media, options) => {
|
|
@@ -381,6 +382,13 @@ function useMediaUrl(key, options) {
|
|
|
381
382
|
return buildMediaUrl(baseUrl, key, { ...options, token: isPublic ? void 0 : token ?? void 0 });
|
|
382
383
|
}, [key, baseUrl, token, options]);
|
|
383
384
|
}
|
|
385
|
+
async function confirmUploadBestEffort(confirmUpload, key, file, dimensions) {
|
|
386
|
+
try {
|
|
387
|
+
await confirmUpload({ key, filename: file.name, ...dimensions });
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.warn("Devultur: could not record the upload; it is recorded later", error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
384
392
|
async function readImageDimensions(file) {
|
|
385
393
|
if (typeof createImageBitmap !== "function" || !file.type.startsWith("image/")) return {};
|
|
386
394
|
try {
|
|
@@ -628,6 +636,7 @@ function useMediaUpload(options) {
|
|
|
628
636
|
if (!api) throw new Error("useMediaUpload must be used within <DevulturProvider>");
|
|
629
637
|
const createUploadUrlAction = useAction(api.createUploadUrl);
|
|
630
638
|
const deleteMediaAction = useAction(api.deleteMedia);
|
|
639
|
+
const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
|
|
631
640
|
const processVideoAction = useAction(api.processVideo ?? api.createUploadUrl);
|
|
632
641
|
const [phase, setPhase] = React2.useState("idle");
|
|
633
642
|
const [percent, setPercent] = React2.useState(0);
|
|
@@ -693,11 +702,13 @@ function useMediaUpload(options) {
|
|
|
693
702
|
const rejected = err instanceof UploadHttpError && (err.status === 413 || err.status === 415);
|
|
694
703
|
return fail(rejected ? "rejected" : "network", err instanceof Error ? err.message : "The upload failed");
|
|
695
704
|
}
|
|
705
|
+
if (api.confirmUpload) await confirmUploadBestEffort(confirmUploadAction, result.key, file, dimensions);
|
|
696
706
|
const uploaded = {
|
|
697
707
|
id: result.id,
|
|
698
708
|
kind: result.kind,
|
|
699
709
|
visibility: result.visibility,
|
|
700
710
|
filename: file.name,
|
|
711
|
+
key: result.key,
|
|
701
712
|
...dimensions
|
|
702
713
|
};
|
|
703
714
|
setMedia(uploaded);
|
|
@@ -708,7 +719,18 @@ function useMediaUpload(options) {
|
|
|
708
719
|
if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
|
|
709
720
|
return uploaded;
|
|
710
721
|
},
|
|
711
|
-
[
|
|
722
|
+
[
|
|
723
|
+
createUploadUrlAction,
|
|
724
|
+
confirmUploadAction,
|
|
725
|
+
api.confirmUpload,
|
|
726
|
+
deleteMediaAction,
|
|
727
|
+
triggerProcess,
|
|
728
|
+
visibility,
|
|
729
|
+
replaces,
|
|
730
|
+
process,
|
|
731
|
+
onComplete,
|
|
732
|
+
onError
|
|
733
|
+
]
|
|
712
734
|
);
|
|
713
735
|
const cancel = React2.useCallback(() => {
|
|
714
736
|
abortRef.current?.abort();
|
package/dist/validators.d.ts
CHANGED
|
@@ -12,9 +12,10 @@ import { DevulturKind } from '@devultur/core';
|
|
|
12
12
|
*/
|
|
13
13
|
declare const dv: {
|
|
14
14
|
media(kind?: DevulturKind): convex_values.VObject<{
|
|
15
|
+
key?: string | undefined;
|
|
15
16
|
filename?: string | undefined;
|
|
16
|
-
width?: number | undefined;
|
|
17
17
|
height?: number | undefined;
|
|
18
|
+
width?: number | undefined;
|
|
18
19
|
visibility: "public" | "private";
|
|
19
20
|
id: string;
|
|
20
21
|
kind: DevulturKind;
|
|
@@ -23,9 +24,10 @@ declare const dv: {
|
|
|
23
24
|
id: convex_values.VString<string, "required">;
|
|
24
25
|
visibility: convex_values.VUnion<"public" | "private", [convex_values.VLiteral<"public", "required">, convex_values.VLiteral<"private", "required">], "required", never>;
|
|
25
26
|
filename: convex_values.VString<string | undefined, "optional">;
|
|
27
|
+
key: convex_values.VString<string | undefined, "optional">;
|
|
26
28
|
width: convex_values.VFloat64<number | undefined, "optional">;
|
|
27
29
|
height: convex_values.VFloat64<number | undefined, "optional">;
|
|
28
|
-
}, "required", "filename" | "visibility" | "
|
|
30
|
+
}, "required", "key" | "filename" | "visibility" | "height" | "width" | "id" | "kind">;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export { dv };
|
package/dist/validators.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devultur/convex",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Convex component for Devultur media: media records, uploads, transcodes, watch progress and React bindings",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"convex",
|
|
@@ -51,8 +51,8 @@
|
|
|
51
51
|
"dist"
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@devultur/core": "1.0
|
|
55
|
-
"@devultur/react": "1.0
|
|
54
|
+
"@devultur/core": "1.1.0",
|
|
55
|
+
"@devultur/react": "1.1.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"convex": ">=1.25.0",
|
|
@@ -64,10 +64,14 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
+
"@testing-library/react": "^16.3.3",
|
|
67
68
|
"@types/node": "^22.0.0",
|
|
68
69
|
"@types/react": "^19.0.0",
|
|
70
|
+
"@types/react-dom": "^19.3.0",
|
|
69
71
|
"convex": "^1.21.0",
|
|
72
|
+
"jsdom": "^30.1.3",
|
|
70
73
|
"react": "^19.0.0",
|
|
74
|
+
"react-dom": "^19.2.7",
|
|
71
75
|
"tsup": "^8.0.0",
|
|
72
76
|
"typescript": "^5.8.0"
|
|
73
77
|
},
|