@btst/stack 2.1.0 → 2.2.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/dist/api/index.cjs +9 -1
- package/dist/api/index.d.cts +4 -4
- package/dist/api/index.d.mts +4 -4
- package/dist/api/index.d.ts +4 -4
- package/dist/api/index.mjs +9 -1
- package/dist/client/index.d.cts +2 -2
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/packages/stack/src/plugins/ai-chat/api/getters.cjs +42 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/getters.mjs +39 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.cjs +5 -0
- package/dist/packages/stack/src/plugins/ai-chat/api/plugin.mjs +5 -0
- package/dist/packages/stack/src/plugins/blog/api/getters.cjs +131 -0
- package/dist/packages/stack/src/plugins/blog/api/getters.mjs +127 -0
- package/dist/packages/stack/src/plugins/blog/api/plugin.cjs +9 -107
- package/dist/packages/stack/src/plugins/blog/api/plugin.mjs +9 -107
- package/dist/packages/stack/src/plugins/blog/client/plugin.cjs +1 -1
- package/dist/packages/stack/src/plugins/blog/client/plugin.mjs +1 -1
- package/dist/packages/stack/src/plugins/cms/api/getters.cjs +146 -0
- package/dist/packages/stack/src/plugins/cms/api/getters.mjs +138 -0
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +560 -622
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +559 -621
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +1 -1
- package/dist/packages/stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +1 -1
- package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.cjs +6 -3
- package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.mjs +6 -3
- package/dist/packages/stack/src/plugins/form-builder/api/getters.cjs +111 -0
- package/dist/packages/stack/src/plugins/form-builder/api/getters.mjs +104 -0
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.cjs +16 -88
- package/dist/packages/stack/src/plugins/form-builder/api/plugin.mjs +12 -84
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +1 -1
- package/dist/packages/stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/api/getters.cjs +84 -0
- package/dist/packages/stack/src/plugins/kanban/api/getters.mjs +81 -0
- package/dist/packages/stack/src/plugins/kanban/api/plugin.cjs +9 -123
- package/dist/packages/stack/src/plugins/kanban/api/plugin.mjs +9 -123
- package/dist/packages/stack/src/plugins/kanban/client/plugin.cjs +1 -1
- package/dist/packages/stack/src/plugins/kanban/client/plugin.mjs +1 -1
- package/dist/plugins/ai-chat/api/index.cjs +3 -0
- package/dist/plugins/ai-chat/api/index.d.cts +27 -4
- package/dist/plugins/ai-chat/api/index.d.mts +27 -4
- package/dist/plugins/ai-chat/api/index.d.ts +27 -4
- package/dist/plugins/ai-chat/api/index.mjs +1 -0
- package/dist/plugins/ai-chat/client/hooks/index.d.cts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.mts +2 -2
- package/dist/plugins/ai-chat/client/hooks/index.d.ts +2 -2
- package/dist/plugins/ai-chat/query-keys.d.cts +9 -284
- package/dist/plugins/ai-chat/query-keys.d.mts +9 -284
- package/dist/plugins/ai-chat/query-keys.d.ts +9 -284
- package/dist/plugins/api/index.d.cts +4 -3
- package/dist/plugins/api/index.d.mts +4 -3
- package/dist/plugins/api/index.d.ts +4 -3
- package/dist/plugins/blog/api/index.cjs +4 -0
- package/dist/plugins/blog/api/index.d.cts +3 -2
- package/dist/plugins/blog/api/index.d.mts +3 -2
- package/dist/plugins/blog/api/index.d.ts +3 -2
- package/dist/plugins/blog/api/index.mjs +1 -0
- package/dist/plugins/blog/client/hooks/index.d.cts +4 -4
- package/dist/plugins/blog/client/hooks/index.d.mts +4 -4
- package/dist/plugins/blog/client/hooks/index.d.ts +4 -4
- package/dist/plugins/blog/client/index.d.cts +1 -1
- package/dist/plugins/blog/client/index.d.mts +1 -1
- package/dist/plugins/blog/client/index.d.ts +1 -1
- package/dist/plugins/blog/query-keys.cjs +7 -4
- package/dist/plugins/blog/query-keys.d.cts +81 -27
- package/dist/plugins/blog/query-keys.d.mts +81 -27
- package/dist/plugins/blog/query-keys.d.ts +81 -27
- package/dist/plugins/blog/query-keys.mjs +7 -4
- package/dist/plugins/client/index.d.cts +2 -2
- package/dist/plugins/client/index.d.mts +2 -2
- package/dist/plugins/client/index.d.ts +2 -2
- package/dist/plugins/cms/api/index.cjs +4 -0
- package/dist/plugins/cms/api/index.d.cts +61 -5
- package/dist/plugins/cms/api/index.d.mts +61 -5
- package/dist/plugins/cms/api/index.d.ts +61 -5
- package/dist/plugins/cms/api/index.mjs +1 -0
- package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
- package/dist/plugins/cms/query-keys.d.cts +2 -1
- package/dist/plugins/cms/query-keys.d.mts +2 -1
- package/dist/plugins/cms/query-keys.d.ts +2 -1
- package/dist/plugins/form-builder/api/index.cjs +4 -0
- package/dist/plugins/form-builder/api/index.d.cts +77 -7
- package/dist/plugins/form-builder/api/index.d.mts +77 -7
- package/dist/plugins/form-builder/api/index.d.ts +77 -7
- package/dist/plugins/form-builder/api/index.mjs +1 -0
- package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.cts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.mts +1 -1
- package/dist/plugins/form-builder/client/hooks/index.d.ts +1 -1
- package/dist/plugins/form-builder/query-keys.d.cts +2 -1
- package/dist/plugins/form-builder/query-keys.d.mts +2 -1
- package/dist/plugins/form-builder/query-keys.d.ts +2 -1
- package/dist/plugins/kanban/api/index.cjs +3 -0
- package/dist/plugins/kanban/api/index.d.cts +40 -43
- package/dist/plugins/kanban/api/index.d.mts +40 -43
- package/dist/plugins/kanban/api/index.d.ts +40 -43
- package/dist/plugins/kanban/api/index.mjs +1 -0
- package/dist/plugins/kanban/client/components/index.d.cts +1 -1
- package/dist/plugins/kanban/client/components/index.d.mts +1 -1
- package/dist/plugins/kanban/client/components/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.cjs +4 -3
- package/dist/plugins/kanban/query-keys.d.cts +2 -1
- package/dist/plugins/kanban/query-keys.d.mts +2 -1
- package/dist/plugins/kanban/query-keys.d.ts +2 -1
- package/dist/plugins/kanban/query-keys.mjs +4 -3
- package/dist/plugins/open-api/api/index.d.cts +2 -2
- package/dist/plugins/open-api/api/index.d.mts +2 -2
- package/dist/plugins/open-api/api/index.d.ts +2 -2
- package/dist/plugins/route-docs/client/index.d.cts +1 -1
- package/dist/plugins/route-docs/client/index.d.mts +1 -1
- package/dist/plugins/route-docs/client/index.d.ts +1 -1
- package/dist/plugins/ui-builder/index.d.cts +1 -1
- package/dist/plugins/ui-builder/index.d.mts +1 -1
- package/dist/plugins/ui-builder/index.d.ts +1 -1
- package/dist/shared/{stack.BoA0xkJv.d.cts → stack.7n9Y_u7N.d.cts} +33 -7
- package/dist/shared/{stack.BoA0xkJv.d.mts → stack.7n9Y_u7N.d.mts} +33 -7
- package/dist/shared/{stack.BoA0xkJv.d.ts → stack.7n9Y_u7N.d.ts} +33 -7
- package/dist/shared/stack.BeSm90va.d.ts +289 -0
- package/dist/shared/{stack.DzH_wcvr.d.mts → stack.CIrIsc-A.d.cts} +2 -2
- package/dist/shared/{stack.DzH_wcvr.d.ts → stack.CIrIsc-A.d.mts} +2 -2
- package/dist/shared/{stack.DzH_wcvr.d.cts → stack.CIrIsc-A.d.ts} +2 -2
- package/dist/shared/stack.CMh_EdxW.d.cts +289 -0
- package/dist/shared/{stack.BsXokfNh.d.mts → stack.CXjzTMsb.d.cts} +1 -1
- package/dist/shared/{stack.BsXokfNh.d.ts → stack.CXjzTMsb.d.mts} +1 -1
- package/dist/shared/{stack.BsXokfNh.d.cts → stack.CXjzTMsb.d.ts} +1 -1
- package/dist/shared/stack.Dg09R0oB.d.mts +289 -0
- package/dist/shared/{stack.DKDMI-QO.d.mts → stack.QD1y_7NY.d.cts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.ts → stack.QD1y_7NY.d.mts} +7 -1
- package/dist/shared/{stack.DKDMI-QO.d.cts → stack.QD1y_7NY.d.ts} +7 -1
- package/package.json +1 -1
- package/src/__tests__/stack-api.test.ts +118 -0
- package/src/api/index.ts +15 -1
- package/src/plugins/ai-chat/__tests__/getters.test.ts +109 -0
- package/src/plugins/ai-chat/api/getters.ts +71 -0
- package/src/plugins/ai-chat/api/index.ts +1 -0
- package/src/plugins/ai-chat/api/plugin.ts +8 -0
- package/src/plugins/api/index.ts +3 -1
- package/src/plugins/blog/__tests__/getters.test.ts +540 -0
- package/src/plugins/blog/api/getters.ts +243 -0
- package/src/plugins/blog/api/index.ts +7 -0
- package/src/plugins/blog/api/plugin.ts +13 -141
- package/src/plugins/blog/client/plugin.tsx +2 -1
- package/src/plugins/blog/query-keys.ts +16 -13
- package/src/plugins/cms/__tests__/getters.test.ts +206 -0
- package/src/plugins/cms/api/getters.ts +244 -0
- package/src/plugins/cms/api/index.ts +5 -0
- package/src/plugins/cms/api/plugin.ts +50 -154
- package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +1 -1
- package/src/plugins/cms/client/hooks/cms-hooks.tsx +3 -0
- package/src/plugins/cms/types.ts +1 -1
- package/src/plugins/form-builder/__tests__/getters.test.ts +159 -0
- package/src/plugins/form-builder/api/getters.ts +203 -0
- package/src/plugins/form-builder/api/index.ts +1 -0
- package/src/plugins/form-builder/api/plugin.ts +22 -115
- package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +1 -1
- package/src/plugins/form-builder/types.ts +2 -2
- package/src/plugins/kanban/__tests__/getters.test.ts +172 -0
- package/src/plugins/kanban/api/getters.ts +149 -0
- package/src/plugins/kanban/api/index.ts +1 -0
- package/src/plugins/kanban/api/plugin.ts +16 -146
- package/src/plugins/kanban/client/plugin.tsx +2 -1
- package/src/plugins/kanban/query-keys.ts +8 -5
- package/src/types.ts +44 -5
- package/dist/shared/{stack.CbuN2zVV.d.cts → stack.BkYlUT_8.d.cts} +6 -6
- package/dist/shared/{stack.CbuN2zVV.d.mts → stack.BkYlUT_8.d.mts} +6 -6
- package/dist/shared/{stack.CbuN2zVV.d.ts → stack.BkYlUT_8.d.ts} +6 -6
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { createMemoryAdapter } from "@btst/adapter-memory";
|
|
3
|
+
import { defineDb } from "@btst/db";
|
|
4
|
+
import type { Adapter } from "@btst/db";
|
|
5
|
+
import { cmsSchema } from "../db";
|
|
6
|
+
import {
|
|
7
|
+
getAllContentTypes,
|
|
8
|
+
getAllContentItems,
|
|
9
|
+
getContentItemBySlug,
|
|
10
|
+
} from "../api/getters";
|
|
11
|
+
|
|
12
|
+
const createTestAdapter = (): Adapter => {
|
|
13
|
+
const db = defineDb({}).use(cmsSchema);
|
|
14
|
+
return createMemoryAdapter(db)({});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const SIMPLE_SCHEMA = JSON.stringify({
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
title: { type: "string" },
|
|
21
|
+
},
|
|
22
|
+
autoFormVersion: 2,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("cms getters", () => {
|
|
26
|
+
let adapter: Adapter;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
adapter = createTestAdapter();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("getAllContentTypes", () => {
|
|
33
|
+
it("returns empty array when no content types exist", async () => {
|
|
34
|
+
const types = await getAllContentTypes(adapter);
|
|
35
|
+
expect(types).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns serialized content types sorted by name", async () => {
|
|
39
|
+
await adapter.create({
|
|
40
|
+
model: "contentType",
|
|
41
|
+
data: {
|
|
42
|
+
name: "Post",
|
|
43
|
+
slug: "post",
|
|
44
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
45
|
+
autoFormVersion: 2,
|
|
46
|
+
createdAt: new Date(),
|
|
47
|
+
updatedAt: new Date(),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
await adapter.create({
|
|
51
|
+
model: "contentType",
|
|
52
|
+
data: {
|
|
53
|
+
name: "Article",
|
|
54
|
+
slug: "article",
|
|
55
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
56
|
+
autoFormVersion: 2,
|
|
57
|
+
createdAt: new Date(),
|
|
58
|
+
updatedAt: new Date(),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const types = await getAllContentTypes(adapter);
|
|
63
|
+
expect(types).toHaveLength(2);
|
|
64
|
+
// Sorted by name
|
|
65
|
+
expect(types[0]!.slug).toBe("article");
|
|
66
|
+
expect(types[1]!.slug).toBe("post");
|
|
67
|
+
// Dates are serialized as strings
|
|
68
|
+
expect(typeof types[0]!.createdAt).toBe("string");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("getAllContentItems", () => {
|
|
73
|
+
it("returns empty result when content type does not exist", async () => {
|
|
74
|
+
const result = await getAllContentItems(adapter, "nonexistent");
|
|
75
|
+
expect(result.items).toEqual([]);
|
|
76
|
+
expect(result.total).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns items for a content type", async () => {
|
|
80
|
+
const ct = (await adapter.create({
|
|
81
|
+
model: "contentType",
|
|
82
|
+
data: {
|
|
83
|
+
name: "Post",
|
|
84
|
+
slug: "post",
|
|
85
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
86
|
+
autoFormVersion: 2,
|
|
87
|
+
createdAt: new Date(),
|
|
88
|
+
updatedAt: new Date(),
|
|
89
|
+
},
|
|
90
|
+
})) as any;
|
|
91
|
+
|
|
92
|
+
await adapter.create({
|
|
93
|
+
model: "contentItem",
|
|
94
|
+
data: {
|
|
95
|
+
contentTypeId: ct.id,
|
|
96
|
+
slug: "my-post",
|
|
97
|
+
data: JSON.stringify({ title: "My Post" }),
|
|
98
|
+
createdAt: new Date(),
|
|
99
|
+
updatedAt: new Date(),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await getAllContentItems(adapter, "post");
|
|
104
|
+
expect(result.items).toHaveLength(1);
|
|
105
|
+
expect(result.total).toBe(1);
|
|
106
|
+
expect(result.items[0]!.slug).toBe("my-post");
|
|
107
|
+
expect(result.items[0]!.parsedData).toEqual({ title: "My Post" });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("filters items by slug", async () => {
|
|
111
|
+
const ct = (await adapter.create({
|
|
112
|
+
model: "contentType",
|
|
113
|
+
data: {
|
|
114
|
+
name: "Post",
|
|
115
|
+
slug: "post",
|
|
116
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
117
|
+
autoFormVersion: 2,
|
|
118
|
+
createdAt: new Date(),
|
|
119
|
+
updatedAt: new Date(),
|
|
120
|
+
},
|
|
121
|
+
})) as any;
|
|
122
|
+
|
|
123
|
+
await adapter.create({
|
|
124
|
+
model: "contentItem",
|
|
125
|
+
data: {
|
|
126
|
+
contentTypeId: ct.id,
|
|
127
|
+
slug: "first",
|
|
128
|
+
data: JSON.stringify({ title: "First" }),
|
|
129
|
+
createdAt: new Date(),
|
|
130
|
+
updatedAt: new Date(),
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
await adapter.create({
|
|
134
|
+
model: "contentItem",
|
|
135
|
+
data: {
|
|
136
|
+
contentTypeId: ct.id,
|
|
137
|
+
slug: "second",
|
|
138
|
+
data: JSON.stringify({ title: "Second" }),
|
|
139
|
+
createdAt: new Date(),
|
|
140
|
+
updatedAt: new Date(),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = await getAllContentItems(adapter, "post", {
|
|
145
|
+
slug: "first",
|
|
146
|
+
});
|
|
147
|
+
expect(result.items).toHaveLength(1);
|
|
148
|
+
expect(result.items[0]!.slug).toBe("first");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("getContentItemBySlug", () => {
|
|
153
|
+
it("returns null when content type does not exist", async () => {
|
|
154
|
+
const item = await getContentItemBySlug(adapter, "nonexistent", "item");
|
|
155
|
+
expect(item).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns null when item does not exist", async () => {
|
|
159
|
+
await adapter.create({
|
|
160
|
+
model: "contentType",
|
|
161
|
+
data: {
|
|
162
|
+
name: "Post",
|
|
163
|
+
slug: "post",
|
|
164
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
165
|
+
autoFormVersion: 2,
|
|
166
|
+
createdAt: new Date(),
|
|
167
|
+
updatedAt: new Date(),
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const item = await getContentItemBySlug(adapter, "post", "nonexistent");
|
|
172
|
+
expect(item).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("returns the serialized item when it exists", async () => {
|
|
176
|
+
const ct = (await adapter.create({
|
|
177
|
+
model: "contentType",
|
|
178
|
+
data: {
|
|
179
|
+
name: "Post",
|
|
180
|
+
slug: "post",
|
|
181
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
182
|
+
autoFormVersion: 2,
|
|
183
|
+
createdAt: new Date(),
|
|
184
|
+
updatedAt: new Date(),
|
|
185
|
+
},
|
|
186
|
+
})) as any;
|
|
187
|
+
|
|
188
|
+
await adapter.create({
|
|
189
|
+
model: "contentItem",
|
|
190
|
+
data: {
|
|
191
|
+
contentTypeId: ct.id,
|
|
192
|
+
slug: "hello",
|
|
193
|
+
data: JSON.stringify({ title: "Hello" }),
|
|
194
|
+
createdAt: new Date(),
|
|
195
|
+
updatedAt: new Date(),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const item = await getContentItemBySlug(adapter, "post", "hello");
|
|
200
|
+
expect(item).not.toBeNull();
|
|
201
|
+
expect(item!.slug).toBe("hello");
|
|
202
|
+
expect(item!.parsedData).toEqual({ title: "Hello" });
|
|
203
|
+
expect(typeof item!.createdAt).toBe("string");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { Adapter } from "@btst/db";
|
|
2
|
+
import type {
|
|
3
|
+
ContentType,
|
|
4
|
+
ContentItem,
|
|
5
|
+
ContentItemWithType,
|
|
6
|
+
SerializedContentType,
|
|
7
|
+
SerializedContentItem,
|
|
8
|
+
SerializedContentItemWithType,
|
|
9
|
+
} from "../types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Serialize a ContentType for SSR/SSG use (convert dates to strings).
|
|
13
|
+
* Applies lazy migration for legacy schemas (version 1 → 2).
|
|
14
|
+
*/
|
|
15
|
+
export function serializeContentType(ct: ContentType): SerializedContentType {
|
|
16
|
+
const needsMigration = !ct.autoFormVersion || ct.autoFormVersion < 2;
|
|
17
|
+
const migratedJsonSchema = needsMigration
|
|
18
|
+
? migrateToUnifiedSchema(ct.jsonSchema, ct.fieldConfig)
|
|
19
|
+
: ct.jsonSchema;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
id: ct.id,
|
|
23
|
+
name: ct.name,
|
|
24
|
+
slug: ct.slug,
|
|
25
|
+
description: ct.description,
|
|
26
|
+
jsonSchema: migratedJsonSchema,
|
|
27
|
+
createdAt: ct.createdAt.toISOString(),
|
|
28
|
+
updatedAt: ct.updatedAt.toISOString(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function migrateToUnifiedSchema(
|
|
33
|
+
jsonSchemaStr: string,
|
|
34
|
+
fieldConfigStr: string | null | undefined,
|
|
35
|
+
): string {
|
|
36
|
+
if (!fieldConfigStr) return jsonSchemaStr;
|
|
37
|
+
try {
|
|
38
|
+
const jsonSchema = JSON.parse(jsonSchemaStr);
|
|
39
|
+
const fieldConfig = JSON.parse(fieldConfigStr);
|
|
40
|
+
if (!jsonSchema.properties || typeof fieldConfig !== "object") {
|
|
41
|
+
return jsonSchemaStr;
|
|
42
|
+
}
|
|
43
|
+
for (const [key, config] of Object.entries(fieldConfig)) {
|
|
44
|
+
if (
|
|
45
|
+
jsonSchema.properties[key] &&
|
|
46
|
+
typeof config === "object" &&
|
|
47
|
+
config !== null &&
|
|
48
|
+
"fieldType" in config
|
|
49
|
+
) {
|
|
50
|
+
jsonSchema.properties[key].fieldType = (
|
|
51
|
+
config as { fieldType: string }
|
|
52
|
+
).fieldType;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return JSON.stringify(jsonSchema);
|
|
56
|
+
} catch {
|
|
57
|
+
return jsonSchemaStr;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Serialize a ContentItem for SSR/SSG use (convert dates to strings).
|
|
63
|
+
*/
|
|
64
|
+
export function serializeContentItem(item: ContentItem): SerializedContentItem {
|
|
65
|
+
return {
|
|
66
|
+
...item,
|
|
67
|
+
createdAt: item.createdAt.toISOString(),
|
|
68
|
+
updatedAt: item.updatedAt.toISOString(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Serialize a ContentItem with parsed data and joined ContentType.
|
|
74
|
+
* Throws a SyntaxError if `item.data` is not valid JSON, so corrupted rows
|
|
75
|
+
* produce a visible, debuggable error rather than silently returning null.
|
|
76
|
+
*/
|
|
77
|
+
export function serializeContentItemWithType(
|
|
78
|
+
item: ContentItemWithType,
|
|
79
|
+
): SerializedContentItemWithType {
|
|
80
|
+
const parsedData = JSON.parse(item.data) as Record<string, unknown>;
|
|
81
|
+
return {
|
|
82
|
+
...serializeContentItem(item),
|
|
83
|
+
parsedData,
|
|
84
|
+
contentType: item.contentType
|
|
85
|
+
? serializeContentType(item.contentType)
|
|
86
|
+
: undefined,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Retrieve all content types.
|
|
92
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
93
|
+
*
|
|
94
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
95
|
+
* responsible for any access-control checks before invoking this function.
|
|
96
|
+
*
|
|
97
|
+
* @param adapter - The database adapter
|
|
98
|
+
*/
|
|
99
|
+
export async function getAllContentTypes(
|
|
100
|
+
adapter: Adapter,
|
|
101
|
+
): Promise<SerializedContentType[]> {
|
|
102
|
+
const contentTypes = await adapter.findMany<ContentType>({
|
|
103
|
+
model: "contentType",
|
|
104
|
+
sortBy: { field: "name", direction: "asc" },
|
|
105
|
+
});
|
|
106
|
+
return contentTypes.map(serializeContentType);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Retrieve all content items for a given content type, with optional pagination.
|
|
111
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
112
|
+
*
|
|
113
|
+
* @remarks **Security:** Authorization hooks (e.g. `onBeforeListItems`) are NOT
|
|
114
|
+
* called. The caller is responsible for any access-control checks before
|
|
115
|
+
* invoking this function.
|
|
116
|
+
*
|
|
117
|
+
* @param adapter - The database adapter
|
|
118
|
+
* @param contentTypeSlug - The slug of the content type to query
|
|
119
|
+
* @param params - Optional filter/pagination parameters
|
|
120
|
+
*/
|
|
121
|
+
export async function getAllContentItems(
|
|
122
|
+
adapter: Adapter,
|
|
123
|
+
contentTypeSlug: string,
|
|
124
|
+
params?: { slug?: string; limit?: number; offset?: number },
|
|
125
|
+
): Promise<{
|
|
126
|
+
items: SerializedContentItemWithType[];
|
|
127
|
+
total: number;
|
|
128
|
+
limit?: number;
|
|
129
|
+
offset?: number;
|
|
130
|
+
}> {
|
|
131
|
+
const contentType = await adapter.findOne<ContentType>({
|
|
132
|
+
model: "contentType",
|
|
133
|
+
where: [
|
|
134
|
+
{
|
|
135
|
+
field: "slug",
|
|
136
|
+
value: contentTypeSlug,
|
|
137
|
+
operator: "eq" as const,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (!contentType) {
|
|
143
|
+
return {
|
|
144
|
+
items: [],
|
|
145
|
+
total: 0,
|
|
146
|
+
limit: params?.limit,
|
|
147
|
+
offset: params?.offset,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const whereConditions: Array<{
|
|
152
|
+
field: string;
|
|
153
|
+
value: string;
|
|
154
|
+
operator: "eq";
|
|
155
|
+
}> = [
|
|
156
|
+
{
|
|
157
|
+
field: "contentTypeId",
|
|
158
|
+
value: contentType.id,
|
|
159
|
+
operator: "eq" as const,
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
if (params?.slug) {
|
|
164
|
+
whereConditions.push({
|
|
165
|
+
field: "slug",
|
|
166
|
+
value: params.slug,
|
|
167
|
+
operator: "eq" as const,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// TODO: remove cast once @btst/db types expose adapter.count()
|
|
172
|
+
const total: number = await adapter.count({
|
|
173
|
+
model: "contentItem",
|
|
174
|
+
where: whereConditions,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const items = await adapter.findMany<ContentItemWithType>({
|
|
178
|
+
model: "contentItem",
|
|
179
|
+
where: whereConditions,
|
|
180
|
+
limit: params?.limit,
|
|
181
|
+
offset: params?.offset,
|
|
182
|
+
sortBy: { field: "createdAt", direction: "desc" },
|
|
183
|
+
join: { contentType: true },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
items: items.map(serializeContentItemWithType),
|
|
188
|
+
total,
|
|
189
|
+
limit: params?.limit,
|
|
190
|
+
offset: params?.offset,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Retrieve a single content item by its slug within a content type.
|
|
196
|
+
* Returns null if the content type or item is not found.
|
|
197
|
+
* Pure DB function — no hooks, no HTTP context. Safe for SSG and server-side use.
|
|
198
|
+
*
|
|
199
|
+
* @remarks **Security:** Authorization hooks are NOT called. The caller is
|
|
200
|
+
* responsible for any access-control checks before invoking this function.
|
|
201
|
+
*
|
|
202
|
+
* @param adapter - The database adapter
|
|
203
|
+
* @param contentTypeSlug - The slug of the content type
|
|
204
|
+
* @param slug - The slug of the content item
|
|
205
|
+
*/
|
|
206
|
+
export async function getContentItemBySlug(
|
|
207
|
+
adapter: Adapter,
|
|
208
|
+
contentTypeSlug: string,
|
|
209
|
+
slug: string,
|
|
210
|
+
): Promise<SerializedContentItemWithType | null> {
|
|
211
|
+
const contentType = await adapter.findOne<ContentType>({
|
|
212
|
+
model: "contentType",
|
|
213
|
+
where: [
|
|
214
|
+
{
|
|
215
|
+
field: "slug",
|
|
216
|
+
value: contentTypeSlug,
|
|
217
|
+
operator: "eq" as const,
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (!contentType) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const item = await adapter.findOne<ContentItemWithType>({
|
|
227
|
+
model: "contentItem",
|
|
228
|
+
where: [
|
|
229
|
+
{
|
|
230
|
+
field: "contentTypeId",
|
|
231
|
+
value: contentType.id,
|
|
232
|
+
operator: "eq" as const,
|
|
233
|
+
},
|
|
234
|
+
{ field: "slug", value: slug, operator: "eq" as const },
|
|
235
|
+
],
|
|
236
|
+
join: { contentType: true },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!item) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return serializeContentItemWithType(item);
|
|
244
|
+
}
|