@devultur/convex 1.3.2 → 1.4.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 +426 -9
- package/dist/bin/init.js +44 -23
- package/dist/chunk-EX3W77VY.js +245 -0
- package/dist/chunk-HOFTKJWT.js +58 -0
- package/dist/chunk-KAIVI7RB.js +21 -0
- package/dist/chunk-QBHIZBOQ.js +139 -0
- package/dist/{chunk-GKLFRSGI.js → chunk-YIPZZDGD.js} +8 -1
- package/dist/component/_generated/api.d.ts +6 -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 +27 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/library.d.ts +93 -0
- package/dist/component/library.d.ts.map +1 -0
- package/dist/component/library.js +74 -0
- package/dist/component/library.js.map +1 -0
- package/dist/component/mediaMeta.d.ts +46 -0
- package/dist/component/mediaMeta.d.ts.map +1 -0
- package/dist/component/mediaMeta.js +82 -0
- package/dist/component/mediaMeta.js.map +1 -0
- package/dist/component/metadata.d.ts +59 -0
- package/dist/component/metadata.d.ts.map +1 -0
- package/dist/component/metadata.js +32 -0
- package/dist/component/metadata.js.map +1 -0
- package/dist/component/public.d.ts +17 -1
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +20 -11
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +17 -2
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +13 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/uploads.d.ts +13 -0
- package/dist/component/uploads.d.ts.map +1 -1
- package/dist/component/uploads.js +24 -5
- package/dist/component/uploads.js.map +1 -1
- package/dist/error-labels-BfqcFh2R.d.ts +19 -0
- package/dist/index.d.ts +204 -93
- package/dist/index.js +200 -128
- package/dist/locales/es.d.ts +11 -0
- package/dist/locales/es.js +162 -0
- package/dist/media-picker-labels-CiVcNUZo.d.ts +114 -0
- package/dist/{pdf-render-CiCK6V1X.d.ts → pdf-render-BwJGagvz.d.ts} +1 -1
- package/dist/pdfjs.d.ts +13 -2
- package/dist/pdfjs.js +9 -0
- package/dist/react.d.ts +441 -66
- package/dist/react.js +1663 -550
- package/dist/types-LrusGVDf.d.ts +62 -0
- package/dist/validators.d.ts +17 -5
- package/dist/validators.js +1 -1
- package/package.json +8 -4
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type PaginationOptions, type PaginationResult } from "convex/server";
|
|
2
|
+
import type { Doc } from "./_generated/dataModel";
|
|
3
|
+
import type { QueryCtx } from "./_generated/server";
|
|
4
|
+
type MediaType = Doc<"media">["type"];
|
|
5
|
+
export declare const BACKFILL_BATCH_SIZE: {
|
|
6
|
+
readonly default: 100;
|
|
7
|
+
readonly max: 1000;
|
|
8
|
+
};
|
|
9
|
+
/** One page of the library, newest first, optionally of one kind (`by_type` index). */
|
|
10
|
+
export declare function listNewestFirst(ctx: QueryCtx, type: MediaType | undefined, paginationOpts: PaginationOptions): Promise<PaginationResult<Doc<"media">>>;
|
|
11
|
+
/**
|
|
12
|
+
* One page of the records whose title or filename matches `search` (the last word as a prefix), best match first,
|
|
13
|
+
* optionally of one kind. A blank `search` lists the library newest first, like `listMediaPage`. Records from before
|
|
14
|
+
* 1.4.0 match only once they have a `searchText` (`backfillSearchText`, or any write to them).
|
|
15
|
+
*/
|
|
16
|
+
export declare const searchMedia: import("convex/server").RegisteredQuery<"public", {
|
|
17
|
+
type?: "video" | "image" | "file" | undefined;
|
|
18
|
+
search: string;
|
|
19
|
+
paginationOpts: {
|
|
20
|
+
id?: number;
|
|
21
|
+
endCursor?: string | null;
|
|
22
|
+
maximumRowsRead?: number;
|
|
23
|
+
maximumBytesRead?: number;
|
|
24
|
+
numItems: number;
|
|
25
|
+
cursor: string | null;
|
|
26
|
+
};
|
|
27
|
+
}, Promise<PaginationResult<{
|
|
28
|
+
_id: import("convex/values").GenericId<"media">;
|
|
29
|
+
_creationTime: number;
|
|
30
|
+
width?: number | undefined;
|
|
31
|
+
height?: number | undefined;
|
|
32
|
+
visibility?: "public" | "private" | undefined;
|
|
33
|
+
objectKey?: string | undefined;
|
|
34
|
+
pageCount?: number | undefined;
|
|
35
|
+
title?: string | undefined;
|
|
36
|
+
alt?: string | undefined;
|
|
37
|
+
tags?: string[] | undefined;
|
|
38
|
+
searchText?: string | undefined;
|
|
39
|
+
transcodeStatus?: "queued" | "transcoding" | "captioning" | "ready" | "failed" | undefined;
|
|
40
|
+
transcodeProgress?: {
|
|
41
|
+
phase?: "captioning" | "downloading" | "encoding" | "uploading" | undefined;
|
|
42
|
+
current: number;
|
|
43
|
+
total: number;
|
|
44
|
+
} | undefined;
|
|
45
|
+
transcodeError?: string | undefined;
|
|
46
|
+
failureReason?: "invalid_input" | "unsupported_codec" | "timeout" | "capacity" | "internal" | undefined;
|
|
47
|
+
retriable?: boolean | undefined;
|
|
48
|
+
playlistUrl?: string | undefined;
|
|
49
|
+
thumbnailKey?: string | undefined;
|
|
50
|
+
captionLocales?: string[] | undefined;
|
|
51
|
+
captionsPending?: boolean | undefined;
|
|
52
|
+
duration?: number | undefined;
|
|
53
|
+
renditions?: {
|
|
54
|
+
width?: number | undefined;
|
|
55
|
+
height?: number | undefined;
|
|
56
|
+
variant: string;
|
|
57
|
+
kind: "hls" | "mp4";
|
|
58
|
+
rung: string;
|
|
59
|
+
key: string;
|
|
60
|
+
url: string;
|
|
61
|
+
}[] | undefined;
|
|
62
|
+
posterUrls?: {
|
|
63
|
+
jpg?: string | undefined;
|
|
64
|
+
avif?: string | undefined;
|
|
65
|
+
} | undefined;
|
|
66
|
+
hasAudio?: boolean | undefined;
|
|
67
|
+
storyboardKey?: string | undefined;
|
|
68
|
+
type: "video" | "image" | "file";
|
|
69
|
+
key: string;
|
|
70
|
+
filename: string;
|
|
71
|
+
contentType: string;
|
|
72
|
+
sizeBytes: number;
|
|
73
|
+
createdAt: number;
|
|
74
|
+
}>>>;
|
|
75
|
+
interface BackfillResult {
|
|
76
|
+
/** Records given a `searchText` by this batch. */
|
|
77
|
+
updated: number;
|
|
78
|
+
/** `false` while later batches are scheduled. */
|
|
79
|
+
isDone: boolean;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Gives every record without a `searchText` (written before 1.4.0) its title and filename, `batchSize` records per
|
|
83
|
+
* mutation (default 100, at most 1000). Runs the first batch now and schedules the rest; safe to run again.
|
|
84
|
+
*/
|
|
85
|
+
export declare const backfillSearchText: import("convex/server").RegisteredMutation<"public", {
|
|
86
|
+
batchSize?: number | undefined;
|
|
87
|
+
}, Promise<BackfillResult>>;
|
|
88
|
+
export declare const backfillSearchTextBatch: import("convex/server").RegisteredMutation<"internal", {
|
|
89
|
+
cursor: string | null;
|
|
90
|
+
batchSize: number;
|
|
91
|
+
}, Promise<BackfillResult>>;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=library.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"library.d.ts","sourceRoot":"","sources":["../../src/component/library.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAA2B,MAAM,eAAe,CAAC;AAGvG,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,KAAK,EAAe,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAKjE,KAAK,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;AAItC,eAAO,MAAM,mBAAmB;;;CAAuC,CAAC;AAExE,uFAAuF;AACvF,wBAAgB,eAAe,CAC9B,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,SAAS,GAAG,SAAS,EAC3B,cAAc,EAAE,iBAAiB,GAC/B,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAGzC;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAetB,CAAC;AAEH,UAAU,cAAc;IACvB,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,MAAM,EAAE,OAAO,CAAC;CAChB;AA0BD;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;2BAG7B,CAAC;AAEH,eAAO,MAAM,uBAAuB;;;2BAGlC,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The media library: search over the records' `searchText` (title + filename), and the one-off backfill that gives
|
|
2
|
+
// records from before 1.4.0 the `searchText` they were written without.
|
|
3
|
+
import { paginationOptsValidator } from "convex/server";
|
|
4
|
+
import { v } from "convex/values";
|
|
5
|
+
import { internal } from "./_generated/api";
|
|
6
|
+
import { internalMutation, mutation, query } from "./_generated/server";
|
|
7
|
+
import { missingSearchText } from "./mediaMeta";
|
|
8
|
+
const mediaType = v.union(v.literal("video"), v.literal("image"), v.literal("file"));
|
|
9
|
+
/** Longest search kept: Convex matches at most 16 terms, so a longer query only costs. */
|
|
10
|
+
const SEARCH_MAX_LENGTH = 200;
|
|
11
|
+
export const BACKFILL_BATCH_SIZE = { default: 100, max: 1000 };
|
|
12
|
+
/** One page of the library, newest first, optionally of one kind (`by_type` index). */
|
|
13
|
+
export function listNewestFirst(ctx, type, paginationOpts) {
|
|
14
|
+
const records = type ? ctx.db.query("media").withIndex("by_type", (q) => q.eq("type", type)) : ctx.db.query("media");
|
|
15
|
+
return records.order("desc").paginate(paginationOpts);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One page of the records whose title or filename matches `search` (the last word as a prefix), best match first,
|
|
19
|
+
* optionally of one kind. A blank `search` lists the library newest first, like `listMediaPage`. Records from before
|
|
20
|
+
* 1.4.0 match only once they have a `searchText` (`backfillSearchText`, or any write to them).
|
|
21
|
+
*/
|
|
22
|
+
export const searchMedia = query({
|
|
23
|
+
args: { search: v.string(), type: v.optional(mediaType), paginationOpts: paginationOptsValidator },
|
|
24
|
+
handler: async (ctx, args) => {
|
|
25
|
+
const { type, paginationOpts } = args;
|
|
26
|
+
const search = args.search.trim().toLowerCase().slice(0, SEARCH_MAX_LENGTH);
|
|
27
|
+
if (search === "")
|
|
28
|
+
return listNewestFirst(ctx, type, paginationOpts);
|
|
29
|
+
return ctx.db
|
|
30
|
+
.query("media")
|
|
31
|
+
.withSearchIndex("search_text", (q) => {
|
|
32
|
+
const matches = q.search("searchText", search);
|
|
33
|
+
return type ? matches.eq("type", type) : matches;
|
|
34
|
+
})
|
|
35
|
+
.paginate(paginationOpts);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
function batchSizeOf(requested) {
|
|
39
|
+
if (requested === undefined || !Number.isFinite(requested))
|
|
40
|
+
return BACKFILL_BATCH_SIZE.default;
|
|
41
|
+
return Math.min(Math.max(Math.floor(requested), 1), BACKFILL_BATCH_SIZE.max);
|
|
42
|
+
}
|
|
43
|
+
/** Fills one page of records, then schedules the next page until the table is done. */
|
|
44
|
+
async function backfillBatch(ctx, cursor, batchSize) {
|
|
45
|
+
const page = await ctx.db.query("media").paginate({ cursor, numItems: batchSize });
|
|
46
|
+
let updated = 0;
|
|
47
|
+
for (const record of page.page) {
|
|
48
|
+
const fill = missingSearchText(record);
|
|
49
|
+
if (fill.searchText === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
await ctx.db.patch(record._id, fill);
|
|
52
|
+
updated++;
|
|
53
|
+
}
|
|
54
|
+
if (!page.isDone) {
|
|
55
|
+
await ctx.scheduler.runAfter(0, internal.library.backfillSearchTextBatch, {
|
|
56
|
+
cursor: page.continueCursor,
|
|
57
|
+
batchSize,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return { updated, isDone: page.isDone };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Gives every record without a `searchText` (written before 1.4.0) its title and filename, `batchSize` records per
|
|
64
|
+
* mutation (default 100, at most 1000). Runs the first batch now and schedules the rest; safe to run again.
|
|
65
|
+
*/
|
|
66
|
+
export const backfillSearchText = mutation({
|
|
67
|
+
args: { batchSize: v.optional(v.number()) },
|
|
68
|
+
handler: async (ctx, args) => backfillBatch(ctx, null, batchSizeOf(args.batchSize)),
|
|
69
|
+
});
|
|
70
|
+
export const backfillSearchTextBatch = internalMutation({
|
|
71
|
+
args: { cursor: v.union(v.string(), v.null()), batchSize: v.number() },
|
|
72
|
+
handler: async (ctx, args) => backfillBatch(ctx, args.cursor, args.batchSize),
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=library.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"library.js","sourceRoot":"","sources":["../../src/component/library.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const MEDIA_META_LIMITS: {
|
|
2
|
+
readonly titleLength: 200;
|
|
3
|
+
readonly altLength: 1000;
|
|
4
|
+
readonly tagCount: 20;
|
|
5
|
+
readonly tagLength: 50;
|
|
6
|
+
/** Largest width, height or page count a client may report. */
|
|
7
|
+
readonly hintValue: 100000;
|
|
8
|
+
};
|
|
9
|
+
export interface UploadHints {
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
pageCount?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface MediaMetaInput {
|
|
15
|
+
title?: string;
|
|
16
|
+
alt?: string;
|
|
17
|
+
tags?: string[];
|
|
18
|
+
}
|
|
19
|
+
/** The fields to patch: a trimmed empty `title` or `alt` clears it (`undefined`), `tags: []` clears the tags. */
|
|
20
|
+
export interface MediaMetaPatch {
|
|
21
|
+
title?: string | undefined;
|
|
22
|
+
alt?: string | undefined;
|
|
23
|
+
tags?: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The hints worth recording: positive integers up to `hintValue`. The bytes are already stored, so a bad hint (a
|
|
27
|
+
* client's bug) is dropped and logged instead of failing the upload.
|
|
28
|
+
*/
|
|
29
|
+
export declare function validUploadHints(hints: UploadHints): UploadHints;
|
|
30
|
+
/** Trimmed, without empty ones or repeats (first spelling wins), within the count and length limits. */
|
|
31
|
+
export declare function normalizeTags(tags: string[]): string[];
|
|
32
|
+
/** The patch for an edit; only the fields the caller passed are in it. Throws `invalid_media_metadata`. */
|
|
33
|
+
export declare function mediaMetaPatch(input: MediaMetaInput): MediaMetaPatch;
|
|
34
|
+
/** What the media library's search matches: the title and the filename, lowercased. */
|
|
35
|
+
export declare function searchTextOf(title: string | undefined, filename: string): string;
|
|
36
|
+
interface SearchableRecord {
|
|
37
|
+
title?: string;
|
|
38
|
+
filename: string;
|
|
39
|
+
searchText?: string;
|
|
40
|
+
}
|
|
41
|
+
/** `{ searchText }` for a record written before 1.4.0 (it has none), else `{}`: spread it into any patch of a record. */
|
|
42
|
+
export declare function missingSearchText(record: SearchableRecord): {
|
|
43
|
+
searchText?: string;
|
|
44
|
+
};
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=mediaMeta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mediaMeta.d.ts","sourceRoot":"","sources":["../../src/component/mediaMeta.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,iBAAiB;;;;;IAK7B,+DAA+D;;CAEtD,CAAC;AAEX,MAAM,WAAW,WAAW;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,iHAAiH;AACjH,MAAM,WAAW,cAAc;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAUD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,WAAW,CAYhE;AAQD,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAgBtD;AAED,2GAA2G;AAC3G,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAMpE;AAED,uFAAuF;AACvF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED,UAAU,gBAAgB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,yHAAyH;AACzH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAGnF"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// A media record's descriptive metadata: what the app edits (`title`, `alt`, `tags`), the hints an uploading client
|
|
2
|
+
// sends (`width`, `height`, `pageCount`), and the `searchText` kept in step with the title and filename. Pure: no
|
|
3
|
+
// database, so it is checked the same way in every function that writes a record.
|
|
4
|
+
import { ConvexError } from "convex/values";
|
|
5
|
+
export const MEDIA_META_LIMITS = {
|
|
6
|
+
titleLength: 200,
|
|
7
|
+
altLength: 1000,
|
|
8
|
+
tagCount: 20,
|
|
9
|
+
tagLength: 50,
|
|
10
|
+
/** Largest width, height or page count a client may report. */
|
|
11
|
+
hintValue: 100_000,
|
|
12
|
+
};
|
|
13
|
+
function invalidMetadata(field, message) {
|
|
14
|
+
return new ConvexError({ code: "invalid_media_metadata", message, status: 400, details: { field } });
|
|
15
|
+
}
|
|
16
|
+
const HINT_FIELDS = ["width", "height", "pageCount"];
|
|
17
|
+
const isValidHint = (value) => Number.isInteger(value) && value > 0 && value <= MEDIA_META_LIMITS.hintValue;
|
|
18
|
+
/**
|
|
19
|
+
* The hints worth recording: positive integers up to `hintValue`. The bytes are already stored, so a bad hint (a
|
|
20
|
+
* client's bug) is dropped and logged instead of failing the upload.
|
|
21
|
+
*/
|
|
22
|
+
export function validUploadHints(hints) {
|
|
23
|
+
const valid = {};
|
|
24
|
+
for (const field of HINT_FIELDS) {
|
|
25
|
+
const value = hints[field];
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
if (isValidHint(value)) {
|
|
29
|
+
valid[field] = value;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
console.warn(`Devultur: dropped the upload hint ${field}=${value} (a whole number from 1 to 100,000)`);
|
|
33
|
+
}
|
|
34
|
+
return valid;
|
|
35
|
+
}
|
|
36
|
+
function trimmedText(field, value, maxLength) {
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (trimmed.length > maxLength)
|
|
39
|
+
throw invalidMetadata(field, `${field} must be at most ${maxLength} characters`);
|
|
40
|
+
return trimmed === "" ? undefined : trimmed;
|
|
41
|
+
}
|
|
42
|
+
/** Trimmed, without empty ones or repeats (first spelling wins), within the count and length limits. */
|
|
43
|
+
export function normalizeTags(tags) {
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const normalized = [];
|
|
46
|
+
for (const tag of tags) {
|
|
47
|
+
const trimmed = tag.trim();
|
|
48
|
+
if (trimmed === "" || seen.has(trimmed.toLowerCase()))
|
|
49
|
+
continue;
|
|
50
|
+
if (trimmed.length > MEDIA_META_LIMITS.tagLength) {
|
|
51
|
+
throw invalidMetadata("tags", `Each tag must be at most ${MEDIA_META_LIMITS.tagLength} characters`);
|
|
52
|
+
}
|
|
53
|
+
seen.add(trimmed.toLowerCase());
|
|
54
|
+
normalized.push(trimmed);
|
|
55
|
+
}
|
|
56
|
+
if (normalized.length > MEDIA_META_LIMITS.tagCount) {
|
|
57
|
+
throw invalidMetadata("tags", `A media can have at most ${MEDIA_META_LIMITS.tagCount} tags`);
|
|
58
|
+
}
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
/** The patch for an edit; only the fields the caller passed are in it. Throws `invalid_media_metadata`. */
|
|
62
|
+
export function mediaMetaPatch(input) {
|
|
63
|
+
const patch = {};
|
|
64
|
+
if (input.title !== undefined)
|
|
65
|
+
patch.title = trimmedText("title", input.title, MEDIA_META_LIMITS.titleLength);
|
|
66
|
+
if (input.alt !== undefined)
|
|
67
|
+
patch.alt = trimmedText("alt", input.alt, MEDIA_META_LIMITS.altLength);
|
|
68
|
+
if (input.tags !== undefined)
|
|
69
|
+
patch.tags = normalizeTags(input.tags);
|
|
70
|
+
return patch;
|
|
71
|
+
}
|
|
72
|
+
/** What the media library's search matches: the title and the filename, lowercased. */
|
|
73
|
+
export function searchTextOf(title, filename) {
|
|
74
|
+
return (title ? `${title} ${filename}` : filename).toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
/** `{ searchText }` for a record written before 1.4.0 (it has none), else `{}`: spread it into any patch of a record. */
|
|
77
|
+
export function missingSearchText(record) {
|
|
78
|
+
if (record.searchText !== undefined)
|
|
79
|
+
return {};
|
|
80
|
+
return { searchText: searchTextOf(record.title, record.filename) };
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=mediaMeta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mediaMeta.js","sourceRoot":"","sources":["../../src/component/mediaMeta.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets a record's `title`, `alt` and/or `tags` (only the ones passed). Trimmed; an empty title or alt clears it, and
|
|
3
|
+
* tags are deduplicated. Throws `invalid_media_metadata` past the limits (title 200, alt 1000, 20 tags of 50). Returns
|
|
4
|
+
* the updated record, or `null` when no record has this media id.
|
|
5
|
+
*/
|
|
6
|
+
export declare const updateMediaMeta: import("convex/server").RegisteredMutation<"public", {
|
|
7
|
+
title?: string | undefined;
|
|
8
|
+
alt?: string | undefined;
|
|
9
|
+
tags?: string[] | undefined;
|
|
10
|
+
key: string;
|
|
11
|
+
}, Promise<{
|
|
12
|
+
_id: import("convex/values").GenericId<"media">;
|
|
13
|
+
_creationTime: number;
|
|
14
|
+
width?: number | undefined;
|
|
15
|
+
height?: number | undefined;
|
|
16
|
+
visibility?: "public" | "private" | undefined;
|
|
17
|
+
objectKey?: string | undefined;
|
|
18
|
+
pageCount?: number | undefined;
|
|
19
|
+
title?: string | undefined;
|
|
20
|
+
alt?: string | undefined;
|
|
21
|
+
tags?: string[] | undefined;
|
|
22
|
+
searchText?: string | undefined;
|
|
23
|
+
transcodeStatus?: "queued" | "transcoding" | "captioning" | "ready" | "failed" | undefined;
|
|
24
|
+
transcodeProgress?: {
|
|
25
|
+
phase?: "captioning" | "downloading" | "encoding" | "uploading" | undefined;
|
|
26
|
+
current: number;
|
|
27
|
+
total: number;
|
|
28
|
+
} | undefined;
|
|
29
|
+
transcodeError?: string | undefined;
|
|
30
|
+
failureReason?: "invalid_input" | "unsupported_codec" | "timeout" | "capacity" | "internal" | undefined;
|
|
31
|
+
retriable?: boolean | undefined;
|
|
32
|
+
playlistUrl?: string | undefined;
|
|
33
|
+
thumbnailKey?: string | undefined;
|
|
34
|
+
captionLocales?: string[] | undefined;
|
|
35
|
+
captionsPending?: boolean | undefined;
|
|
36
|
+
duration?: number | undefined;
|
|
37
|
+
renditions?: {
|
|
38
|
+
width?: number | undefined;
|
|
39
|
+
height?: number | undefined;
|
|
40
|
+
variant: string;
|
|
41
|
+
kind: "hls" | "mp4";
|
|
42
|
+
rung: string;
|
|
43
|
+
key: string;
|
|
44
|
+
url: string;
|
|
45
|
+
}[] | undefined;
|
|
46
|
+
posterUrls?: {
|
|
47
|
+
jpg?: string | undefined;
|
|
48
|
+
avif?: string | undefined;
|
|
49
|
+
} | undefined;
|
|
50
|
+
hasAudio?: boolean | undefined;
|
|
51
|
+
storyboardKey?: string | undefined;
|
|
52
|
+
type: "video" | "image" | "file";
|
|
53
|
+
key: string;
|
|
54
|
+
filename: string;
|
|
55
|
+
contentType: string;
|
|
56
|
+
sizeBytes: number;
|
|
57
|
+
createdAt: number;
|
|
58
|
+
} | null>>;
|
|
59
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/component/metadata.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAoB1B,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Editing a media record's descriptive metadata (title, alt text, tags). The upload's own facts (size, dimensions,
|
|
2
|
+
// duration, page count) are recorded by `uploads.ts` and the transcode webhook, never edited here.
|
|
3
|
+
import { v } from "convex/values";
|
|
4
|
+
import { mutation } from "./_generated/server";
|
|
5
|
+
import { mediaMetaPatch, searchTextOf } from "./mediaMeta";
|
|
6
|
+
/**
|
|
7
|
+
* Sets a record's `title`, `alt` and/or `tags` (only the ones passed). Trimmed; an empty title or alt clears it, and
|
|
8
|
+
* tags are deduplicated. Throws `invalid_media_metadata` past the limits (title 200, alt 1000, 20 tags of 50). Returns
|
|
9
|
+
* the updated record, or `null` when no record has this media id.
|
|
10
|
+
*/
|
|
11
|
+
export const updateMediaMeta = mutation({
|
|
12
|
+
args: {
|
|
13
|
+
key: v.string(),
|
|
14
|
+
title: v.optional(v.string()),
|
|
15
|
+
alt: v.optional(v.string()),
|
|
16
|
+
tags: v.optional(v.array(v.string())),
|
|
17
|
+
},
|
|
18
|
+
handler: async (ctx, args) => {
|
|
19
|
+
const { key, ...input } = args;
|
|
20
|
+
const patch = mediaMetaPatch(input);
|
|
21
|
+
const record = await ctx.db
|
|
22
|
+
.query("media")
|
|
23
|
+
.withIndex("by_key", (q) => q.eq("key", key))
|
|
24
|
+
.first();
|
|
25
|
+
if (!record)
|
|
26
|
+
return null;
|
|
27
|
+
const title = "title" in patch ? patch.title : record.title;
|
|
28
|
+
await ctx.db.patch(record._id, { ...patch, searchText: searchTextOf(title, record.filename) });
|
|
29
|
+
return ctx.db.get(record._id);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../src/component/metadata.ts"],"names":[],"mappings":"AAAA,mHAAmH;AACnH,mGAAmG;AACnG,OAAO,EAAE,CAAC,EAAE,MAAM,eAAe,CAAC;AAElC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3D;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,QAAQ,CAAC;IACvC,IAAI,EAAE;QACL,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;QACf,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7B,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC3B,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;KACrC;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAgC,EAAE;QAC1D,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC;QAC/B,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,EAAE;aACzB,KAAK,CAAC,OAAO,CAAC;aACd,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;aAC5C,KAAK,EAAE,CAAC;QACV,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAEzB,MAAM,KAAK,GAAG,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAC5D,MAAM,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC/F,OAAO,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;CACD,CAAC,CAAC"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export declare const issueViewerToken: import("convex/server").RegisteredAction<"public", {
|
|
2
|
+
userId?: string | undefined;
|
|
2
3
|
sessionSecret?: string | undefined;
|
|
3
4
|
}, Promise<{
|
|
4
5
|
token: string;
|
|
5
6
|
expiresIn: number;
|
|
6
7
|
}>>;
|
|
7
8
|
export declare const createUploadUrl: import("convex/server").RegisteredAction<"public", {
|
|
8
|
-
hold?: boolean | undefined;
|
|
9
9
|
visibility?: "public" | "private" | undefined;
|
|
10
10
|
lock?: {
|
|
11
11
|
retention?: {
|
|
@@ -16,6 +16,7 @@ export declare const createUploadUrl: import("convex/server").RegisteredAction<"
|
|
|
16
16
|
} | undefined;
|
|
17
17
|
hold?: boolean | undefined;
|
|
18
18
|
} | undefined;
|
|
19
|
+
hold?: boolean | undefined;
|
|
19
20
|
filename: string;
|
|
20
21
|
contentType: string;
|
|
21
22
|
}, Promise<import("@devultur/core").WorkerUploadResult>>;
|
|
@@ -76,6 +77,11 @@ export declare const getMediaByKey: import("convex/server").RegisteredQuery<"pub
|
|
|
76
77
|
height?: number | undefined;
|
|
77
78
|
visibility?: "public" | "private" | undefined;
|
|
78
79
|
objectKey?: string | undefined;
|
|
80
|
+
pageCount?: number | undefined;
|
|
81
|
+
title?: string | undefined;
|
|
82
|
+
alt?: string | undefined;
|
|
83
|
+
tags?: string[] | undefined;
|
|
84
|
+
searchText?: string | undefined;
|
|
79
85
|
transcodeStatus?: "queued" | "transcoding" | "captioning" | "ready" | "failed" | undefined;
|
|
80
86
|
transcodeProgress?: {
|
|
81
87
|
phase?: "captioning" | "downloading" | "encoding" | "uploading" | undefined;
|
|
@@ -121,6 +127,11 @@ export declare const listMedia: import("convex/server").RegisteredQuery<"public"
|
|
|
121
127
|
height?: number | undefined;
|
|
122
128
|
visibility?: "public" | "private" | undefined;
|
|
123
129
|
objectKey?: string | undefined;
|
|
130
|
+
pageCount?: number | undefined;
|
|
131
|
+
title?: string | undefined;
|
|
132
|
+
alt?: string | undefined;
|
|
133
|
+
tags?: string[] | undefined;
|
|
134
|
+
searchText?: string | undefined;
|
|
124
135
|
transcodeStatus?: "queued" | "transcoding" | "captioning" | "ready" | "failed" | undefined;
|
|
125
136
|
transcodeProgress?: {
|
|
126
137
|
phase?: "captioning" | "downloading" | "encoding" | "uploading" | undefined;
|
|
@@ -175,6 +186,11 @@ export declare const listMediaPage: import("convex/server").RegisteredQuery<"pub
|
|
|
175
186
|
height?: number | undefined;
|
|
176
187
|
visibility?: "public" | "private" | undefined;
|
|
177
188
|
objectKey?: string | undefined;
|
|
189
|
+
pageCount?: number | undefined;
|
|
190
|
+
title?: string | undefined;
|
|
191
|
+
alt?: string | undefined;
|
|
192
|
+
tags?: string[] | undefined;
|
|
193
|
+
searchText?: string | undefined;
|
|
178
194
|
transcodeStatus?: "queued" | "transcoding" | "captioning" | "ready" | "failed" | undefined;
|
|
179
195
|
transcodeProgress?: {
|
|
180
196
|
phase?: "captioning" | "downloading" | "encoding" | "uploading" | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/component/public.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/component/public.ts"],"names":[],"mappings":"AAqCA,eAAO,MAAM,gBAAgB;;;;;;GAY3B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;wDAoB1B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;iBAQ5B,CAAC;AAEH,eAAO,MAAM,WAAW;;;uDAUtB,CAAC;AAKH,eAAO,MAAM,mBAAmB;;iBAe9B,CAAC;AAEH,eAAO,MAAM,YAAY;;;;;wDAyEvB,CAAC;AAIH,eAAO,MAAM,YAAY;;iBAgBvB,CAAC;AAIH,eAAO,MAAM,sBAAsB;;iBAkBjC,CAAC;AAIH,eAAO,MAAM,mBAAmB;;iBAmB9B,CAAC;AAIH,eAAO,MAAM,WAAW;;;;;;uDAqBtB,CAAC;AAEH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;iBAuCtB,CAAC;AAIH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAQxB,CAAC;AAGH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KASpB,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAMxB,CAAC;AAIH,eAAO,MAAM,aAAa;;;;;0DAmBxB,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;KAQ9B,CAAC;AAWH,eAAO,MAAM,YAAY;;;;0DAgBvB,CAAC;AAEH,eAAO,MAAM,YAAY;;;;;;;;;;;;UAKvB,CAAC;AAEH,eAAO,MAAM,YAAY;;;0DAmBvB,CAAC;AAEH,eAAO,MAAM,aAAa;;;iBAQxB,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;aAK5B,CAAC;AAIH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAgG7B,CAAC"}
|
package/dist/component/public.js
CHANGED
|
@@ -3,8 +3,10 @@ import { paginationOptsValidator } from "convex/server";
|
|
|
3
3
|
import { v } from "convex/values";
|
|
4
4
|
import { api, internal } from "./_generated/api";
|
|
5
5
|
import { internalMutation, mutation, query } from "./_generated/server";
|
|
6
|
+
import { listNewestFirst } from "./library";
|
|
6
7
|
import { objectLockValidator } from "./lockValidators";
|
|
7
8
|
import { getMediaClient } from "./mediaClient";
|
|
9
|
+
import { missingSearchText, searchTextOf } from "./mediaMeta";
|
|
8
10
|
import { transcodeOutputFields } from "./schema";
|
|
9
11
|
import { UPLOAD_CONFIRM_BACKSTOP_MS } from "./uploads";
|
|
10
12
|
import { workerAction } from "./workerErrors";
|
|
@@ -22,10 +24,12 @@ const STUCK_TIMEOUT_MS = 45 * 60 * 1000;
|
|
|
22
24
|
const failureReasonValidator = v.union(v.literal("invalid_input"), v.literal("unsupported_codec"), v.literal("timeout"), v.literal("capacity"), v.literal("internal"));
|
|
23
25
|
// --- Media Actions (call CF Worker) ---
|
|
24
26
|
export const issueViewerToken = workerAction({
|
|
25
|
-
|
|
27
|
+
// `userId`: the token's subject. Your server sets it (a guard's `userId`, see `defineDevulturFunctions`), never a
|
|
28
|
+
// browser: the exposed `issueViewerToken` only takes `sessionSecret`. Omitted → the shared `viewer` subject.
|
|
29
|
+
args: { sessionSecret: v.optional(v.string()), userId: v.optional(v.string()) },
|
|
26
30
|
handler: async (_ctx, args) => {
|
|
27
31
|
const media = getMediaClient();
|
|
28
|
-
const { token, expiresIn } = await media.issueToken("viewer", {
|
|
32
|
+
const { token, expiresIn } = await media.issueToken(args.userId ?? "viewer", {
|
|
29
33
|
expiresIn: DEFAULT_TOKEN_EXPIRY_SECONDS,
|
|
30
34
|
sessionSecret: args.sessionSecret,
|
|
31
35
|
});
|
|
@@ -173,6 +177,7 @@ export const skipCaptions = internalMutation({
|
|
|
173
177
|
return;
|
|
174
178
|
const isWaitingForCaptions = record.transcodeStatus === MediaStatus.CAPTIONING;
|
|
175
179
|
await ctx.db.patch(record._id, {
|
|
180
|
+
...missingSearchText(record),
|
|
176
181
|
captionsPending: false,
|
|
177
182
|
...(isWaitingForCaptions ? { transcodeStatus: MediaStatus.READY } : {}),
|
|
178
183
|
});
|
|
@@ -190,6 +195,7 @@ export const checkTranscodeAccepted = internalMutation({
|
|
|
190
195
|
if (!record || record.transcodeStatus !== MediaStatus.QUEUED)
|
|
191
196
|
return;
|
|
192
197
|
await ctx.db.patch(record._id, {
|
|
198
|
+
...missingSearchText(record),
|
|
193
199
|
transcodeStatus: MediaStatus.FAILED,
|
|
194
200
|
transcodeError: "The transcode didn't start in time. Retry.",
|
|
195
201
|
failureReason: "capacity",
|
|
@@ -212,6 +218,7 @@ export const checkTranscodeStuck = internalMutation({
|
|
|
212
218
|
if (record.transcodeStatus === MediaStatus.READY || record.transcodeStatus === MediaStatus.FAILED)
|
|
213
219
|
return;
|
|
214
220
|
await ctx.db.patch(record._id, {
|
|
221
|
+
...missingSearchText(record),
|
|
215
222
|
transcodeStatus: MediaStatus.FAILED,
|
|
216
223
|
transcodeError: "The transcode didn't finish in time. Retry.",
|
|
217
224
|
failureReason: "timeout",
|
|
@@ -237,7 +244,11 @@ export const createMedia = mutation({
|
|
|
237
244
|
.first();
|
|
238
245
|
if (existing)
|
|
239
246
|
return existing._id;
|
|
240
|
-
return ctx.db.insert("media", {
|
|
247
|
+
return ctx.db.insert("media", {
|
|
248
|
+
...args,
|
|
249
|
+
searchText: searchTextOf(undefined, args.filename),
|
|
250
|
+
createdAt: Date.now(),
|
|
251
|
+
});
|
|
241
252
|
},
|
|
242
253
|
});
|
|
243
254
|
export const updateMedia = mutation({
|
|
@@ -262,7 +273,8 @@ export const updateMedia = mutation({
|
|
|
262
273
|
if (!record)
|
|
263
274
|
return;
|
|
264
275
|
const { key: _mediaKey, ...fields } = args;
|
|
265
|
-
|
|
276
|
+
// Any write to a record from before 1.4.0 also gives it its `searchText`.
|
|
277
|
+
const patch = { ...missingSearchText(record) };
|
|
266
278
|
for (const [fieldName, fieldValue] of Object.entries(fields)) {
|
|
267
279
|
if (fieldValue !== undefined)
|
|
268
280
|
patch[fieldName] = fieldValue;
|
|
@@ -299,13 +311,7 @@ export const listMediaPage = query({
|
|
|
299
311
|
type: v.optional(v.union(v.literal("video"), v.literal("image"), v.literal("file"))),
|
|
300
312
|
paginationOpts: paginationOptsValidator,
|
|
301
313
|
},
|
|
302
|
-
handler: async (ctx, args) =>
|
|
303
|
-
const { type } = args;
|
|
304
|
-
const records = type
|
|
305
|
-
? ctx.db.query("media").withIndex("by_type", (q) => q.eq("type", type))
|
|
306
|
-
: ctx.db.query("media");
|
|
307
|
-
return records.order("desc").paginate(args.paginationOpts);
|
|
308
|
-
},
|
|
314
|
+
handler: async (ctx, args) => listNewestFirst(ctx, args.type, args.paginationOpts),
|
|
309
315
|
});
|
|
310
316
|
// --- Captions ---
|
|
311
317
|
export const upsertCaption = mutation({
|
|
@@ -430,6 +436,9 @@ export const handleWebhookEvent = internalMutation({
|
|
|
430
436
|
}
|
|
431
437
|
if (!record)
|
|
432
438
|
return;
|
|
439
|
+
const searchTextFill = missingSearchText(record);
|
|
440
|
+
if (searchTextFill.searchText !== undefined)
|
|
441
|
+
await ctx.db.patch(record._id, searchTextFill);
|
|
433
442
|
switch (args.event) {
|
|
434
443
|
case MediaEventName.TRANSCODE_STARTED:
|
|
435
444
|
await ctx.db.patch(record._id, {
|