@btst/stack 2.8.1 → 2.9.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 +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/api/index.d.cts +2 -2
- package/dist/plugins/blog/api/index.d.mts +2 -2
- package/dist/plugins/blog/api/index.d.ts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
- package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
- package/dist/plugins/blog/client/index.d.cts +60 -3
- package/dist/plugins/blog/client/index.d.mts +60 -3
- package/dist/plugins/blog/client/index.d.ts +60 -3
- package/dist/plugins/blog/query-keys.d.cts +2 -2
- package/dist/plugins/blog/query-keys.d.mts +2 -2
- package/dist/plugins/blog/query-keys.d.ts +2 -2
- 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 +54 -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 +52 -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.DOZ1EXjM.d.mts → stack.6mEHS2WH.d.mts} +3 -3
- package/dist/shared/{stack.DX-tQ93o.d.cts → stack.AJTXI7kw.d.cts} +3 -3
- package/dist/shared/{stack.DRpeDS6X.d.ts → stack.BMx2QYOK.d.ts} +25 -0
- package/dist/shared/stack.BUTXWiG-.d.ts +286 -0
- package/dist/shared/stack.C7Y9sBDg.d.mts +286 -0
- package/dist/shared/stack.C7vfOBmO.d.mts +63 -0
- package/dist/shared/stack.CAni8dnD.d.cts +63 -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.CoBj86jf.d.cts +109 -0
- package/dist/shared/stack.CoBj86jf.d.mts +109 -0
- package/dist/shared/stack.CoBj86jf.d.ts +109 -0
- package/dist/shared/{stack.BXxrFL9R.d.ts → stack.D7HSzZdG.d.ts} +5 -5
- package/dist/shared/{stack.DzOhpIYM.d.mts → stack.DjgpFWq3.d.cts} +5 -5
- package/dist/shared/{stack.BxFl46lB.d.cts → stack.DxQl8Wa1.d.cts} +25 -0
- package/dist/shared/{stack.BSqJrCTM.d.cts → stack.IUeyQKrm.d.mts} +5 -5
- package/dist/shared/{stack.VF6FhyZw.d.ts → stack.QYn-Px94.d.ts} +3 -3
- package/dist/shared/stack.vxskCkim.d.cts +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 +132 -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.BWp0hcm9.d.ts → stack.BQmuNl5p.d.cts} +3 -3
- package/dist/shared/{stack.BWp0hcm9.d.cts → stack.BQmuNl5p.d.mts} +3 -3
- package/dist/shared/{stack.BWp0hcm9.d.mts → stack.BQmuNl5p.d.ts} +3 -3
- package/dist/shared/{stack.BvCR4-9H.d.ts → stack.D4Cea8II.d.ts} +3 -3
- package/dist/shared/{stack.CWxAl9K3.d.mts → stack.HE_IvqV5.d.mts} +3 -3
- package/dist/shared/{stack.BOokfhZD.d.cts → stack.Rtcvl8sS.d.cts} +3 -3
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
3
|
+
import { defineDb } from "@btst/db";
|
|
4
|
+
import type { DBAdapter as Adapter } from "@btst/db";
|
|
5
|
+
import { mediaSchema } from "../db";
|
|
6
|
+
import {
|
|
7
|
+
listAssets,
|
|
8
|
+
getAssetById,
|
|
9
|
+
listFolders,
|
|
10
|
+
getFolderById,
|
|
11
|
+
} from "../api/getters";
|
|
12
|
+
|
|
13
|
+
const createTestAdapter = (): Adapter => {
|
|
14
|
+
const db = defineDb({}).use(mediaSchema);
|
|
15
|
+
return createMemoryAdapter(db)({});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const makeAsset = (
|
|
19
|
+
overrides: Partial<{
|
|
20
|
+
filename: string;
|
|
21
|
+
originalName: string;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
size: number;
|
|
24
|
+
url: string;
|
|
25
|
+
folderId: string | undefined;
|
|
26
|
+
alt: string | undefined;
|
|
27
|
+
}> = {},
|
|
28
|
+
) => ({
|
|
29
|
+
filename: "image.jpg",
|
|
30
|
+
originalName: "My Image.jpg",
|
|
31
|
+
mimeType: "image/jpeg",
|
|
32
|
+
size: 1024,
|
|
33
|
+
url: "https://example.com/image.jpg",
|
|
34
|
+
folderId: undefined,
|
|
35
|
+
alt: undefined,
|
|
36
|
+
createdAt: new Date(),
|
|
37
|
+
...overrides,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const makeFolder = (
|
|
41
|
+
overrides: Partial<{
|
|
42
|
+
name: string;
|
|
43
|
+
parentId: string | undefined;
|
|
44
|
+
}> = {},
|
|
45
|
+
) => ({
|
|
46
|
+
name: "My Folder",
|
|
47
|
+
parentId: undefined,
|
|
48
|
+
createdAt: new Date(),
|
|
49
|
+
...overrides,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("media getters", () => {
|
|
53
|
+
let adapter: Adapter;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
adapter = createTestAdapter();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── listAssets ────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
describe("listAssets", () => {
|
|
62
|
+
it("returns empty result when no assets exist", async () => {
|
|
63
|
+
const result = await listAssets(adapter);
|
|
64
|
+
expect(result.items).toEqual([]);
|
|
65
|
+
expect(result.total).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("returns all assets with correct fields", async () => {
|
|
69
|
+
await adapter.create({
|
|
70
|
+
model: "mediaAsset",
|
|
71
|
+
data: makeAsset({
|
|
72
|
+
filename: "photo.jpg",
|
|
73
|
+
url: "https://example.com/photo.jpg",
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const result = await listAssets(adapter);
|
|
78
|
+
expect(result.items).toHaveLength(1);
|
|
79
|
+
expect(result.total).toBe(1);
|
|
80
|
+
expect(result.items[0]!.filename).toBe("photo.jpg");
|
|
81
|
+
expect(result.items[0]!.mimeType).toBe("image/jpeg");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("filters assets by folderId", async () => {
|
|
85
|
+
await adapter.create({
|
|
86
|
+
model: "mediaFolder",
|
|
87
|
+
data: makeFolder({ name: "Photos" }),
|
|
88
|
+
});
|
|
89
|
+
const folder = await adapter.findOne<{ id: string }>({
|
|
90
|
+
model: "mediaFolder",
|
|
91
|
+
where: [{ field: "name", value: "Photos", operator: "eq" }],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await adapter.create({
|
|
95
|
+
model: "mediaAsset",
|
|
96
|
+
data: makeAsset({ filename: "in-folder.jpg", folderId: folder!.id }),
|
|
97
|
+
});
|
|
98
|
+
await adapter.create({
|
|
99
|
+
model: "mediaAsset",
|
|
100
|
+
data: makeAsset({ filename: "no-folder.jpg" }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await listAssets(adapter, { folderId: folder!.id });
|
|
104
|
+
expect(result.items).toHaveLength(1);
|
|
105
|
+
expect(result.items[0]!.filename).toBe("in-folder.jpg");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("filters assets by mimeType", async () => {
|
|
109
|
+
await adapter.create({
|
|
110
|
+
model: "mediaAsset",
|
|
111
|
+
data: makeAsset({ filename: "image.jpg", mimeType: "image/jpeg" }),
|
|
112
|
+
});
|
|
113
|
+
await adapter.create({
|
|
114
|
+
model: "mediaAsset",
|
|
115
|
+
data: makeAsset({ filename: "doc.pdf", mimeType: "application/pdf" }),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const images = await listAssets(adapter, { mimeType: "image/jpeg" });
|
|
119
|
+
expect(images.items).toHaveLength(1);
|
|
120
|
+
expect(images.items[0]!.filename).toBe("image.jpg");
|
|
121
|
+
|
|
122
|
+
const pdfs = await listAssets(adapter, { mimeType: "application/pdf" });
|
|
123
|
+
expect(pdfs.items).toHaveLength(1);
|
|
124
|
+
expect(pdfs.items[0]!.filename).toBe("doc.pdf");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("searches assets by query string across filename, originalName, and alt", async () => {
|
|
128
|
+
await adapter.create({
|
|
129
|
+
model: "mediaAsset",
|
|
130
|
+
data: makeAsset({
|
|
131
|
+
filename: "holiday-photo.jpg",
|
|
132
|
+
originalName: "Holiday Photo.jpg",
|
|
133
|
+
alt: "Beach sunset",
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
await adapter.create({
|
|
137
|
+
model: "mediaAsset",
|
|
138
|
+
data: makeAsset({
|
|
139
|
+
filename: "logo.png",
|
|
140
|
+
originalName: "Company Logo.png",
|
|
141
|
+
alt: "Brand logo",
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const holidayResult = await listAssets(adapter, { query: "holiday" });
|
|
146
|
+
expect(holidayResult.items).toHaveLength(1);
|
|
147
|
+
expect(holidayResult.items[0]!.filename).toBe("holiday-photo.jpg");
|
|
148
|
+
|
|
149
|
+
// "beach" matches only the alt text of the first asset
|
|
150
|
+
const beachResult = await listAssets(adapter, { query: "beach" });
|
|
151
|
+
expect(beachResult.items).toHaveLength(1);
|
|
152
|
+
expect(beachResult.items[0]!.alt).toBe("Beach sunset");
|
|
153
|
+
|
|
154
|
+
// "logo" matches only the second asset (filename, originalName, alt)
|
|
155
|
+
const logoResult = await listAssets(adapter, { query: "logo" });
|
|
156
|
+
expect(logoResult.items).toHaveLength(1);
|
|
157
|
+
expect(logoResult.items[0]!.filename).toBe("logo.png");
|
|
158
|
+
|
|
159
|
+
// "photo" matches only the first asset via filename and originalName
|
|
160
|
+
const photoResult = await listAssets(adapter, { query: "photo" });
|
|
161
|
+
expect(photoResult.items).toHaveLength(1);
|
|
162
|
+
expect(photoResult.items[0]!.filename).toBe("holiday-photo.jpg");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("paginates results with limit and offset", async () => {
|
|
166
|
+
for (let i = 0; i < 5; i++) {
|
|
167
|
+
await adapter.create({
|
|
168
|
+
model: "mediaAsset",
|
|
169
|
+
data: makeAsset({
|
|
170
|
+
filename: `asset-${i}.jpg`,
|
|
171
|
+
url: `https://example.com/${i}.jpg`,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const page1 = await listAssets(adapter, { limit: 2, offset: 0 });
|
|
177
|
+
expect(page1.items).toHaveLength(2);
|
|
178
|
+
expect(page1.total).toBe(5);
|
|
179
|
+
|
|
180
|
+
const page2 = await listAssets(adapter, { limit: 2, offset: 2 });
|
|
181
|
+
expect(page2.items).toHaveLength(2);
|
|
182
|
+
expect(page2.total).toBe(5);
|
|
183
|
+
|
|
184
|
+
const page3 = await listAssets(adapter, { limit: 2, offset: 4 });
|
|
185
|
+
expect(page3.items).toHaveLength(1);
|
|
186
|
+
expect(page3.total).toBe(5);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── getAssetById ─────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
describe("getAssetById", () => {
|
|
193
|
+
it("returns null when asset does not exist", async () => {
|
|
194
|
+
const result = await getAssetById(adapter, "nonexistent-id");
|
|
195
|
+
expect(result).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("returns the asset by ID", async () => {
|
|
199
|
+
const created = await adapter.create<{ id: string }>({
|
|
200
|
+
model: "mediaAsset",
|
|
201
|
+
data: makeAsset({ filename: "found.jpg" }),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const result = await getAssetById(adapter, created.id);
|
|
205
|
+
expect(result).not.toBeNull();
|
|
206
|
+
expect(result!.filename).toBe("found.jpg");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── listFolders ───────────────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
describe("listFolders", () => {
|
|
213
|
+
it("returns empty array when no folders exist", async () => {
|
|
214
|
+
const result = await listFolders(adapter);
|
|
215
|
+
expect(result).toEqual([]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("returns all folders sorted by name", async () => {
|
|
219
|
+
await adapter.create({
|
|
220
|
+
model: "mediaFolder",
|
|
221
|
+
data: makeFolder({ name: "Zeta" }),
|
|
222
|
+
});
|
|
223
|
+
await adapter.create({
|
|
224
|
+
model: "mediaFolder",
|
|
225
|
+
data: makeFolder({ name: "Alpha" }),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const result = await listFolders(adapter);
|
|
229
|
+
expect(result).toHaveLength(2);
|
|
230
|
+
expect(result[0]!.name).toBe("Alpha");
|
|
231
|
+
expect(result[1]!.name).toBe("Zeta");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("filters folders by parentId", async () => {
|
|
235
|
+
const root = await adapter.create<{ id: string }>({
|
|
236
|
+
model: "mediaFolder",
|
|
237
|
+
data: makeFolder({ name: "Root" }),
|
|
238
|
+
});
|
|
239
|
+
await adapter.create({
|
|
240
|
+
model: "mediaFolder",
|
|
241
|
+
data: makeFolder({ name: "Child", parentId: root.id }),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// No params — returns ALL folders
|
|
245
|
+
const allFolders = await listFolders(adapter);
|
|
246
|
+
expect(allFolders).toHaveLength(2);
|
|
247
|
+
|
|
248
|
+
// Filter children of root
|
|
249
|
+
const childFolders = await listFolders(adapter, { parentId: root.id });
|
|
250
|
+
expect(childFolders).toHaveLength(1);
|
|
251
|
+
expect(childFolders[0]!.name).toBe("Child");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ── getFolderById ─────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
describe("getFolderById", () => {
|
|
258
|
+
it("returns null when folder does not exist", async () => {
|
|
259
|
+
const result = await getFolderById(adapter, "nonexistent-id");
|
|
260
|
+
expect(result).toBeNull();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("returns the folder by ID", async () => {
|
|
264
|
+
const created = await adapter.create<{ id: string }>({
|
|
265
|
+
model: "mediaFolder",
|
|
266
|
+
data: makeFolder({ name: "Test Folder" }),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const result = await getFolderById(adapter, created.id);
|
|
270
|
+
expect(result).not.toBeNull();
|
|
271
|
+
expect(result!.name).toBe("Test Folder");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
3
|
+
import { defineDb } from "@btst/db";
|
|
4
|
+
import type { DBAdapter as Adapter } from "@btst/db";
|
|
5
|
+
import { mediaSchema } from "../db";
|
|
6
|
+
import type { Asset, Folder } from "../types";
|
|
7
|
+
import {
|
|
8
|
+
createAsset,
|
|
9
|
+
updateAsset,
|
|
10
|
+
deleteAsset,
|
|
11
|
+
createFolder,
|
|
12
|
+
deleteFolder,
|
|
13
|
+
} from "../api/mutations";
|
|
14
|
+
import { getAssetById, getFolderById } from "../api/getters";
|
|
15
|
+
|
|
16
|
+
const createTestAdapter = (): Adapter => {
|
|
17
|
+
const db = defineDb({}).use(mediaSchema);
|
|
18
|
+
return createMemoryAdapter(db)({});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const assetInput = {
|
|
22
|
+
filename: "photo.jpg",
|
|
23
|
+
originalName: "My Photo.jpg",
|
|
24
|
+
mimeType: "image/jpeg",
|
|
25
|
+
size: 2048,
|
|
26
|
+
url: "https://example.com/photo.jpg",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("media mutations", () => {
|
|
30
|
+
let adapter: Adapter;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
adapter = createTestAdapter();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── createAsset ───────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
describe("createAsset", () => {
|
|
39
|
+
it("creates an asset with required fields", async () => {
|
|
40
|
+
const asset = await createAsset(adapter, assetInput);
|
|
41
|
+
|
|
42
|
+
expect(asset.id).toBeDefined();
|
|
43
|
+
expect(asset.filename).toBe("photo.jpg");
|
|
44
|
+
expect(asset.originalName).toBe("My Photo.jpg");
|
|
45
|
+
expect(asset.mimeType).toBe("image/jpeg");
|
|
46
|
+
expect(asset.size).toBe(2048);
|
|
47
|
+
expect(asset.url).toBe("https://example.com/photo.jpg");
|
|
48
|
+
expect(asset.createdAt).toBeInstanceOf(Date);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("creates an asset with optional fields", async () => {
|
|
52
|
+
const asset = await createAsset(adapter, {
|
|
53
|
+
...assetInput,
|
|
54
|
+
folderId: "folder-123",
|
|
55
|
+
alt: "A beautiful photo",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(asset.folderId).toBe("folder-123");
|
|
59
|
+
expect(asset.alt).toBe("A beautiful photo");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("creates multiple independent assets", async () => {
|
|
63
|
+
await createAsset(adapter, {
|
|
64
|
+
...assetInput,
|
|
65
|
+
filename: "a.jpg",
|
|
66
|
+
url: "https://example.com/a.jpg",
|
|
67
|
+
});
|
|
68
|
+
await createAsset(adapter, {
|
|
69
|
+
...assetInput,
|
|
70
|
+
filename: "b.jpg",
|
|
71
|
+
url: "https://example.com/b.jpg",
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const all = await adapter.findMany<Asset>({ model: "mediaAsset" });
|
|
75
|
+
expect(all).toHaveLength(2);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── updateAsset ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
describe("updateAsset", () => {
|
|
82
|
+
it("updates the alt text of an asset", async () => {
|
|
83
|
+
const asset = await createAsset(adapter, assetInput);
|
|
84
|
+
const updated = await updateAsset(adapter, asset.id, {
|
|
85
|
+
alt: "Updated alt text",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(updated).not.toBeNull();
|
|
89
|
+
expect(updated!.alt).toBe("Updated alt text");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("updates the folderId of an asset", async () => {
|
|
93
|
+
const asset = await createAsset(adapter, assetInput);
|
|
94
|
+
const updated = await updateAsset(adapter, asset.id, {
|
|
95
|
+
folderId: "folder-abc",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(updated!.folderId).toBe("folder-abc");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns the updated asset when only folderId is changed", async () => {
|
|
102
|
+
const asset = await createAsset(adapter, {
|
|
103
|
+
...assetInput,
|
|
104
|
+
folderId: "folder-old",
|
|
105
|
+
});
|
|
106
|
+
const updated = await updateAsset(adapter, asset.id, {
|
|
107
|
+
folderId: "folder-new",
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(updated).not.toBeNull();
|
|
111
|
+
expect(updated!.folderId).toBe("folder-new");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("clears the folder association when folderId is null", async () => {
|
|
115
|
+
const asset = await createAsset(adapter, {
|
|
116
|
+
...assetInput,
|
|
117
|
+
folderId: "folder-to-clear",
|
|
118
|
+
});
|
|
119
|
+
expect(asset.folderId).toBe("folder-to-clear");
|
|
120
|
+
|
|
121
|
+
const updated = await updateAsset(adapter, asset.id, { folderId: null });
|
|
122
|
+
expect(updated).not.toBeNull();
|
|
123
|
+
expect(updated!.folderId == null).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("does not change folderId when folderId is not in the input", async () => {
|
|
127
|
+
const asset = await createAsset(adapter, {
|
|
128
|
+
...assetInput,
|
|
129
|
+
folderId: "folder-keep",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const updated = await updateAsset(adapter, asset.id, {
|
|
133
|
+
alt: "new alt text",
|
|
134
|
+
});
|
|
135
|
+
expect(updated).not.toBeNull();
|
|
136
|
+
expect(updated!.folderId).toBe("folder-keep");
|
|
137
|
+
expect(updated!.alt).toBe("new alt text");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("returns null for nonexistent asset", async () => {
|
|
141
|
+
const result = await updateAsset(adapter, "nonexistent-id", {
|
|
142
|
+
alt: "test",
|
|
143
|
+
});
|
|
144
|
+
expect(result).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── deleteAsset ───────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe("deleteAsset", () => {
|
|
151
|
+
it("removes the asset from the database", async () => {
|
|
152
|
+
const asset = await createAsset(adapter, assetInput);
|
|
153
|
+
await deleteAsset(adapter, asset.id);
|
|
154
|
+
|
|
155
|
+
const found = await getAssetById(adapter, asset.id);
|
|
156
|
+
expect(found).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("does not throw when deleting a nonexistent asset", async () => {
|
|
160
|
+
await expect(
|
|
161
|
+
deleteAsset(adapter, "nonexistent-id"),
|
|
162
|
+
).resolves.not.toThrow();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("only deletes the targeted asset", async () => {
|
|
166
|
+
const a = await createAsset(adapter, {
|
|
167
|
+
...assetInput,
|
|
168
|
+
filename: "a.jpg",
|
|
169
|
+
url: "https://example.com/a.jpg",
|
|
170
|
+
});
|
|
171
|
+
await createAsset(adapter, {
|
|
172
|
+
...assetInput,
|
|
173
|
+
filename: "b.jpg",
|
|
174
|
+
url: "https://example.com/b.jpg",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await deleteAsset(adapter, a.id);
|
|
178
|
+
|
|
179
|
+
const remaining = await adapter.findMany<Asset>({ model: "mediaAsset" });
|
|
180
|
+
expect(remaining).toHaveLength(1);
|
|
181
|
+
expect(remaining[0]!.filename).toBe("b.jpg");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── createFolder ──────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
describe("createFolder", () => {
|
|
188
|
+
it("creates a root folder", async () => {
|
|
189
|
+
const folder = await createFolder(adapter, { name: "Uploads" });
|
|
190
|
+
|
|
191
|
+
expect(folder.id).toBeDefined();
|
|
192
|
+
expect(folder.name).toBe("Uploads");
|
|
193
|
+
expect(folder.parentId).toBeUndefined();
|
|
194
|
+
expect(folder.createdAt).toBeInstanceOf(Date);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("creates a nested folder with parentId", async () => {
|
|
198
|
+
const parent = await createFolder(adapter, { name: "Root" });
|
|
199
|
+
const child = await createFolder(adapter, {
|
|
200
|
+
name: "Photos",
|
|
201
|
+
parentId: parent.id,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(child.parentId).toBe(parent.id);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ── deleteFolder ──────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
describe("deleteFolder", () => {
|
|
211
|
+
it("deletes an empty folder", async () => {
|
|
212
|
+
const folder = await createFolder(adapter, { name: "Empty" });
|
|
213
|
+
await deleteFolder(adapter, folder.id);
|
|
214
|
+
|
|
215
|
+
const found = await getFolderById(adapter, folder.id);
|
|
216
|
+
expect(found).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("throws an error when the folder contains assets", async () => {
|
|
220
|
+
const folder = await createFolder(adapter, { name: "Full Folder" });
|
|
221
|
+
await createAsset(adapter, { ...assetInput, folderId: folder.id });
|
|
222
|
+
|
|
223
|
+
await expect(deleteFolder(adapter, folder.id)).rejects.toThrow(
|
|
224
|
+
"Cannot delete folder",
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const stillExists = await getFolderById(adapter, folder.id);
|
|
228
|
+
expect(stillExists).not.toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("allows deletion after assets are removed", async () => {
|
|
232
|
+
const folder = await createFolder(adapter, { name: "Soon Empty" });
|
|
233
|
+
const asset = await createAsset(adapter, {
|
|
234
|
+
...assetInput,
|
|
235
|
+
folderId: folder.id,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await deleteAsset(adapter, asset.id);
|
|
239
|
+
await expect(deleteFolder(adapter, folder.id)).resolves.not.toThrow();
|
|
240
|
+
|
|
241
|
+
const found = await getFolderById(adapter, folder.id);
|
|
242
|
+
expect(found).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("does not throw when deleting a nonexistent folder", async () => {
|
|
246
|
+
await expect(
|
|
247
|
+
deleteFolder(adapter, "nonexistent-id"),
|
|
248
|
+
).resolves.not.toThrow();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("only deletes the targeted folder and not siblings", async () => {
|
|
252
|
+
const a = await createFolder(adapter, { name: "Folder A" });
|
|
253
|
+
await createFolder(adapter, { name: "Folder B" });
|
|
254
|
+
|
|
255
|
+
await deleteFolder(adapter, a.id);
|
|
256
|
+
|
|
257
|
+
const remaining = await adapter.findMany<Folder>({
|
|
258
|
+
model: "mediaFolder",
|
|
259
|
+
});
|
|
260
|
+
expect(remaining).toHaveLength(1);
|
|
261
|
+
expect(remaining[0]!.name).toBe("Folder B");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("cascade-deletes child folders when the subtree has no assets", async () => {
|
|
265
|
+
const parent = await createFolder(adapter, { name: "Parent" });
|
|
266
|
+
const child = await createFolder(adapter, {
|
|
267
|
+
name: "Child",
|
|
268
|
+
parentId: parent.id,
|
|
269
|
+
});
|
|
270
|
+
const grandchild = await createFolder(adapter, {
|
|
271
|
+
name: "Grandchild",
|
|
272
|
+
parentId: child.id,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await deleteFolder(adapter, parent.id);
|
|
276
|
+
|
|
277
|
+
expect(await getFolderById(adapter, parent.id)).toBeNull();
|
|
278
|
+
expect(await getFolderById(adapter, child.id)).toBeNull();
|
|
279
|
+
expect(await getFolderById(adapter, grandchild.id)).toBeNull();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("throws when a child folder contains assets and leaves the whole subtree intact", async () => {
|
|
283
|
+
const parent = await createFolder(adapter, { name: "Parent" });
|
|
284
|
+
const child = await createFolder(adapter, {
|
|
285
|
+
name: "Child",
|
|
286
|
+
parentId: parent.id,
|
|
287
|
+
});
|
|
288
|
+
await createAsset(adapter, { ...assetInput, folderId: child.id });
|
|
289
|
+
|
|
290
|
+
await expect(deleteFolder(adapter, parent.id)).rejects.toThrow(
|
|
291
|
+
"Cannot delete folder",
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Both folders still exist — no partial deletion.
|
|
295
|
+
expect(await getFolderById(adapter, parent.id)).not.toBeNull();
|
|
296
|
+
expect(await getFolderById(adapter, child.id)).not.toBeNull();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|