@btst/stack 2.8.1 → 2.9.1
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 +3 -2
- package/dist/components/markdown/index.d.cts +15 -2
- package/dist/components/markdown/index.d.mts +15 -2
- package/dist/components/markdown/index.d.ts +15 -2
- package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.cjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/forms/image-field.mjs +30 -1
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.cjs +49 -9
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.mjs +50 -10
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.cjs +77 -9
- package/dist/packages/stack/src/plugins/blog/client/components/forms/markdown-editor.mjs +77 -9
- package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -5
- package/dist/packages/stack/src/plugins/cms/client/components/forms/content-form.mjs +24 -5
- package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.cjs +47 -13
- package/dist/packages/stack/src/plugins/cms/client/components/forms/file-upload.mjs +47 -13
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.cjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/board-form.mjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +6 -2
- package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +6 -2
- package/dist/packages/stack/src/plugins/media/api/adapters/local.cjs +55 -0
- package/dist/packages/stack/src/plugins/media/api/adapters/local.mjs +37 -0
- package/dist/packages/stack/src/plugins/media/api/getters.cjs +83 -0
- package/dist/packages/stack/src/plugins/media/api/getters.mjs +78 -0
- package/dist/packages/stack/src/plugins/media/api/mutations.cjs +88 -0
- package/dist/packages/stack/src/plugins/media/api/mutations.mjs +82 -0
- package/dist/packages/stack/src/plugins/media/api/plugin.cjs +525 -0
- package/dist/packages/stack/src/plugins/media/api/plugin.mjs +523 -0
- package/dist/packages/stack/src/plugins/media/api/query-key-defs.cjs +19 -0
- package/dist/packages/stack/src/plugins/media/api/query-key-defs.mjs +16 -0
- package/dist/packages/stack/src/plugins/media/api/serializers.cjs +17 -0
- package/dist/packages/stack/src/plugins/media/api/serializers.mjs +14 -0
- package/dist/packages/stack/src/plugins/media/api/storage-adapter.cjs +15 -0
- package/dist/packages/stack/src/plugins/media/api/storage-adapter.mjs +11 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.cjs +129 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-card.mjs +127 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.cjs +58 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/asset-preview-button.mjs +56 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.cjs +94 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/browse-tab.mjs +92 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.cjs +171 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/folder-tree.mjs +168 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.cjs +308 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/index.mjs +305 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.cjs +104 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/upload-tab.mjs +102 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.cjs +70 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/url-tab.mjs +68 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.cjs +21 -0
- package/dist/packages/stack/src/plugins/media/client/components/media-picker/utils.mjs +17 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.cjs +35 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.cjs +125 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.internal.mjs +123 -0
- package/dist/packages/stack/src/plugins/media/client/components/pages/library-page.mjs +33 -0
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.cjs +222 -0
- package/dist/packages/stack/src/plugins/media/client/hooks/use-media.mjs +214 -0
- package/dist/packages/stack/src/plugins/media/client/plugin.cjs +94 -0
- package/dist/packages/stack/src/plugins/media/client/plugin.mjs +92 -0
- package/dist/packages/stack/src/plugins/media/client/upload.cjs +121 -0
- package/dist/packages/stack/src/plugins/media/client/upload.mjs +119 -0
- package/dist/packages/stack/src/plugins/media/client/utils/image-compression.cjs +67 -0
- package/dist/packages/stack/src/plugins/media/client/utils/image-compression.mjs +65 -0
- package/dist/packages/stack/src/plugins/media/db.cjs +62 -0
- package/dist/packages/stack/src/plugins/media/db.mjs +60 -0
- package/dist/packages/stack/src/plugins/media/schemas.cjs +41 -0
- package/dist/packages/stack/src/plugins/media/schemas.mjs +35 -0
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.cjs +18 -1
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-block.mjs +19 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.cjs +2 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/image/image-edit-dialog.mjs +2 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.cjs +3 -2
- package/dist/packages/ui/src/components/minimal-tiptap/components/section/five.mjs +3 -2
- package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.cjs +12 -5
- package/dist/packages/ui/src/components/minimal-tiptap/minimal-tiptap.mjs +12 -5
- package/dist/plugins/blog/client/index.d.cts +58 -1
- package/dist/plugins/blog/client/index.d.mts +58 -1
- package/dist/plugins/blog/client/index.d.ts +58 -1
- package/dist/plugins/cms/client/index.d.cts +73 -3
- package/dist/plugins/cms/client/index.d.mts +73 -3
- package/dist/plugins/cms/client/index.d.ts +73 -3
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
- package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
- package/dist/plugins/kanban/client/index.d.cts +1 -1
- package/dist/plugins/kanban/client/index.d.mts +1 -1
- package/dist/plugins/kanban/client/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/plugins/media/api/adapters/s3.cjs +106 -0
- package/dist/plugins/media/api/adapters/s3.d.cts +60 -0
- package/dist/plugins/media/api/adapters/s3.d.mts +60 -0
- package/dist/plugins/media/api/adapters/s3.d.ts +60 -0
- package/dist/plugins/media/api/adapters/s3.mjs +104 -0
- package/dist/plugins/media/api/adapters/vercel-blob.cjs +53 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.cts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.mts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.d.ts +41 -0
- package/dist/plugins/media/api/adapters/vercel-blob.mjs +51 -0
- package/dist/plugins/media/api/index.cjs +26 -0
- package/dist/plugins/media/api/index.d.cts +116 -0
- package/dist/plugins/media/api/index.d.mts +116 -0
- package/dist/plugins/media/api/index.d.ts +116 -0
- package/dist/plugins/media/api/index.mjs +6 -0
- package/dist/plugins/media/client/components/index.cjs +10 -0
- package/dist/plugins/media/client/components/index.d.cts +55 -0
- package/dist/plugins/media/client/components/index.d.mts +55 -0
- package/dist/plugins/media/client/components/index.d.ts +55 -0
- package/dist/plugins/media/client/components/index.mjs +2 -0
- package/dist/plugins/media/client/hooks/index.cjs +13 -0
- package/dist/plugins/media/client/hooks/index.d.cts +53 -0
- package/dist/plugins/media/client/hooks/index.d.mts +53 -0
- package/dist/plugins/media/client/hooks/index.d.ts +53 -0
- package/dist/plugins/media/client/hooks/index.mjs +1 -0
- package/dist/plugins/media/client/index.cjs +9 -0
- package/dist/plugins/media/client/index.d.cts +242 -0
- package/dist/plugins/media/client/index.d.mts +242 -0
- package/dist/plugins/media/client/index.d.ts +242 -0
- package/dist/plugins/media/client/index.mjs +2 -0
- package/dist/plugins/media/client.css +1 -0
- package/dist/plugins/media/query-keys.cjs +72 -0
- package/dist/plugins/media/query-keys.d.cts +49 -0
- package/dist/plugins/media/query-keys.d.mts +49 -0
- package/dist/plugins/media/query-keys.d.ts +49 -0
- package/dist/plugins/media/query-keys.mjs +70 -0
- package/dist/plugins/media/style.css +1 -0
- package/dist/shared/{stack.DRpeDS6X.d.ts → stack.BMx2QYOK.d.ts} +25 -0
- package/dist/shared/stack.BttDsJJn.d.cts +109 -0
- package/dist/shared/stack.BttDsJJn.d.mts +109 -0
- package/dist/shared/stack.BttDsJJn.d.ts +109 -0
- package/dist/shared/stack.C7vfOBmO.d.mts +63 -0
- package/dist/shared/stack.CAni8dnD.d.cts +63 -0
- package/dist/shared/stack.CI8iRKKi.d.cts +286 -0
- package/dist/shared/stack.CLcnSF_b.d.cts +25 -0
- package/dist/shared/stack.CLcnSF_b.d.mts +25 -0
- package/dist/shared/stack.CLcnSF_b.d.ts +25 -0
- package/dist/shared/stack.CYSwntXC.d.ts +63 -0
- package/dist/shared/{stack.Jb0kQDJC.d.mts → stack.Cd6McBu1.d.mts} +25 -0
- package/dist/shared/stack.DJDjdG64.d.ts +286 -0
- package/dist/shared/{stack.BxFl46lB.d.cts → stack.DxQl8Wa1.d.cts} +25 -0
- package/dist/shared/stack.FgBVDSPi.d.mts +286 -0
- package/package.json +113 -4
- package/src/plugins/blog/client/components/forms/image-field.tsx +35 -4
- package/src/plugins/blog/client/components/forms/markdown-editor-with-overrides.tsx +67 -12
- package/src/plugins/blog/client/components/forms/markdown-editor.tsx +106 -10
- package/src/plugins/blog/client/overrides.ts +58 -1
- package/src/plugins/cms/client/components/forms/content-form.tsx +26 -7
- package/src/plugins/cms/client/components/forms/file-upload.tsx +73 -15
- package/src/plugins/cms/client/overrides.ts +57 -2
- package/src/plugins/kanban/client/components/forms/board-form.tsx +1 -1
- package/src/plugins/kanban/client/components/forms/task-form.tsx +7 -1
- package/src/plugins/kanban/client/overrides.ts +25 -0
- package/src/plugins/media/__tests__/__stubs__/vercel-blob-server.ts +9 -0
- package/src/plugins/media/__tests__/getters.test.ts +274 -0
- package/src/plugins/media/__tests__/mutations.test.ts +299 -0
- package/src/plugins/media/__tests__/plugin.test.ts +752 -0
- package/src/plugins/media/__tests__/query-key-defs.test.ts +54 -0
- package/src/plugins/media/__tests__/storage-adapters.test.ts +351 -0
- package/src/plugins/media/api/adapters/local.ts +79 -0
- package/src/plugins/media/api/adapters/s3.ts +198 -0
- package/src/plugins/media/api/adapters/vercel-blob.ts +131 -0
- package/src/plugins/media/api/getters.ts +174 -0
- package/src/plugins/media/api/index.ts +41 -0
- package/src/plugins/media/api/mutations.ts +179 -0
- package/src/plugins/media/api/plugin.ts +855 -0
- package/src/plugins/media/api/query-key-defs.ts +41 -0
- package/src/plugins/media/api/serializers.ts +28 -0
- package/src/plugins/media/api/storage-adapter.ts +139 -0
- package/src/plugins/media/client/components/index.tsx +6 -0
- package/src/plugins/media/client/components/media-picker/asset-card.tsx +150 -0
- package/src/plugins/media/client/components/media-picker/asset-preview-button.tsx +67 -0
- package/src/plugins/media/client/components/media-picker/browse-tab.tsx +116 -0
- package/src/plugins/media/client/components/media-picker/folder-tree.tsx +188 -0
- package/src/plugins/media/client/components/media-picker/index.tsx +347 -0
- package/src/plugins/media/client/components/media-picker/upload-tab.tsx +108 -0
- package/src/plugins/media/client/components/media-picker/url-tab.tsx +72 -0
- package/src/plugins/media/client/components/media-picker/utils.ts +17 -0
- package/src/plugins/media/client/components/pages/library-page.internal.tsx +134 -0
- package/src/plugins/media/client/components/pages/library-page.tsx +42 -0
- package/src/plugins/media/client/hooks/index.tsx +9 -0
- package/src/plugins/media/client/hooks/use-media.tsx +289 -0
- package/src/plugins/media/client/index.ts +4 -0
- package/src/plugins/media/client/overrides.ts +127 -0
- package/src/plugins/media/client/plugin.tsx +184 -0
- package/src/plugins/media/client/upload.ts +171 -0
- package/src/plugins/media/client/utils/image-compression.ts +131 -0
- package/src/plugins/media/client.css +1 -0
- package/src/plugins/media/db.ts +62 -0
- package/src/plugins/media/query-keys.ts +96 -0
- package/src/plugins/media/schemas.ts +37 -0
- package/src/plugins/media/style.css +1 -0
- package/src/plugins/media/types.ts +26 -0
- package/dist/shared/{stack.BOokfhZD.d.cts → stack.B6S3cgwN.d.cts} +16 -16
- package/dist/shared/{stack.CWxAl9K3.d.mts → stack.Bzfx-_lq.d.mts} +16 -16
- package/dist/shared/{stack.BvCR4-9H.d.ts → stack.j5SFLC1d.d.ts} +16 -16
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
VercelBlobStorageAdapter,
|
|
3
|
+
VercelBlobHandlerCallbacks,
|
|
4
|
+
} from "../storage-adapter";
|
|
5
|
+
|
|
6
|
+
export interface VercelBlobStorageAdapterOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The `BLOB_READ_WRITE_TOKEN` environment variable is read automatically
|
|
9
|
+
* by `@vercel/blob`. You only need to provide this option if you store
|
|
10
|
+
* the token under a different name.
|
|
11
|
+
*/
|
|
12
|
+
token?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal subset of the `@vercel/blob/client` `handleUpload` options.
|
|
17
|
+
* Defined inline so we do not hard-depend on a specific `@vercel/blob` release.
|
|
18
|
+
*/
|
|
19
|
+
interface HandleUploadOptions {
|
|
20
|
+
body: unknown;
|
|
21
|
+
request: Request;
|
|
22
|
+
token?: string;
|
|
23
|
+
onBeforeGenerateToken: (
|
|
24
|
+
pathname: string,
|
|
25
|
+
clientPayload?: string | null,
|
|
26
|
+
) => Promise<{
|
|
27
|
+
addRandomSuffix?: boolean;
|
|
28
|
+
allowedContentTypes?: string[];
|
|
29
|
+
maximumSizeInBytes?: number;
|
|
30
|
+
}>;
|
|
31
|
+
onUploadCompleted: (args: {
|
|
32
|
+
blob: { url: string; pathname: string };
|
|
33
|
+
tokenPayload?: string | null;
|
|
34
|
+
}) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type HandleUploadFn = (options: HandleUploadOptions) => Promise<unknown>;
|
|
38
|
+
type DelFn = (url: string, options?: { token?: string }) => Promise<void>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a Vercel Blob storage adapter using the signed direct-upload protocol.
|
|
42
|
+
* The server never receives file bytes — it only issues short-lived client tokens
|
|
43
|
+
* via `@vercel/blob`'s `handleUpload` helper (available via `@vercel/blob/client`
|
|
44
|
+
* in compatible versions).
|
|
45
|
+
*
|
|
46
|
+
* @remarks Requires `@vercel/blob` as an optional peer dependency (version
|
|
47
|
+
* with `handleUpload` exported from `@vercel/blob/client`).
|
|
48
|
+
*
|
|
49
|
+
* Upload flow:
|
|
50
|
+
* 1. Client calls `POST /media/upload/vercel-blob` to obtain a client token.
|
|
51
|
+
* 2. Client uses `@vercel/blob/client`'s `upload()` to upload directly to Vercel.
|
|
52
|
+
* 3. After upload, client calls `POST /media/assets` to save metadata to the DB.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* mediaBackendPlugin({
|
|
57
|
+
* storageAdapter: vercelBlobAdapter(),
|
|
58
|
+
* hooks: {
|
|
59
|
+
* onBeforeUpload: async (_meta, ctx) => {
|
|
60
|
+
* const session = await getSession(ctx.headers);
|
|
61
|
+
* if (!session) throw new Error("Unauthorized");
|
|
62
|
+
* },
|
|
63
|
+
* },
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export function vercelBlobAdapter(
|
|
68
|
+
options: VercelBlobStorageAdapterOptions = {},
|
|
69
|
+
): VercelBlobStorageAdapter {
|
|
70
|
+
return {
|
|
71
|
+
type: "vercel-blob" as const,
|
|
72
|
+
urlHostnameSuffix: ".public.blob.vercel-storage.com",
|
|
73
|
+
|
|
74
|
+
async handleRequest(
|
|
75
|
+
request: Request,
|
|
76
|
+
callbacks: VercelBlobHandlerCallbacks,
|
|
77
|
+
): Promise<unknown> {
|
|
78
|
+
let handleUpload: HandleUploadFn;
|
|
79
|
+
try {
|
|
80
|
+
const vercelBlobClient =
|
|
81
|
+
/* @vite-ignore */
|
|
82
|
+
(await import("@vercel/blob/client")) as {
|
|
83
|
+
handleUpload: HandleUploadFn;
|
|
84
|
+
};
|
|
85
|
+
({ handleUpload } = vercelBlobClient);
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"[@btst/stack] Vercel Blob adapter requires '@vercel/blob' with " +
|
|
89
|
+
"'handleUpload' exported from '@vercel/blob/client'. " +
|
|
90
|
+
"Run: npm install @vercel/blob",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const body = await request.json();
|
|
95
|
+
|
|
96
|
+
return handleUpload({
|
|
97
|
+
body,
|
|
98
|
+
request,
|
|
99
|
+
token: options.token,
|
|
100
|
+
onBeforeGenerateToken: async (pathname, clientPayload) => {
|
|
101
|
+
const tokenOptions =
|
|
102
|
+
(await callbacks.onBeforeGenerateToken?.(
|
|
103
|
+
pathname,
|
|
104
|
+
clientPayload ?? null,
|
|
105
|
+
)) ?? {};
|
|
106
|
+
return {
|
|
107
|
+
addRandomSuffix: true,
|
|
108
|
+
...tokenOptions,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
onUploadCompleted: async () => {
|
|
112
|
+
// DB record is created by the client calling POST /media/assets
|
|
113
|
+
// after the upload completes. Nothing to do server-side here.
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async delete(url: string): Promise<void> {
|
|
119
|
+
let del: DelFn;
|
|
120
|
+
try {
|
|
121
|
+
({ del } = (await import("@vercel/blob")) as { del: DelFn });
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error(
|
|
124
|
+
"[@btst/stack] Vercel Blob adapter requires '@vercel/blob'. " +
|
|
125
|
+
"Run: npm install @vercel/blob",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
await del(url, options.token ? { token: options.token } : undefined);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { DBAdapter as Adapter } from "@btst/db";
|
|
2
|
+
import type { Asset, Folder } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parameters for filtering and paginating the asset list.
|
|
6
|
+
*/
|
|
7
|
+
export interface AssetListParams {
|
|
8
|
+
folderId?: string;
|
|
9
|
+
mimeType?: string;
|
|
10
|
+
query?: string;
|
|
11
|
+
offset?: number;
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Paginated result returned by {@link listAssets}.
|
|
17
|
+
*/
|
|
18
|
+
export interface AssetListResult {
|
|
19
|
+
items: Asset[];
|
|
20
|
+
total: number;
|
|
21
|
+
limit?: number;
|
|
22
|
+
offset?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parameters for filtering the folder list.
|
|
27
|
+
*/
|
|
28
|
+
export interface FolderListParams {
|
|
29
|
+
parentId?: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Retrieve all assets matching optional filter criteria.
|
|
34
|
+
* Pure DB function — no hooks, no HTTP context. Safe for server-side use.
|
|
35
|
+
*
|
|
36
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
37
|
+
* responsible for any access-control checks before invoking this function.
|
|
38
|
+
*/
|
|
39
|
+
export async function listAssets(
|
|
40
|
+
adapter: Adapter,
|
|
41
|
+
params?: AssetListParams,
|
|
42
|
+
): Promise<AssetListResult> {
|
|
43
|
+
const query = params ?? {};
|
|
44
|
+
|
|
45
|
+
const whereConditions: Array<{
|
|
46
|
+
field: string;
|
|
47
|
+
value: string | number | boolean | string[] | number[] | Date | null;
|
|
48
|
+
operator: "eq" | "in";
|
|
49
|
+
}> = [];
|
|
50
|
+
|
|
51
|
+
if (query.folderId !== undefined) {
|
|
52
|
+
whereConditions.push({
|
|
53
|
+
field: "folderId",
|
|
54
|
+
value: query.folderId,
|
|
55
|
+
operator: "eq" as const,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (query.mimeType) {
|
|
60
|
+
whereConditions.push({
|
|
61
|
+
field: "mimeType",
|
|
62
|
+
value: query.mimeType,
|
|
63
|
+
operator: "eq" as const,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const needsInMemoryFilter = !!query.query;
|
|
68
|
+
const dbWhere = whereConditions.length > 0 ? whereConditions : undefined;
|
|
69
|
+
|
|
70
|
+
const dbTotal: number | undefined = !needsInMemoryFilter
|
|
71
|
+
? await adapter.count({ model: "mediaAsset", where: dbWhere })
|
|
72
|
+
: undefined;
|
|
73
|
+
|
|
74
|
+
let assets = await adapter.findMany<Asset>({
|
|
75
|
+
model: "mediaAsset",
|
|
76
|
+
limit: !needsInMemoryFilter ? query.limit : undefined,
|
|
77
|
+
offset: !needsInMemoryFilter ? query.offset : undefined,
|
|
78
|
+
where: dbWhere,
|
|
79
|
+
sortBy: { field: "createdAt", direction: "desc" },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (query.query) {
|
|
83
|
+
const searchLower = query.query.toLowerCase();
|
|
84
|
+
assets = assets.filter(
|
|
85
|
+
(asset) =>
|
|
86
|
+
asset.filename.toLowerCase().includes(searchLower) ||
|
|
87
|
+
asset.originalName.toLowerCase().includes(searchLower) ||
|
|
88
|
+
asset.alt?.toLowerCase().includes(searchLower),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (needsInMemoryFilter) {
|
|
93
|
+
const total = assets.length;
|
|
94
|
+
const offset = query.offset ?? 0;
|
|
95
|
+
const limit = query.limit;
|
|
96
|
+
assets = assets.slice(
|
|
97
|
+
offset,
|
|
98
|
+
limit !== undefined ? offset + limit : undefined,
|
|
99
|
+
);
|
|
100
|
+
return { items: assets, total, limit: query.limit, offset: query.offset };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
items: assets,
|
|
105
|
+
total: dbTotal ?? assets.length,
|
|
106
|
+
limit: query.limit,
|
|
107
|
+
offset: query.offset,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Retrieve a single asset by its ID.
|
|
113
|
+
* Returns `null` if no asset is found.
|
|
114
|
+
* Pure DB function — no hooks, no HTTP context.
|
|
115
|
+
*
|
|
116
|
+
* @remarks **Security:** Authorization hooks are NOT called.
|
|
117
|
+
*/
|
|
118
|
+
export async function getAssetById(
|
|
119
|
+
adapter: Adapter,
|
|
120
|
+
id: string,
|
|
121
|
+
): Promise<Asset | null> {
|
|
122
|
+
return adapter.findOne<Asset>({
|
|
123
|
+
model: "mediaAsset",
|
|
124
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Retrieve all folders, optionally filtered by `parentId`.
|
|
130
|
+
* Pass `null` to list root-level folders (those without a parent).
|
|
131
|
+
* Pure DB function — no hooks, no HTTP context.
|
|
132
|
+
*
|
|
133
|
+
* @remarks **Security:** Authorization hooks are NOT called.
|
|
134
|
+
*/
|
|
135
|
+
export async function listFolders(
|
|
136
|
+
adapter: Adapter,
|
|
137
|
+
params?: FolderListParams,
|
|
138
|
+
): Promise<Folder[]> {
|
|
139
|
+
// Only add a where clause when parentId is explicitly provided as a string.
|
|
140
|
+
// Passing undefined (or no params) returns all folders unfiltered.
|
|
141
|
+
const where =
|
|
142
|
+
params?.parentId !== undefined
|
|
143
|
+
? [
|
|
144
|
+
{
|
|
145
|
+
field: "parentId",
|
|
146
|
+
value: params.parentId,
|
|
147
|
+
operator: "eq" as const,
|
|
148
|
+
},
|
|
149
|
+
]
|
|
150
|
+
: undefined;
|
|
151
|
+
|
|
152
|
+
return adapter.findMany<Folder>({
|
|
153
|
+
model: "mediaFolder",
|
|
154
|
+
where,
|
|
155
|
+
sortBy: { field: "name", direction: "asc" },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Retrieve a single folder by its ID.
|
|
161
|
+
* Returns `null` if no folder is found.
|
|
162
|
+
* Pure DB function — no hooks, no HTTP context.
|
|
163
|
+
*
|
|
164
|
+
* @remarks **Security:** Authorization hooks are NOT called.
|
|
165
|
+
*/
|
|
166
|
+
export async function getFolderById(
|
|
167
|
+
adapter: Adapter,
|
|
168
|
+
id: string,
|
|
169
|
+
): Promise<Folder | null> {
|
|
170
|
+
return adapter.findOne<Folder>({
|
|
171
|
+
model: "mediaFolder",
|
|
172
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export * from "./plugin";
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
listAssets,
|
|
5
|
+
getAssetById,
|
|
6
|
+
listFolders,
|
|
7
|
+
getFolderById,
|
|
8
|
+
type AssetListParams,
|
|
9
|
+
type AssetListResult,
|
|
10
|
+
type FolderListParams,
|
|
11
|
+
} from "./getters";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
createAsset,
|
|
15
|
+
updateAsset,
|
|
16
|
+
deleteAsset,
|
|
17
|
+
createFolder,
|
|
18
|
+
deleteFolder,
|
|
19
|
+
type CreateAssetInput,
|
|
20
|
+
type UpdateAssetInput,
|
|
21
|
+
type CreateFolderInput,
|
|
22
|
+
} from "./mutations";
|
|
23
|
+
|
|
24
|
+
export { serializeAsset, serializeFolder } from "./serializers";
|
|
25
|
+
|
|
26
|
+
export { MEDIA_QUERY_KEYS, assetListDiscriminator } from "./query-key-defs";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
localAdapter,
|
|
30
|
+
type LocalStorageAdapterOptions,
|
|
31
|
+
} from "./adapters/local";
|
|
32
|
+
|
|
33
|
+
export type {
|
|
34
|
+
StorageAdapter,
|
|
35
|
+
DirectStorageAdapter,
|
|
36
|
+
S3StorageAdapter,
|
|
37
|
+
S3UploadToken,
|
|
38
|
+
VercelBlobStorageAdapter,
|
|
39
|
+
VercelBlobHandlerCallbacks,
|
|
40
|
+
UploadOptions,
|
|
41
|
+
} from "./storage-adapter";
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { DBAdapter as Adapter } from "@btst/db";
|
|
2
|
+
import type { Asset, Folder } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Input for creating a new asset record.
|
|
6
|
+
*/
|
|
7
|
+
export interface CreateAssetInput {
|
|
8
|
+
filename: string;
|
|
9
|
+
originalName: string;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
size: number;
|
|
12
|
+
url: string;
|
|
13
|
+
folderId?: string;
|
|
14
|
+
alt?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Input for updating an existing asset record.
|
|
19
|
+
*/
|
|
20
|
+
export interface UpdateAssetInput {
|
|
21
|
+
alt?: string;
|
|
22
|
+
folderId?: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Input for creating a new folder.
|
|
27
|
+
*/
|
|
28
|
+
export interface CreateFolderInput {
|
|
29
|
+
name: string;
|
|
30
|
+
parentId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create an asset record in the database.
|
|
35
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
36
|
+
*
|
|
37
|
+
* @remarks **Security:** No authorization hooks (e.g. `onBeforeUpload`) are called.
|
|
38
|
+
* The caller is responsible for any access-control checks before invoking this function.
|
|
39
|
+
*/
|
|
40
|
+
export async function createAsset(
|
|
41
|
+
adapter: Adapter,
|
|
42
|
+
input: CreateAssetInput,
|
|
43
|
+
): Promise<Asset> {
|
|
44
|
+
return adapter.create<Asset>({
|
|
45
|
+
model: "mediaAsset",
|
|
46
|
+
data: {
|
|
47
|
+
filename: input.filename,
|
|
48
|
+
originalName: input.originalName,
|
|
49
|
+
mimeType: input.mimeType,
|
|
50
|
+
size: input.size,
|
|
51
|
+
url: input.url,
|
|
52
|
+
folderId: input.folderId,
|
|
53
|
+
alt: input.alt,
|
|
54
|
+
createdAt: new Date(),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Update an asset's `alt` text or `folderId`.
|
|
61
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
62
|
+
*
|
|
63
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
64
|
+
*/
|
|
65
|
+
export async function updateAsset(
|
|
66
|
+
adapter: Adapter,
|
|
67
|
+
id: string,
|
|
68
|
+
input: UpdateAssetInput,
|
|
69
|
+
): Promise<Asset | null> {
|
|
70
|
+
const update: Record<string, unknown> = {};
|
|
71
|
+
|
|
72
|
+
if (input.alt !== undefined) {
|
|
73
|
+
update.alt = input.alt;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if ("folderId" in input) {
|
|
77
|
+
// null explicitly clears the folder association; undefined means "not provided"
|
|
78
|
+
update.folderId = input.folderId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return adapter.update<Asset>({
|
|
82
|
+
model: "mediaAsset",
|
|
83
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
84
|
+
update,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Delete an asset record from the database by its ID.
|
|
90
|
+
* Does NOT delete the underlying file — the caller must do that via the storage adapter.
|
|
91
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
92
|
+
*
|
|
93
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
94
|
+
*/
|
|
95
|
+
export async function deleteAsset(adapter: Adapter, id: string): Promise<void> {
|
|
96
|
+
await adapter.delete<Asset>({
|
|
97
|
+
model: "mediaAsset",
|
|
98
|
+
where: [{ field: "id", value: id, operator: "eq" as const }],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Create a folder record in the database.
|
|
104
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
105
|
+
*
|
|
106
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
107
|
+
*/
|
|
108
|
+
export async function createFolder(
|
|
109
|
+
adapter: Adapter,
|
|
110
|
+
input: CreateFolderInput,
|
|
111
|
+
): Promise<Folder> {
|
|
112
|
+
return adapter.create<Folder>({
|
|
113
|
+
model: "mediaFolder",
|
|
114
|
+
data: {
|
|
115
|
+
name: input.name,
|
|
116
|
+
parentId: input.parentId,
|
|
117
|
+
createdAt: new Date(),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Delete a folder record from the database by its ID.
|
|
124
|
+
* Child folders are cascade-deleted automatically. Throws if the folder or
|
|
125
|
+
* any of its descendants contain assets (which have associated storage files
|
|
126
|
+
* that must be deleted via the storage adapter first).
|
|
127
|
+
* Pure DB function — no authorization hooks, no HTTP context.
|
|
128
|
+
*
|
|
129
|
+
* @remarks **Security:** No authorization hooks are called.
|
|
130
|
+
*/
|
|
131
|
+
export async function deleteFolder(
|
|
132
|
+
adapter: Adapter,
|
|
133
|
+
id: string,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
// BFS to collect the target folder and all of its descendants.
|
|
136
|
+
const allFolderIds: string[] = [id];
|
|
137
|
+
const queue: string[] = [id];
|
|
138
|
+
|
|
139
|
+
while (queue.length > 0) {
|
|
140
|
+
const parentId = queue.shift()!;
|
|
141
|
+
const children = await adapter.findMany<Folder>({
|
|
142
|
+
model: "mediaFolder",
|
|
143
|
+
where: [{ field: "parentId", value: parentId, operator: "eq" as const }],
|
|
144
|
+
});
|
|
145
|
+
for (const child of children) {
|
|
146
|
+
allFolderIds.push(child.id);
|
|
147
|
+
queue.push(child.id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Reject the deletion if any folder in the subtree contains assets.
|
|
152
|
+
// Assets map to real storage files — the caller must delete them via the
|
|
153
|
+
// storage adapter before removing the DB records.
|
|
154
|
+
let totalAssets = 0;
|
|
155
|
+
for (const folderId of allFolderIds) {
|
|
156
|
+
totalAssets += await adapter.count({
|
|
157
|
+
model: "mediaAsset",
|
|
158
|
+
where: [{ field: "folderId", value: folderId, operator: "eq" as const }],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (totalAssets > 0) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Cannot delete folder: it or one of its subfolders contains ${totalAssets} asset(s). Move or delete them first.`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Wrap all deletions in a transaction so the subtree is removed atomically.
|
|
169
|
+
// If any individual deletion fails the entire subtree is left intact.
|
|
170
|
+
await adapter.transaction(async (tx) => {
|
|
171
|
+
// Delete deepest folders first, then work back up to the root.
|
|
172
|
+
for (const folderId of [...allFolderIds].reverse()) {
|
|
173
|
+
await tx.delete<Folder>({
|
|
174
|
+
model: "mediaFolder",
|
|
175
|
+
where: [{ field: "id", value: folderId, operator: "eq" as const }],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|