@btst/stack 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.cts +2 -2
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.d.ts +2 -2
- 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/context/index.d.cts +1 -1
- package/dist/context/index.d.mts +1 -1
- package/dist/context/index.d.ts +1 -1
- 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/cms/api/getters.cjs +142 -6
- package/dist/packages/stack/src/plugins/cms/api/getters.mjs +139 -8
- package/dist/packages/stack/src/plugins/cms/api/operations.cjs +67 -61
- package/dist/packages/stack/src/plugins/cms/api/operations.mjs +69 -63
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +26 -8
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +27 -9
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +15 -1
- package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +14 -2
- package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.cjs +45 -24
- package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.mjs +45 -24
- package/dist/packages/stack/src/plugins/cms/schemas.cjs +22 -0
- package/dist/packages/stack/src/plugins/cms/schemas.mjs +20 -1
- package/dist/plugins/api/index.d.cts +2 -2
- package/dist/plugins/api/index.d.mts +2 -2
- package/dist/plugins/api/index.d.ts +2 -2
- 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 +2 -2
- package/dist/plugins/blog/client/index.d.mts +2 -2
- package/dist/plugins/blog/client/index.d.ts +2 -2
- 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/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 +1 -0
- package/dist/plugins/cms/api/index.d.cts +43 -11
- package/dist/plugins/cms/api/index.d.mts +43 -11
- package/dist/plugins/cms/api/index.d.ts +43 -11
- package/dist/plugins/cms/api/index.mjs +1 -1
- package/dist/plugins/cms/client/hooks/index.d.cts +90 -45
- package/dist/plugins/cms/client/hooks/index.d.mts +90 -45
- package/dist/plugins/cms/client/hooks/index.d.ts +90 -45
- package/dist/plugins/cms/client/index.d.cts +2 -1
- package/dist/plugins/cms/client/index.d.mts +2 -1
- package/dist/plugins/cms/client/index.d.ts +2 -1
- package/dist/plugins/cms/query-keys.cjs +69 -13
- package/dist/plugins/cms/query-keys.d.cts +89 -212
- package/dist/plugins/cms/query-keys.d.mts +89 -212
- package/dist/plugins/cms/query-keys.d.ts +89 -212
- package/dist/plugins/cms/query-keys.mjs +70 -14
- 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/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/client/hooks/index.d.cts +3 -3
- package/dist/plugins/ui-builder/client/hooks/index.d.mts +3 -3
- package/dist/plugins/ui-builder/client/hooks/index.d.ts +3 -3
- package/dist/plugins/ui-builder/client/index.d.cts +3 -3
- package/dist/plugins/ui-builder/client/index.d.mts +3 -3
- package/dist/plugins/ui-builder/client/index.d.ts +3 -3
- package/dist/plugins/ui-builder/query-keys.d.cts +4 -3
- package/dist/plugins/ui-builder/query-keys.d.mts +4 -3
- package/dist/plugins/ui-builder/query-keys.d.ts +4 -3
- package/dist/shared/{stack.DMj68qY0.d.mts → stack.B2jlVVhD.d.mts} +5 -5
- package/dist/shared/{stack.Cvnj99cs.d.ts → stack.BB9tubJJ.d.ts} +9 -3
- package/dist/shared/{stack.Bt3WG2yb.d.cts → stack.BWduIMKG.d.cts} +35 -35
- package/dist/shared/stack.Bw09Lg2z.d.ts +1420 -0
- package/dist/shared/{stack.CkEz7vSt.d.mts → stack.C2Pvl-RS.d.mts} +9 -3
- package/dist/shared/{stack.C2l2wd_X.d.ts → stack.Ch9-ppmQ.d.ts} +35 -35
- package/dist/shared/{stack.Ht8mhDj5.d.cts → stack.D17yFglU.d.cts} +5 -5
- package/dist/shared/{stack.CiD115Ov.d.mts → stack.D6fPBfYe.d.mts} +35 -35
- package/dist/shared/stack.DFGj0gb7.d.mts +1420 -0
- package/dist/shared/{stack.pi0FxoEa.d.ts → stack.Dcfg4GNN.d.ts} +5 -5
- package/dist/shared/{stack.3rxARhgS.d.cts → stack.DrB3L7Ih.d.cts} +52 -3
- package/dist/shared/{stack.3rxARhgS.d.mts → stack.DrB3L7Ih.d.mts} +52 -3
- package/dist/shared/{stack.3rxARhgS.d.ts → stack.DrB3L7Ih.d.ts} +52 -3
- package/dist/shared/stack.NNrBKBkM.d.cts +1420 -0
- package/dist/shared/{stack.mRRyZBUg.d.cts → stack.Rb87lHqK.d.cts} +9 -3
- package/package.json +1 -1
- package/src/__tests__/cms-query-keys.test.ts +57 -0
- package/src/plugins/api/index.ts +1 -0
- package/src/plugins/api/operation.ts +16 -3
- package/src/plugins/cms/__tests__/authorization.test.ts +652 -187
- package/src/plugins/cms/__tests__/client-runtime.browser.test.tsx +655 -0
- package/src/plugins/cms/__tests__/content-read.typecheck.ts +351 -0
- package/src/plugins/cms/__tests__/getters.test.ts +286 -0
- package/src/plugins/cms/api/getters.ts +270 -12
- package/src/plugins/cms/api/index.ts +10 -0
- package/src/plugins/cms/api/operations.ts +163 -75
- package/src/plugins/cms/api/plugin.ts +50 -12
- package/src/plugins/cms/api/query-key-defs.ts +29 -2
- package/src/plugins/cms/client/hooks/cms-hooks.tsx +400 -91
- package/src/plugins/cms/client/index.ts +9 -0
- package/src/plugins/cms/query-keys.ts +171 -16
- package/src/plugins/cms/schemas.ts +30 -0
- package/src/plugins/cms/types.ts +49 -2
- package/src/plugins/media/__tests__/identity-partition.test.tsx +26 -16
- package/dist/shared/stack.CAmVUwjV.d.cts +0 -764
- package/dist/shared/stack.CQK02ujt.d.ts +0 -764
- package/dist/shared/stack.D9x1fR5a.d.mts +0 -764
- package/dist/shared/{stack.CKc_Bais.d.cts → stack.Csi-4Mgz.d.cts} +7 -7
- package/dist/shared/{stack.CKc_Bais.d.mts → stack.Csi-4Mgz.d.mts} +7 -7
- package/dist/shared/{stack.CKc_Bais.d.ts → stack.Csi-4Mgz.d.ts} +7 -7
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { DBAdapter } from "@btst/db";
|
|
2
|
+
import { createBackendStack } from "../../../api";
|
|
3
|
+
import {
|
|
4
|
+
cmsBackendPlugin,
|
|
5
|
+
getContentItemById,
|
|
6
|
+
getContentItemBySlug,
|
|
7
|
+
getAllContentItems,
|
|
8
|
+
getContentItemsByRelation,
|
|
9
|
+
type CMSReadOptions,
|
|
10
|
+
} from "../api";
|
|
11
|
+
import {
|
|
12
|
+
useContent,
|
|
13
|
+
useContentOptions,
|
|
14
|
+
useContentItem,
|
|
15
|
+
useContentItemBySlug,
|
|
16
|
+
useSuspenseContent,
|
|
17
|
+
useSuspenseContentItem,
|
|
18
|
+
useContentByRelation,
|
|
19
|
+
useSuspenseContentByRelation,
|
|
20
|
+
useInverseRelationItems,
|
|
21
|
+
} from "../client/hooks";
|
|
22
|
+
import { createCMSQueryKeys } from "../query-keys";
|
|
23
|
+
import { createApiClient } from "../../client";
|
|
24
|
+
import type { CMSApiRouter } from "../api";
|
|
25
|
+
|
|
26
|
+
async function cmsContentReadTypes(
|
|
27
|
+
adapter: DBAdapter,
|
|
28
|
+
request: Request,
|
|
29
|
+
options: CMSReadOptions,
|
|
30
|
+
) {
|
|
31
|
+
const backend = createBackendStack({
|
|
32
|
+
basePath: "/api",
|
|
33
|
+
adapter: () => adapter,
|
|
34
|
+
plugins: { cms: cmsBackendPlugin({ contentTypes: [] }) },
|
|
35
|
+
});
|
|
36
|
+
const selected = await backend.trusted.cms.getContentItem({
|
|
37
|
+
typeSlug: "article",
|
|
38
|
+
id: "id",
|
|
39
|
+
view: "content",
|
|
40
|
+
fields: ["title"] as const,
|
|
41
|
+
});
|
|
42
|
+
selected.parsedData.title satisfies unknown;
|
|
43
|
+
// @ts-expect-error A literal selection excludes other content properties.
|
|
44
|
+
selected.parsedData.body;
|
|
45
|
+
const selectedList = await backend.trusted.cms.listContentItems({
|
|
46
|
+
typeSlug: "article",
|
|
47
|
+
query: { view: "content", fields: ["title"] as const },
|
|
48
|
+
});
|
|
49
|
+
// @ts-expect-error Projected list operations exclude unselected properties.
|
|
50
|
+
selectedList.items[0]?.parsedData.body;
|
|
51
|
+
const selectedGetter = await getContentItemById(adapter, "id", {
|
|
52
|
+
view: "content",
|
|
53
|
+
fields: ["title"],
|
|
54
|
+
});
|
|
55
|
+
// @ts-expect-error Getters preserve the selected property set.
|
|
56
|
+
selectedGetter?.parsedData.body;
|
|
57
|
+
const selectedSlug = await getContentItemBySlug(adapter, "article", "slug", {
|
|
58
|
+
view: "content",
|
|
59
|
+
fields: ["title"],
|
|
60
|
+
});
|
|
61
|
+
// @ts-expect-error Slug getter selections stay literal.
|
|
62
|
+
selectedSlug?.parsedData.body;
|
|
63
|
+
const selectedRelated = await getContentItemsByRelation(adapter, "article", {
|
|
64
|
+
field: "authorId",
|
|
65
|
+
targetId: "author-1",
|
|
66
|
+
view: "content",
|
|
67
|
+
fields: ["title"],
|
|
68
|
+
});
|
|
69
|
+
// @ts-expect-error Related getters exclude unselected properties.
|
|
70
|
+
selectedRelated.items[0]?.parsedData.body;
|
|
71
|
+
const full = await backend.trusted.cms.listContentItems({
|
|
72
|
+
typeSlug: "article",
|
|
73
|
+
query: {},
|
|
74
|
+
});
|
|
75
|
+
full.items[0]!.data satisfies string;
|
|
76
|
+
const content = await backend.trusted.cms.listContentItems({
|
|
77
|
+
typeSlug: "article",
|
|
78
|
+
query: { view: "content" },
|
|
79
|
+
});
|
|
80
|
+
content.items[0]!.slug satisfies string;
|
|
81
|
+
// @ts-expect-error Content view omits duplicated raw JSON.
|
|
82
|
+
content.items[0]!.data;
|
|
83
|
+
const item = await backend.forRequest(request).operations.cms.getContentItem({
|
|
84
|
+
typeSlug: "article",
|
|
85
|
+
id: "id",
|
|
86
|
+
view: "content",
|
|
87
|
+
});
|
|
88
|
+
// @ts-expect-error Content view omits schema and editor metadata.
|
|
89
|
+
item.contentType;
|
|
90
|
+
const defaultItem = await backend.trusted.cms.getContentItem({
|
|
91
|
+
typeSlug: "article",
|
|
92
|
+
id: "id",
|
|
93
|
+
});
|
|
94
|
+
defaultItem.data satisfies string;
|
|
95
|
+
const dynamic = await backend.trusted.cms.getContentItem({
|
|
96
|
+
typeSlug: "article",
|
|
97
|
+
id: "id",
|
|
98
|
+
...options,
|
|
99
|
+
});
|
|
100
|
+
// @ts-expect-error A dynamic output view cannot promise raw JSON.
|
|
101
|
+
dynamic.data;
|
|
102
|
+
const getterItem = await getContentItemById(adapter, "id", {
|
|
103
|
+
view: "content",
|
|
104
|
+
});
|
|
105
|
+
// @ts-expect-error Adapter getters have the same reduced envelope.
|
|
106
|
+
getterItem?.authorId;
|
|
107
|
+
(await getContentItemBySlug(adapter, "article", "slug"))?.data satisfies
|
|
108
|
+
| string
|
|
109
|
+
| undefined;
|
|
110
|
+
const getterList = await getAllContentItems(adapter, "article", {
|
|
111
|
+
view: "content",
|
|
112
|
+
});
|
|
113
|
+
// @ts-expect-error Content list getters omit duplicated raw JSON.
|
|
114
|
+
getterList.items[0]?.data;
|
|
115
|
+
const queries = createCMSQueryKeys(
|
|
116
|
+
createApiClient<CMSApiRouter>({
|
|
117
|
+
baseURL: "https://example.com",
|
|
118
|
+
basePath: "/api",
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
const selectedQuery = await queries.cmsContent
|
|
122
|
+
.detail("article", "id", { view: "content", fields: ["title"] })
|
|
123
|
+
.queryFn();
|
|
124
|
+
// @ts-expect-error Query factories expose selected content only.
|
|
125
|
+
selectedQuery?.parsedData.body;
|
|
126
|
+
const selectedRelationQuery = await queries.cmsContent
|
|
127
|
+
.byRelation({
|
|
128
|
+
typeSlug: "article",
|
|
129
|
+
field: "authorId",
|
|
130
|
+
targetId: "author-1",
|
|
131
|
+
view: "content",
|
|
132
|
+
fields: ["title"],
|
|
133
|
+
})
|
|
134
|
+
.queryFn();
|
|
135
|
+
// @ts-expect-error Relation query factories keep selected content types.
|
|
136
|
+
selectedRelationQuery.items[0]?.parsedData.body;
|
|
137
|
+
const selectedOptions = await queries.cmsContent
|
|
138
|
+
.options({ typeSlug: "article", view: "content", fields: ["title"] })
|
|
139
|
+
.queryFn();
|
|
140
|
+
// @ts-expect-error Picker query factories omit unselected properties.
|
|
141
|
+
selectedOptions[0]?.parsedData.body;
|
|
142
|
+
(await queries.cmsContent.list({ typeSlug: "article" }).queryFn()).items[0]!
|
|
143
|
+
.data satisfies string;
|
|
144
|
+
const contentPage = await queries.cmsContent
|
|
145
|
+
.list({ typeSlug: "article", view: "content" })
|
|
146
|
+
.queryFn();
|
|
147
|
+
// @ts-expect-error Content query factories omit the editor envelope too.
|
|
148
|
+
contentPage.items[0]?.contentType;
|
|
149
|
+
const queryItem = await queries.cmsContent
|
|
150
|
+
.detail("article", "id", { view: "content" })
|
|
151
|
+
.queryFn();
|
|
152
|
+
// @ts-expect-error Content detail query factories omit duplicated raw JSON.
|
|
153
|
+
queryItem?.data;
|
|
154
|
+
const querySlug = await queries.cmsContent
|
|
155
|
+
.bySlug("article", "slug", { view: "content" })
|
|
156
|
+
.queryFn();
|
|
157
|
+
// @ts-expect-error Content slug query factories omit duplicated raw JSON.
|
|
158
|
+
querySlug?.data;
|
|
159
|
+
const relationParams = { field: "authorId", targetId: "author-1" };
|
|
160
|
+
(
|
|
161
|
+
await backend.trusted.cms.listContentByRelation({
|
|
162
|
+
typeSlug: "article",
|
|
163
|
+
query: relationParams,
|
|
164
|
+
})
|
|
165
|
+
).items[0]!.data satisfies string;
|
|
166
|
+
const related = await backend
|
|
167
|
+
.forRequest(request)
|
|
168
|
+
.operations.cms.listContentByRelation({
|
|
169
|
+
typeSlug: "article",
|
|
170
|
+
query: { ...relationParams, view: "content" },
|
|
171
|
+
});
|
|
172
|
+
// @ts-expect-error Content relation operations omit editor metadata.
|
|
173
|
+
related.items[0]?.contentType;
|
|
174
|
+
const inverseParams = { itemId: "author-1", fieldName: "authorId" };
|
|
175
|
+
(
|
|
176
|
+
await backend.trusted.cms.listInverseRelationItems({
|
|
177
|
+
slug: "author",
|
|
178
|
+
sourceType: "article",
|
|
179
|
+
query: inverseParams,
|
|
180
|
+
})
|
|
181
|
+
).items[0]!.data satisfies string;
|
|
182
|
+
const inverse = await backend.trusted.cms.listInverseRelationItems({
|
|
183
|
+
slug: "author",
|
|
184
|
+
sourceType: "article",
|
|
185
|
+
query: { ...inverseParams, view: "content" },
|
|
186
|
+
});
|
|
187
|
+
// @ts-expect-error Content inverse operations omit raw JSON.
|
|
188
|
+
inverse.items[0]?.data;
|
|
189
|
+
const getterRelated = await getContentItemsByRelation(adapter, "article", {
|
|
190
|
+
...relationParams,
|
|
191
|
+
view: "content",
|
|
192
|
+
});
|
|
193
|
+
// @ts-expect-error Content relation getters omit raw JSON.
|
|
194
|
+
getterRelated.items[0]?.data;
|
|
195
|
+
(await getContentItemsByRelation(adapter, "article", relationParams))
|
|
196
|
+
.items[0]!.data satisfies string;
|
|
197
|
+
const relatedQuery = await queries.cmsContent
|
|
198
|
+
.byRelation({ typeSlug: "article", ...relationParams, view: "content" })
|
|
199
|
+
.queryFn();
|
|
200
|
+
// @ts-expect-error Content relation query factories omit raw JSON.
|
|
201
|
+
relatedQuery.items[0]?.data;
|
|
202
|
+
(
|
|
203
|
+
await queries.cmsContent
|
|
204
|
+
.byRelation({ typeSlug: "article", ...relationParams })
|
|
205
|
+
.queryFn()
|
|
206
|
+
).items[0]!.data satisfies string;
|
|
207
|
+
const inverseQuery = await queries.cmsTypes
|
|
208
|
+
.inverseRelationItems({
|
|
209
|
+
slug: "author",
|
|
210
|
+
sourceType: "article",
|
|
211
|
+
...inverseParams,
|
|
212
|
+
view: "content",
|
|
213
|
+
})
|
|
214
|
+
.queryFn();
|
|
215
|
+
// @ts-expect-error Content inverse query factories omit raw JSON.
|
|
216
|
+
inverseQuery.items[0]?.data;
|
|
217
|
+
(
|
|
218
|
+
await queries.cmsTypes
|
|
219
|
+
.inverseRelationItems({
|
|
220
|
+
slug: "author",
|
|
221
|
+
sourceType: "article",
|
|
222
|
+
...inverseParams,
|
|
223
|
+
})
|
|
224
|
+
.queryFn()
|
|
225
|
+
).items[0]!.data satisfies string;
|
|
226
|
+
}
|
|
227
|
+
void cmsContentReadTypes;
|
|
228
|
+
|
|
229
|
+
function useCMSContentReadTypes() {
|
|
230
|
+
type Types = { article: { title: string; author: string } };
|
|
231
|
+
useContent<Types, "article">("article").items[0]!.data satisfies string;
|
|
232
|
+
const list = useContent<Types, "article">("article", { view: "content" });
|
|
233
|
+
list.items[0]!.parsedData.author satisfies string;
|
|
234
|
+
// @ts-expect-error Content hooks omit editor metadata with an explicit data map too.
|
|
235
|
+
list.items[0]!.contentTypeId;
|
|
236
|
+
const item = useContentItem<Types, "article">("article", "id", {
|
|
237
|
+
view: "content",
|
|
238
|
+
});
|
|
239
|
+
item.item?.parsedData.title satisfies string | undefined;
|
|
240
|
+
// @ts-expect-error Content detail hooks omit raw JSON.
|
|
241
|
+
item.item?.data;
|
|
242
|
+
// @ts-expect-error Content slug hooks omit raw JSON.
|
|
243
|
+
useContentItemBySlug("article", "slug", { view: "content" }).item?.data;
|
|
244
|
+
// @ts-expect-error Suspense content list hooks omit raw JSON.
|
|
245
|
+
useSuspenseContent("article", { view: "content" }).items[0]?.data;
|
|
246
|
+
// @ts-expect-error Suspense content detail hooks omit raw JSON.
|
|
247
|
+
useSuspenseContentItem("article", "id", { view: "content" }).item?.data;
|
|
248
|
+
useContentByRelation<Types, "article">("article", "authorId", "author-1")
|
|
249
|
+
.items[0]!.data satisfies string;
|
|
250
|
+
const related = useContentByRelation<Types, "article">(
|
|
251
|
+
"article",
|
|
252
|
+
"authorId",
|
|
253
|
+
"author-1",
|
|
254
|
+
{ view: "content" },
|
|
255
|
+
);
|
|
256
|
+
related.items[0]!.parsedData.author satisfies string;
|
|
257
|
+
// @ts-expect-error Content relation hooks omit editor metadata with a data map.
|
|
258
|
+
related.items[0]?.contentTypeId;
|
|
259
|
+
const suspenseRelated = useSuspenseContentByRelation(
|
|
260
|
+
"article",
|
|
261
|
+
"authorId",
|
|
262
|
+
"author-1",
|
|
263
|
+
{
|
|
264
|
+
view: "content",
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
// @ts-expect-error Suspense relation hooks omit raw JSON.
|
|
268
|
+
suspenseRelated.items[0]?.data;
|
|
269
|
+
const inverse = useInverseRelationItems(
|
|
270
|
+
{
|
|
271
|
+
contentTypeSlug: "author",
|
|
272
|
+
sourceType: "article",
|
|
273
|
+
itemId: "author-1",
|
|
274
|
+
fieldName: "authorId",
|
|
275
|
+
},
|
|
276
|
+
{ view: "content" },
|
|
277
|
+
);
|
|
278
|
+
// @ts-expect-error Content inverse hooks omit raw JSON.
|
|
279
|
+
inverse.items[0]?.data;
|
|
280
|
+
}
|
|
281
|
+
function useCMSSelectedContentTypes() {
|
|
282
|
+
type Types = { article: { title: string; body: string; optional?: string } };
|
|
283
|
+
const inferred = useContentItem("article", "id", {
|
|
284
|
+
view: "content",
|
|
285
|
+
fields: ["title"],
|
|
286
|
+
});
|
|
287
|
+
// @ts-expect-error Literal selections omit unrequested properties without new schema inference.
|
|
288
|
+
inferred.item?.parsedData.body;
|
|
289
|
+
const typed = useContent<Types, "article", readonly ["title", "optional"]>(
|
|
290
|
+
"article",
|
|
291
|
+
{ view: "content", fields: ["title", "optional"] },
|
|
292
|
+
);
|
|
293
|
+
typed.items[0]?.parsedData.title satisfies string | undefined;
|
|
294
|
+
typed.items[0]?.parsedData.optional satisfies string | undefined;
|
|
295
|
+
// @ts-expect-error Explicit data-map selection tuples preserve omitted fields.
|
|
296
|
+
typed.items[0]?.parsedData.body;
|
|
297
|
+
const dynamicFields: string[] = ["title"];
|
|
298
|
+
const dynamic = useContent<Types, "article">("article", {
|
|
299
|
+
view: "content",
|
|
300
|
+
fields: dynamicFields,
|
|
301
|
+
});
|
|
302
|
+
dynamic.items[0]?.parsedData.title satisfies string | undefined;
|
|
303
|
+
// @ts-expect-error Dynamic arrays cannot promise any field is returned.
|
|
304
|
+
dynamic.items[0]!.parsedData.title satisfies string;
|
|
305
|
+
const related = useContentByRelation("article", "authorId", "author-1", {
|
|
306
|
+
view: "content",
|
|
307
|
+
fields: ["title"],
|
|
308
|
+
});
|
|
309
|
+
// @ts-expect-error Direct relation selections omit unrequested properties.
|
|
310
|
+
related.items[0]?.parsedData.body;
|
|
311
|
+
const inverse = useInverseRelationItems(
|
|
312
|
+
{
|
|
313
|
+
contentTypeSlug: "author",
|
|
314
|
+
sourceType: "article",
|
|
315
|
+
itemId: "author-1",
|
|
316
|
+
fieldName: "authorId",
|
|
317
|
+
},
|
|
318
|
+
{ view: "content", fields: ["title"] },
|
|
319
|
+
);
|
|
320
|
+
// @ts-expect-error Inverse relation selections omit unrequested properties.
|
|
321
|
+
inverse.items[0]?.parsedData.body;
|
|
322
|
+
}
|
|
323
|
+
void useCMSSelectedContentTypes;
|
|
324
|
+
function useCMSPickerTypes() {
|
|
325
|
+
const full = useContentOptions({
|
|
326
|
+
targetType: "article",
|
|
327
|
+
value: [],
|
|
328
|
+
getOptionLabel: (item) => item.data,
|
|
329
|
+
});
|
|
330
|
+
full.options[0]?.item?.data satisfies string | undefined;
|
|
331
|
+
const selected = useContentOptions({
|
|
332
|
+
targetType: "article",
|
|
333
|
+
value: [],
|
|
334
|
+
view: "content",
|
|
335
|
+
fields: ["title"],
|
|
336
|
+
getOptionLabel: (item) => {
|
|
337
|
+
// @ts-expect-error A projected option label cannot read editor metadata.
|
|
338
|
+
item.data;
|
|
339
|
+
// @ts-expect-error A projected option label cannot read unselected content.
|
|
340
|
+
item.parsedData.body;
|
|
341
|
+
return String(item.parsedData.title);
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
// @ts-expect-error Projected selected option records omit metadata too.
|
|
345
|
+
selected.selectedOptions[0]?.item?.data;
|
|
346
|
+
// @ts-expect-error Selected-ID preloads retain the projection's field type.
|
|
347
|
+
selected.selectedOptions[0]?.item?.parsedData.body;
|
|
348
|
+
}
|
|
349
|
+
void useCMSPickerTypes;
|
|
350
|
+
|
|
351
|
+
void useCMSContentReadTypes;
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
getAllContentTypes,
|
|
8
8
|
getAllContentItems,
|
|
9
9
|
getContentItemBySlug,
|
|
10
|
+
getContentItemById,
|
|
11
|
+
getContentItemsByRelation,
|
|
10
12
|
} from "../api/getters";
|
|
13
|
+
import type { ContentItem, ContentType } from "../types";
|
|
11
14
|
|
|
12
15
|
const createTestAdapter = (): Adapter => {
|
|
13
16
|
const db = defineDb({}).use(cmsSchema);
|
|
@@ -29,6 +32,289 @@ describe("cms getters", () => {
|
|
|
29
32
|
adapter = createTestAdapter();
|
|
30
33
|
});
|
|
31
34
|
|
|
35
|
+
it("supports content-only list, ID and slug reads without changing defaults or errors", async () => {
|
|
36
|
+
const contentType = await adapter.create<{ id: string }>({
|
|
37
|
+
model: "contentType",
|
|
38
|
+
data: {
|
|
39
|
+
name: "Article",
|
|
40
|
+
slug: "article",
|
|
41
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
42
|
+
autoFormVersion: 2,
|
|
43
|
+
createdAt: new Date(),
|
|
44
|
+
updatedAt: new Date(),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const now = new Date("2026-01-01T00:00:00.000Z");
|
|
48
|
+
const item = await adapter.create<{ id: string }>({
|
|
49
|
+
model: "contentItem",
|
|
50
|
+
data: {
|
|
51
|
+
contentTypeId: contentType.id,
|
|
52
|
+
slug: "content-only",
|
|
53
|
+
data: JSON.stringify({
|
|
54
|
+
title: "Article",
|
|
55
|
+
author: "Visible content author",
|
|
56
|
+
nested: { tags: ["one"] },
|
|
57
|
+
}),
|
|
58
|
+
authorId: "private-author",
|
|
59
|
+
createdAt: now,
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
const expected = {
|
|
64
|
+
id: item.id,
|
|
65
|
+
slug: "content-only",
|
|
66
|
+
parsedData: {
|
|
67
|
+
title: "Article",
|
|
68
|
+
author: "Visible content author",
|
|
69
|
+
nested: { tags: ["one"] },
|
|
70
|
+
},
|
|
71
|
+
createdAt: now.toISOString(),
|
|
72
|
+
updatedAt: now.toISOString(),
|
|
73
|
+
};
|
|
74
|
+
expect(
|
|
75
|
+
await getContentItemById(adapter, item.id, { view: "content" }),
|
|
76
|
+
).toEqual(expected);
|
|
77
|
+
expect(
|
|
78
|
+
await getContentItemBySlug(adapter, "article", "content-only", {
|
|
79
|
+
view: "content",
|
|
80
|
+
}),
|
|
81
|
+
).toEqual(expected);
|
|
82
|
+
expect(
|
|
83
|
+
(
|
|
84
|
+
await getAllContentItems(adapter, "article", {
|
|
85
|
+
view: "content",
|
|
86
|
+
search: "Visible content author",
|
|
87
|
+
})
|
|
88
|
+
).items,
|
|
89
|
+
).toEqual([expected]);
|
|
90
|
+
const selection = { view: "content", fields: ["title"] } as const;
|
|
91
|
+
expect(
|
|
92
|
+
(await getContentItemById(adapter, item.id, selection))?.parsedData,
|
|
93
|
+
).toEqual({ title: "Article" });
|
|
94
|
+
expect(
|
|
95
|
+
(
|
|
96
|
+
await getContentItemBySlug(
|
|
97
|
+
adapter,
|
|
98
|
+
"article",
|
|
99
|
+
"content-only",
|
|
100
|
+
selection,
|
|
101
|
+
)
|
|
102
|
+
)?.parsedData,
|
|
103
|
+
).toEqual({ title: "Article" });
|
|
104
|
+
expect(
|
|
105
|
+
(
|
|
106
|
+
await getAllContentItems(adapter, "article", {
|
|
107
|
+
...selection,
|
|
108
|
+
search: "Visible content author",
|
|
109
|
+
})
|
|
110
|
+
).items[0]?.parsedData,
|
|
111
|
+
).toEqual({ title: "Article" });
|
|
112
|
+
await expect(
|
|
113
|
+
getAllContentItems(adapter, "article", {
|
|
114
|
+
view: "content",
|
|
115
|
+
fields: ["unknown"],
|
|
116
|
+
offset: 100,
|
|
117
|
+
}),
|
|
118
|
+
).rejects.toThrow("Unknown top-level");
|
|
119
|
+
await expect(
|
|
120
|
+
getContentItemById(adapter, item.id, { fields: ["title"] }),
|
|
121
|
+
).rejects.toThrow("requires view");
|
|
122
|
+
await expect(
|
|
123
|
+
getContentItemBySlug(adapter, "article", "content-only", {
|
|
124
|
+
view: "content",
|
|
125
|
+
fields: [],
|
|
126
|
+
}),
|
|
127
|
+
).rejects.toThrow();
|
|
128
|
+
expect(await getContentItemById(adapter, item.id)).toHaveProperty("data");
|
|
129
|
+
expect(
|
|
130
|
+
await getContentItemById(adapter, "missing", { view: "content" }),
|
|
131
|
+
).toBeNull();
|
|
132
|
+
expect(
|
|
133
|
+
await getContentItemBySlug(adapter, "missing", "content-only", {
|
|
134
|
+
view: "content",
|
|
135
|
+
}),
|
|
136
|
+
).toBeNull();
|
|
137
|
+
await adapter.update({
|
|
138
|
+
model: "contentType",
|
|
139
|
+
where: [{ field: "id", value: contentType.id }],
|
|
140
|
+
update: {
|
|
141
|
+
jsonSchema: JSON.stringify({
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: Object.fromEntries([
|
|
144
|
+
["__proto__", { type: "object" }],
|
|
145
|
+
["title", { type: "string" }],
|
|
146
|
+
]),
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
await adapter.update({
|
|
151
|
+
model: "contentItem",
|
|
152
|
+
where: [{ field: "id", value: item.id }],
|
|
153
|
+
update: {
|
|
154
|
+
data: JSON.stringify(
|
|
155
|
+
Object.fromEntries([
|
|
156
|
+
["__proto__", { safe: true }],
|
|
157
|
+
["title", "Article"],
|
|
158
|
+
]),
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const literalPrototype = await getContentItemById(adapter, item.id, {
|
|
163
|
+
view: "content",
|
|
164
|
+
fields: ["__proto__"],
|
|
165
|
+
});
|
|
166
|
+
expect(Object.hasOwn(literalPrototype!.parsedData, "__proto__")).toBe(true);
|
|
167
|
+
expect(Object.getPrototypeOf(literalPrototype!.parsedData)).toBe(
|
|
168
|
+
Object.prototype,
|
|
169
|
+
);
|
|
170
|
+
expect(
|
|
171
|
+
Object.getOwnPropertyDescriptor(literalPrototype!.parsedData, "__proto__")
|
|
172
|
+
?.value,
|
|
173
|
+
).toEqual({ safe: true });
|
|
174
|
+
await adapter.update({
|
|
175
|
+
model: "contentItem",
|
|
176
|
+
where: [{ field: "id", value: item.id, operator: "eq" }],
|
|
177
|
+
update: { data: "{" },
|
|
178
|
+
});
|
|
179
|
+
await expect(
|
|
180
|
+
getContentItemById(adapter, item.id, { view: "content" }),
|
|
181
|
+
).rejects.toThrow(SyntaxError);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("reads related pages with default pagination and preserves returned corruption errors", async () => {
|
|
185
|
+
const params = { field: "categoryId", targetId: "category-1" };
|
|
186
|
+
expect(await getContentItemsByRelation(adapter, "missing", params)).toEqual(
|
|
187
|
+
{ items: [], total: 0, limit: 20, offset: 0 },
|
|
188
|
+
);
|
|
189
|
+
const type = await adapter.create<ContentType>({
|
|
190
|
+
model: "contentType",
|
|
191
|
+
data: {
|
|
192
|
+
name: "Article",
|
|
193
|
+
slug: "article",
|
|
194
|
+
jsonSchema: SIMPLE_SCHEMA,
|
|
195
|
+
createdAt: new Date(),
|
|
196
|
+
updatedAt: new Date(),
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
expect(await getContentItemsByRelation(adapter, "article", params)).toEqual(
|
|
200
|
+
{ items: [], total: 0, limit: 20, offset: 0 },
|
|
201
|
+
);
|
|
202
|
+
for (const [id, date] of [
|
|
203
|
+
["older", "2026-01-01"],
|
|
204
|
+
["newer", "2026-01-02"],
|
|
205
|
+
] as const) {
|
|
206
|
+
await adapter.create<ContentItem>({
|
|
207
|
+
model: "contentItem",
|
|
208
|
+
forceAllowId: true,
|
|
209
|
+
data: {
|
|
210
|
+
id,
|
|
211
|
+
contentTypeId: type.id,
|
|
212
|
+
slug: id,
|
|
213
|
+
data: JSON.stringify({ title: id }),
|
|
214
|
+
createdAt: new Date(date),
|
|
215
|
+
updatedAt: new Date(date),
|
|
216
|
+
} as ContentItem,
|
|
217
|
+
});
|
|
218
|
+
await adapter.create({
|
|
219
|
+
model: "contentRelation",
|
|
220
|
+
data: {
|
|
221
|
+
sourceId: id,
|
|
222
|
+
targetId: params.targetId,
|
|
223
|
+
fieldName: params.field,
|
|
224
|
+
createdAt: new Date(),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
expect(
|
|
229
|
+
(
|
|
230
|
+
await getContentItemsByRelation(adapter, "article", {
|
|
231
|
+
...params,
|
|
232
|
+
view: "content",
|
|
233
|
+
fields: ["title"],
|
|
234
|
+
limit: 1,
|
|
235
|
+
})
|
|
236
|
+
).items[0]?.parsedData,
|
|
237
|
+
).toEqual({ title: "newer" });
|
|
238
|
+
await expect(
|
|
239
|
+
getContentItemsByRelation(adapter, "article", {
|
|
240
|
+
...params,
|
|
241
|
+
view: "content",
|
|
242
|
+
fields: ["unknown"],
|
|
243
|
+
offset: 100,
|
|
244
|
+
}),
|
|
245
|
+
).rejects.toThrow("Unknown top-level");
|
|
246
|
+
const first = await getContentItemsByRelation(adapter, "article", {
|
|
247
|
+
...params,
|
|
248
|
+
limit: 1,
|
|
249
|
+
});
|
|
250
|
+
expect(first).toMatchObject({
|
|
251
|
+
items: [
|
|
252
|
+
{
|
|
253
|
+
id: "newer",
|
|
254
|
+
parsedData: { title: "newer" },
|
|
255
|
+
createdAt: "2026-01-02T00:00:00.000Z",
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
total: 2,
|
|
259
|
+
limit: 1,
|
|
260
|
+
offset: 0,
|
|
261
|
+
});
|
|
262
|
+
expect(
|
|
263
|
+
await getContentItemsByRelation(adapter, "article", {
|
|
264
|
+
...params,
|
|
265
|
+
limit: 1,
|
|
266
|
+
view: "content",
|
|
267
|
+
}),
|
|
268
|
+
).toEqual({
|
|
269
|
+
items: [
|
|
270
|
+
{
|
|
271
|
+
id: "newer",
|
|
272
|
+
slug: "newer",
|
|
273
|
+
parsedData: { title: "newer" },
|
|
274
|
+
createdAt: "2026-01-02T00:00:00.000Z",
|
|
275
|
+
updatedAt: "2026-01-02T00:00:00.000Z",
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
total: 2,
|
|
279
|
+
limit: 1,
|
|
280
|
+
offset: 0,
|
|
281
|
+
});
|
|
282
|
+
await adapter.update({
|
|
283
|
+
model: "contentItem",
|
|
284
|
+
where: [{ field: "id", value: "older" }],
|
|
285
|
+
update: { data: "invalid json" },
|
|
286
|
+
});
|
|
287
|
+
expect(
|
|
288
|
+
(
|
|
289
|
+
await getContentItemsByRelation(adapter, "article", {
|
|
290
|
+
...params,
|
|
291
|
+
limit: 1,
|
|
292
|
+
})
|
|
293
|
+
).items[0]?.id,
|
|
294
|
+
).toBe("newer");
|
|
295
|
+
await expect(
|
|
296
|
+
getContentItemsByRelation(adapter, "article", {
|
|
297
|
+
...params,
|
|
298
|
+
limit: 1,
|
|
299
|
+
offset: 1,
|
|
300
|
+
}),
|
|
301
|
+
).rejects.toBeInstanceOf(SyntaxError);
|
|
302
|
+
await expect(
|
|
303
|
+
getContentItemsByRelation(adapter, "article", {
|
|
304
|
+
...params,
|
|
305
|
+
limit: 1,
|
|
306
|
+
offset: 1,
|
|
307
|
+
view: "content",
|
|
308
|
+
}),
|
|
309
|
+
).rejects.toBeInstanceOf(SyntaxError);
|
|
310
|
+
expect(
|
|
311
|
+
await getContentItemsByRelation(adapter, "article", {
|
|
312
|
+
...params,
|
|
313
|
+
offset: 2,
|
|
314
|
+
}),
|
|
315
|
+
).toMatchObject({ items: [], total: 2 });
|
|
316
|
+
});
|
|
317
|
+
|
|
32
318
|
describe("getAllContentTypes", () => {
|
|
33
319
|
it("returns empty array when no content types exist", async () => {
|
|
34
320
|
const types = await getAllContentTypes(adapter);
|