@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
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export { cmsClientPlugin } from "./plugin";
|
|
2
2
|
export type { CMSClientConfig, CMSClientHooks, LoaderContext } from "./plugin";
|
|
3
3
|
export type { ContentTypeConfig } from "../types";
|
|
4
|
+
export type {
|
|
5
|
+
CMSReadOptions,
|
|
6
|
+
CMSContentFields,
|
|
7
|
+
CMSSelectedData,
|
|
8
|
+
CMSReadView,
|
|
9
|
+
SerializedContentItemContent,
|
|
10
|
+
CMSContentItemResult,
|
|
11
|
+
PaginatedContentItems,
|
|
12
|
+
} from "../types";
|
|
4
13
|
export type {
|
|
5
14
|
CMSPluginOverrides,
|
|
6
15
|
CMSProviderConfig,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CMSApiRouter } from "./api";
|
|
2
|
+
import { normalizeContentFields } from "./schemas";
|
|
2
3
|
import {
|
|
3
4
|
createApiClient,
|
|
4
5
|
createResourceQueryKeys,
|
|
@@ -9,11 +10,19 @@ import type {
|
|
|
9
10
|
SerializedContentItemWithType,
|
|
10
11
|
PaginatedContentItems,
|
|
11
12
|
InverseRelation,
|
|
13
|
+
CMSReadOptions,
|
|
14
|
+
CMSContentFields,
|
|
15
|
+
CMSReadView,
|
|
16
|
+
CMSContentItemResult,
|
|
12
17
|
} from "./types";
|
|
13
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
contentListDiscriminator,
|
|
20
|
+
contentReadDiscriminator,
|
|
21
|
+
contentReadKey,
|
|
22
|
+
} from "./api/query-key-defs";
|
|
14
23
|
|
|
15
24
|
/** Params for the paginated content list (one page per `limit`). */
|
|
16
|
-
export interface ContentListParams {
|
|
25
|
+
export interface ContentListParams extends CMSReadOptions {
|
|
17
26
|
typeSlug: string;
|
|
18
27
|
limit?: number;
|
|
19
28
|
/**
|
|
@@ -26,13 +35,13 @@ export interface ContentListParams {
|
|
|
26
35
|
search?: string;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
interface ContentOptionsParams {
|
|
38
|
+
interface ContentOptionsParams extends CMSReadOptions {
|
|
30
39
|
typeSlug: string;
|
|
31
40
|
search?: string;
|
|
32
41
|
limit?: number;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
|
-
interface ContentByRelationParams {
|
|
44
|
+
interface ContentByRelationParams extends CMSReadOptions {
|
|
36
45
|
typeSlug: string;
|
|
37
46
|
field: string;
|
|
38
47
|
targetId: string;
|
|
@@ -44,11 +53,13 @@ interface InverseRelationsParams {
|
|
|
44
53
|
itemId: string;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
interface InverseRelationItemsParams {
|
|
56
|
+
interface InverseRelationItemsParams extends CMSReadOptions {
|
|
48
57
|
slug: string;
|
|
49
58
|
sourceType: string;
|
|
50
59
|
itemId: string;
|
|
51
60
|
fieldName: string;
|
|
61
|
+
limit?: number;
|
|
62
|
+
offset?: number;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
/** Normalizes an empty/whitespace search term to `undefined`. */
|
|
@@ -56,6 +67,29 @@ function normalizeSearch(search: string | undefined): string | undefined {
|
|
|
56
67
|
return search !== undefined && search.trim() === "" ? undefined : search;
|
|
57
68
|
}
|
|
58
69
|
|
|
70
|
+
function contentReadQuery(options?: CMSReadOptions) {
|
|
71
|
+
return {
|
|
72
|
+
view: options?.view,
|
|
73
|
+
fields:
|
|
74
|
+
options?.fields === undefined
|
|
75
|
+
? undefined
|
|
76
|
+
: JSON.stringify(normalizeContentFields(options.fields)),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function relationPageKey(
|
|
81
|
+
params: CMSReadOptions & { limit?: number; offset?: number },
|
|
82
|
+
) {
|
|
83
|
+
const limit = params.limit ?? 20;
|
|
84
|
+
const offset = params.offset ?? 0;
|
|
85
|
+
return limit === 20 &&
|
|
86
|
+
offset === 0 &&
|
|
87
|
+
!params.view &&
|
|
88
|
+
params.fields === undefined
|
|
89
|
+
? []
|
|
90
|
+
: [{ limit, offset, ...contentReadDiscriminator(params) }];
|
|
91
|
+
}
|
|
92
|
+
|
|
59
93
|
/**
|
|
60
94
|
* `getNextPageParam` for `{ items, total }` page envelopes: stop when the
|
|
61
95
|
* last page is short or everything is loaded, otherwise continue at the
|
|
@@ -122,14 +156,18 @@ export const cmsResources = {
|
|
|
122
156
|
sourceType: p.sourceType,
|
|
123
157
|
}),
|
|
124
158
|
query: (p: InverseRelationItemsParams) => ({
|
|
159
|
+
...contentReadQuery(p),
|
|
125
160
|
itemId: p.itemId,
|
|
126
161
|
fieldName: p.fieldName,
|
|
162
|
+
limit: p.limit ?? 20,
|
|
163
|
+
offset: p.offset ?? 0,
|
|
127
164
|
}),
|
|
128
165
|
key: (p: InverseRelationItemsParams) => [
|
|
129
166
|
p.slug,
|
|
130
167
|
p.sourceType,
|
|
131
168
|
p.itemId,
|
|
132
169
|
p.fieldName,
|
|
170
|
+
...relationPageKey(p),
|
|
133
171
|
],
|
|
134
172
|
select: (
|
|
135
173
|
data: any,
|
|
@@ -145,6 +183,7 @@ export const cmsResources = {
|
|
|
145
183
|
path: "/content/:typeSlug",
|
|
146
184
|
params: (p: ContentListParams) => ({ typeSlug: p.typeSlug }),
|
|
147
185
|
query: (p: ContentListParams) => ({
|
|
186
|
+
...contentReadQuery(p),
|
|
148
187
|
limit: p.limit ?? 20,
|
|
149
188
|
search: normalizeSearch(p.search),
|
|
150
189
|
}),
|
|
@@ -162,21 +201,44 @@ export const cmsResources = {
|
|
|
162
201
|
|
|
163
202
|
detail: {
|
|
164
203
|
path: "/content/:typeSlug/:id",
|
|
165
|
-
params: (typeSlug: string, id: string) => ({
|
|
166
|
-
|
|
204
|
+
params: (typeSlug: string, id: string, _options?: CMSReadOptions) => ({
|
|
205
|
+
typeSlug,
|
|
206
|
+
id,
|
|
207
|
+
}),
|
|
208
|
+
query: (_typeSlug: string, _id: string, options?: CMSReadOptions) =>
|
|
209
|
+
contentReadQuery(options),
|
|
210
|
+
key: (typeSlug: string, id: string, options?: CMSReadOptions) => [
|
|
211
|
+
typeSlug,
|
|
212
|
+
id,
|
|
213
|
+
...contentReadKey(options),
|
|
214
|
+
],
|
|
167
215
|
select: (
|
|
168
216
|
data: any,
|
|
169
217
|
_typeSlug: string,
|
|
170
218
|
_id: string,
|
|
219
|
+
_options?: CMSReadOptions,
|
|
171
220
|
): SerializedContentItemWithType | null => data ?? null,
|
|
172
221
|
skip: (typeSlug: string, id: string) => !typeSlug || !id,
|
|
173
222
|
},
|
|
174
223
|
|
|
175
224
|
bySlug: {
|
|
176
225
|
path: "/content/:typeSlug",
|
|
177
|
-
params: (
|
|
178
|
-
|
|
179
|
-
|
|
226
|
+
params: (
|
|
227
|
+
typeSlug: string,
|
|
228
|
+
_slug: string,
|
|
229
|
+
_options?: CMSReadOptions,
|
|
230
|
+
) => ({ typeSlug }),
|
|
231
|
+
query: (_typeSlug: string, slug: string, options?: CMSReadOptions) => ({
|
|
232
|
+
slug,
|
|
233
|
+
limit: 1,
|
|
234
|
+
...contentReadQuery(options),
|
|
235
|
+
}),
|
|
236
|
+
key: (typeSlug: string, slug: string, options?: CMSReadOptions) => [
|
|
237
|
+
"bySlug",
|
|
238
|
+
typeSlug,
|
|
239
|
+
slug,
|
|
240
|
+
...contentReadKey(options),
|
|
241
|
+
],
|
|
180
242
|
select: (data: any): SerializedContentItemWithType | null =>
|
|
181
243
|
data?.items?.[0] ?? null,
|
|
182
244
|
skip: (typeSlug: string, slug: string) => !typeSlug || !slug,
|
|
@@ -195,11 +257,17 @@ export const cmsResources = {
|
|
|
195
257
|
path: "/content/:typeSlug/by-relation",
|
|
196
258
|
params: (p: ContentByRelationParams) => ({ typeSlug: p.typeSlug }),
|
|
197
259
|
query: (p: ContentByRelationParams) => ({
|
|
260
|
+
...contentReadQuery(p),
|
|
198
261
|
field: p.field,
|
|
199
262
|
targetId: p.targetId,
|
|
200
263
|
limit: p.limit ?? 20,
|
|
201
264
|
}),
|
|
202
|
-
key: (p: ContentByRelationParams) => [
|
|
265
|
+
key: (p: ContentByRelationParams) => [
|
|
266
|
+
p.typeSlug,
|
|
267
|
+
p.field,
|
|
268
|
+
p.targetId,
|
|
269
|
+
...relationPageKey(p),
|
|
270
|
+
],
|
|
203
271
|
select: (
|
|
204
272
|
data: any,
|
|
205
273
|
_p: ContentByRelationParams,
|
|
@@ -218,6 +286,7 @@ export const cmsResources = {
|
|
|
218
286
|
path: "/content/:typeSlug",
|
|
219
287
|
params: (p: ContentOptionsParams) => ({ typeSlug: p.typeSlug }),
|
|
220
288
|
query: (p: ContentOptionsParams) => ({
|
|
289
|
+
...contentReadQuery(p),
|
|
221
290
|
search: normalizeSearch(p.search),
|
|
222
291
|
limit: p.limit ?? 50,
|
|
223
292
|
}),
|
|
@@ -226,6 +295,7 @@ export const cmsResources = {
|
|
|
226
295
|
p.typeSlug,
|
|
227
296
|
normalizeSearch(p.search),
|
|
228
297
|
p.limit ?? 50,
|
|
298
|
+
...contentReadKey(p),
|
|
229
299
|
],
|
|
230
300
|
select: (
|
|
231
301
|
data: any,
|
|
@@ -247,7 +317,12 @@ export const cmsResources = {
|
|
|
247
317
|
body: { slug: vars.slug, data: vars.data },
|
|
248
318
|
}),
|
|
249
319
|
select: (data: any) => data as SerializedContentItemWithType | null,
|
|
250
|
-
invalidates: [
|
|
320
|
+
invalidates: [
|
|
321
|
+
"cmsContent",
|
|
322
|
+
"cmsTypes.list",
|
|
323
|
+
"cmsTypes.inverseRelationItems",
|
|
324
|
+
"cmsTypes.inverseRelations",
|
|
325
|
+
],
|
|
251
326
|
},
|
|
252
327
|
update: {
|
|
253
328
|
path: "@put/content/:typeSlug/:id",
|
|
@@ -261,7 +336,11 @@ export const cmsResources = {
|
|
|
261
336
|
body: vars.data,
|
|
262
337
|
}),
|
|
263
338
|
select: (data: any) => data as SerializedContentItemWithType | null,
|
|
264
|
-
invalidates: [
|
|
339
|
+
invalidates: [
|
|
340
|
+
"cmsContent",
|
|
341
|
+
"cmsTypes.inverseRelationItems",
|
|
342
|
+
"cmsTypes.inverseRelations",
|
|
343
|
+
],
|
|
265
344
|
setData: {
|
|
266
345
|
query: "detail",
|
|
267
346
|
args: (updated: SerializedContentItemWithType | null) =>
|
|
@@ -277,12 +356,88 @@ export const cmsResources = {
|
|
|
277
356
|
params: { typeSlug: vars.typeSlug, id: vars.id },
|
|
278
357
|
}),
|
|
279
358
|
select: (data: any) => data as { success: boolean },
|
|
280
|
-
invalidates: [
|
|
359
|
+
invalidates: [
|
|
360
|
+
"cmsContent",
|
|
361
|
+
"cmsTypes.list",
|
|
362
|
+
"cmsTypes.inverseRelationItems",
|
|
363
|
+
"cmsTypes.inverseRelations",
|
|
364
|
+
],
|
|
281
365
|
},
|
|
282
366
|
},
|
|
283
367
|
},
|
|
284
368
|
} satisfies ResourcesDeclaration;
|
|
285
369
|
|
|
370
|
+
type CMSResourceQueryKeys = ReturnType<
|
|
371
|
+
typeof createResourceQueryKeys<typeof cmsResources>
|
|
372
|
+
>;
|
|
373
|
+
type ContentQueryResult<TData> = {
|
|
374
|
+
queryKey: readonly unknown[];
|
|
375
|
+
queryFn: (context?: { pageParam?: unknown }) => Promise<TData>;
|
|
376
|
+
};
|
|
377
|
+
type CMSItemQueryEntry = {
|
|
378
|
+
<
|
|
379
|
+
TView extends CMSReadView = undefined,
|
|
380
|
+
const TFields extends CMSContentFields = undefined,
|
|
381
|
+
>(
|
|
382
|
+
typeSlug: string,
|
|
383
|
+
identity: string,
|
|
384
|
+
options?: CMSReadOptions<TView, TFields>,
|
|
385
|
+
): ContentQueryResult<CMSContentItemResult<
|
|
386
|
+
Record<string, unknown>,
|
|
387
|
+
TView,
|
|
388
|
+
TFields
|
|
389
|
+
> | null>;
|
|
390
|
+
_def: readonly [string, string];
|
|
391
|
+
};
|
|
392
|
+
type CMSRelationQueryEntry<TParams> = {
|
|
393
|
+
<
|
|
394
|
+
TView extends CMSReadView = undefined,
|
|
395
|
+
const TFields extends CMSContentFields = undefined,
|
|
396
|
+
>(
|
|
397
|
+
params: Omit<TParams, "view" | "fields"> & CMSReadOptions<TView, TFields>,
|
|
398
|
+
): ContentQueryResult<
|
|
399
|
+
PaginatedContentItems<Record<string, unknown>, TView, TFields>
|
|
400
|
+
>;
|
|
401
|
+
_def: readonly [string, string];
|
|
402
|
+
};
|
|
403
|
+
type CMSQueryKeys = Omit<CMSResourceQueryKeys, "cmsContent" | "cmsTypes"> & {
|
|
404
|
+
cmsTypes: Omit<CMSResourceQueryKeys["cmsTypes"], "inverseRelationItems"> & {
|
|
405
|
+
inverseRelationItems: CMSRelationQueryEntry<InverseRelationItemsParams>;
|
|
406
|
+
};
|
|
407
|
+
cmsContent: Omit<
|
|
408
|
+
CMSResourceQueryKeys["cmsContent"],
|
|
409
|
+
"list" | "detail" | "bySlug" | "byRelation" | "options"
|
|
410
|
+
> & {
|
|
411
|
+
list: {
|
|
412
|
+
<
|
|
413
|
+
TView extends CMSReadView = undefined,
|
|
414
|
+
const TFields extends CMSContentFields = undefined,
|
|
415
|
+
>(
|
|
416
|
+
params: Omit<ContentListParams, "view" | "fields"> &
|
|
417
|
+
CMSReadOptions<TView, TFields>,
|
|
418
|
+
): ContentQueryResult<
|
|
419
|
+
PaginatedContentItems<Record<string, unknown>, TView, TFields>
|
|
420
|
+
>;
|
|
421
|
+
_def: readonly [string, string];
|
|
422
|
+
};
|
|
423
|
+
options: {
|
|
424
|
+
<
|
|
425
|
+
TView extends CMSReadView = undefined,
|
|
426
|
+
const TFields extends CMSContentFields = undefined,
|
|
427
|
+
>(
|
|
428
|
+
params: Omit<ContentOptionsParams, "view" | "fields"> &
|
|
429
|
+
CMSReadOptions<TView, TFields>,
|
|
430
|
+
): ContentQueryResult<
|
|
431
|
+
CMSContentItemResult<Record<string, unknown>, TView, TFields>[]
|
|
432
|
+
>;
|
|
433
|
+
_def: readonly [string, string];
|
|
434
|
+
};
|
|
435
|
+
detail: CMSItemQueryEntry;
|
|
436
|
+
bySlug: CMSItemQueryEntry;
|
|
437
|
+
byRelation: CMSRelationQueryEntry<ContentByRelationParams>;
|
|
438
|
+
};
|
|
439
|
+
};
|
|
440
|
+
|
|
286
441
|
/**
|
|
287
442
|
* Create CMS query keys for React Query
|
|
288
443
|
* Used by consumers and SSR loaders to fetch content types and content items
|
|
@@ -290,6 +445,6 @@ export const cmsResources = {
|
|
|
290
445
|
export function createCMSQueryKeys(
|
|
291
446
|
client: ReturnType<typeof createApiClient<CMSApiRouter>>,
|
|
292
447
|
headers?: HeadersInit,
|
|
293
|
-
) {
|
|
294
|
-
return createResourceQueryKeys(client, cmsResources, headers);
|
|
448
|
+
): CMSQueryKeys {
|
|
449
|
+
return createResourceQueryKeys(client, cmsResources, headers) as CMSQueryKeys;
|
|
295
450
|
}
|
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** Normalize a field set without changing the caller's array. */
|
|
3
|
+
export function normalizeContentFields(
|
|
4
|
+
fields: readonly string[] | undefined,
|
|
5
|
+
): string[] | undefined {
|
|
6
|
+
return fields === undefined ? undefined : [...new Set(fields)].sort();
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
/** Default upper bound for a single page when no maxPageSize is configured. */
|
|
4
10
|
export const DEFAULT_MAX_PAGE_SIZE = 1000;
|
|
5
11
|
|
|
12
|
+
/** Shared operation and HTTP output options; authorization remains unchanged. */
|
|
13
|
+
export const contentReadOptionsSchema = z.object({
|
|
14
|
+
view: z.literal("content").optional(),
|
|
15
|
+
fields: z
|
|
16
|
+
.array(z.string())
|
|
17
|
+
.min(1)
|
|
18
|
+
.transform((fields) => normalizeContentFields(fields)!)
|
|
19
|
+
.optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** HTTP transports one JSON-encoded array; operation inputs retain string arrays. */
|
|
23
|
+
export const contentReadQuerySchema = contentReadOptionsSchema.extend({
|
|
24
|
+
fields: z.preprocess((value) => {
|
|
25
|
+
if (value === undefined) return undefined;
|
|
26
|
+
if (typeof value !== "string") return z.NEVER;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(value);
|
|
29
|
+
} catch {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
}, contentReadOptionsSchema.shape.fields),
|
|
33
|
+
});
|
|
34
|
+
|
|
6
35
|
/**
|
|
7
36
|
* Factory that creates the list-content query schema with a configurable
|
|
8
37
|
* upper bound on the `limit` parameter.
|
|
@@ -14,6 +43,7 @@ export function createListContentQuerySchema(
|
|
|
14
43
|
maxPageSize = DEFAULT_MAX_PAGE_SIZE,
|
|
15
44
|
) {
|
|
16
45
|
return z.object({
|
|
46
|
+
...contentReadOptionsSchema.shape,
|
|
17
47
|
slug: z.string().optional(),
|
|
18
48
|
search: z.string().max(200).optional(),
|
|
19
49
|
limit: z.coerce.number().min(1).max(maxPageSize).optional().default(20),
|
package/src/plugins/cms/types.ts
CHANGED
|
@@ -189,12 +189,59 @@ export interface SerializedContentItemWithType<TData = Record<string, unknown>>
|
|
|
189
189
|
_relations?: Record<string, SerializedContentItemWithType[]>;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/** Output shape for CMS reads; omission retains the full editor envelope. */
|
|
193
|
+
export type CMSReadView = "content" | undefined;
|
|
194
|
+
|
|
195
|
+
/** Literal top-level property names selected from parsed content. */
|
|
196
|
+
export type CMSContentFields = readonly string[] | undefined;
|
|
197
|
+
|
|
198
|
+
/** Select known literal properties, or an honest partial shape for dynamic arrays. */
|
|
199
|
+
export type CMSSelectedData<
|
|
200
|
+
TData,
|
|
201
|
+
TFields extends CMSContentFields = undefined,
|
|
202
|
+
> = TFields extends readonly string[]
|
|
203
|
+
? string extends TFields[number]
|
|
204
|
+
? Partial<TData>
|
|
205
|
+
: Pick<TData, Extract<TFields[number], keyof TData>>
|
|
206
|
+
: TData;
|
|
207
|
+
|
|
208
|
+
/** Optional output controls for CMS reads. These do not change access checks. */
|
|
209
|
+
export interface CMSReadOptions<
|
|
210
|
+
TView extends CMSReadView = CMSReadView,
|
|
211
|
+
TFields extends CMSContentFields = CMSContentFields,
|
|
212
|
+
> {
|
|
213
|
+
/** Return identity, parsed content and timestamps without editor metadata. */
|
|
214
|
+
view?: TView;
|
|
215
|
+
/** Literal declared top-level content properties. Requires content view. */
|
|
216
|
+
fields?: TFields;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Serialized content envelope without raw JSON or editor metadata. */
|
|
220
|
+
export type SerializedContentItemContent<TData = Record<string, unknown>> =
|
|
221
|
+
Pick<
|
|
222
|
+
SerializedContentItemWithType<TData>,
|
|
223
|
+
"id" | "slug" | "parsedData" | "createdAt" | "updatedAt"
|
|
224
|
+
>;
|
|
225
|
+
|
|
226
|
+
/** Result envelope selected by a statically known read view. */
|
|
227
|
+
export type CMSContentItemResult<
|
|
228
|
+
TData = Record<string, unknown>,
|
|
229
|
+
TView extends CMSReadView = undefined,
|
|
230
|
+
TFields extends CMSContentFields = undefined,
|
|
231
|
+
> = TView extends "content"
|
|
232
|
+
? SerializedContentItemContent<CMSSelectedData<TData, TFields>>
|
|
233
|
+
: SerializedContentItemWithType<TData>;
|
|
234
|
+
|
|
192
235
|
/**
|
|
193
236
|
* Paginated list response for content items
|
|
194
237
|
* @template TData - The type of the parsed data (defaults to Record<string, unknown>)
|
|
195
238
|
*/
|
|
196
|
-
export interface PaginatedContentItems<
|
|
197
|
-
|
|
239
|
+
export interface PaginatedContentItems<
|
|
240
|
+
TData = Record<string, unknown>,
|
|
241
|
+
TView extends CMSReadView = undefined,
|
|
242
|
+
TFields extends CMSContentFields = undefined,
|
|
243
|
+
> {
|
|
244
|
+
items: CMSContentItemResult<TData, TView, TFields>[];
|
|
198
245
|
total: number;
|
|
199
246
|
limit: number;
|
|
200
247
|
offset: number;
|
|
@@ -499,20 +499,26 @@ describe("Media protected query identity partition", () => {
|
|
|
499
499
|
status: 200,
|
|
500
500
|
headers: { "content-type": "application/json" },
|
|
501
501
|
}),
|
|
502
|
-
)
|
|
502
|
+
)
|
|
503
|
+
.mockResolvedValueOnce(responseFor("user-a-refreshed"));
|
|
503
504
|
const auth = createIdentityTestAuth(vi.fn(() => null));
|
|
504
505
|
let attemptedIdentity: string | undefined;
|
|
506
|
+
let committedIdentity: string | undefined;
|
|
507
|
+
let filename: string | undefined;
|
|
505
508
|
let deleteAsset:
|
|
506
509
|
| ReturnType<typeof useDeleteAsset>["mutateAsync"]
|
|
507
510
|
| undefined;
|
|
508
|
-
let suspend = false;
|
|
509
511
|
const never = new Promise<never>(() => {});
|
|
510
512
|
|
|
511
513
|
function Probe() {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
+
const identity = useIdentity().identity?.id;
|
|
515
|
+
attemptedIdentity = identity;
|
|
516
|
+
useLayoutEffect(() => {
|
|
517
|
+
committedIdentity = identity;
|
|
518
|
+
}, [identity]);
|
|
519
|
+
filename = useAssets({ limit: 40 }).data?.pages[0]?.items[0]?.filename;
|
|
514
520
|
deleteAsset = useDeleteAsset().mutateAsync;
|
|
515
|
-
if (
|
|
521
|
+
if (identity === "user-b") throw never;
|
|
516
522
|
return null;
|
|
517
523
|
}
|
|
518
524
|
|
|
@@ -532,24 +538,28 @@ describe("Media protected query identity partition", () => {
|
|
|
532
538
|
);
|
|
533
539
|
|
|
534
540
|
await act(async () => root.render(tree({ id: "user-a" })));
|
|
535
|
-
await waitFor(() =>
|
|
541
|
+
await waitFor(() => filename === "user-a.jpg");
|
|
536
542
|
const committedDelete = deleteAsset;
|
|
537
|
-
suspend = true;
|
|
538
543
|
await act(() => {
|
|
539
544
|
startTransition(() => root.render(tree({ id: "user-b" })));
|
|
540
545
|
});
|
|
541
546
|
await waitFor(() => attemptedIdentity === "user-b");
|
|
547
|
+
expect(committedIdentity).toBe("user-a");
|
|
542
548
|
await act(async () => committedDelete?.("asset-user-a"));
|
|
543
549
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
550
|
+
const committedData = queryClient.getQueryData<{
|
|
551
|
+
pages: Array<{ items: Array<{ filename: string }> }>;
|
|
552
|
+
}>(
|
|
553
|
+
MEDIA_QUERY_KEYS.assetsList(
|
|
554
|
+
{ limit: 40 },
|
|
555
|
+
{ id: "user-a" },
|
|
556
|
+
MEDIA_ENDPOINT,
|
|
551
557
|
),
|
|
552
|
-
)
|
|
558
|
+
);
|
|
559
|
+
expect(committedIdentity).toBe("user-a");
|
|
560
|
+
expect(committedData?.pages[0]?.items[0]?.filename).toBe(
|
|
561
|
+
"user-a-refreshed.jpg",
|
|
562
|
+
);
|
|
553
563
|
expect(
|
|
554
564
|
queryClient.getQueryData(
|
|
555
565
|
MEDIA_QUERY_KEYS.assetsList(
|
|
@@ -559,7 +569,7 @@ describe("Media protected query identity partition", () => {
|
|
|
559
569
|
),
|
|
560
570
|
),
|
|
561
571
|
).toBeUndefined();
|
|
562
|
-
expect(fetchMock).toHaveBeenCalledTimes(
|
|
572
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
563
573
|
});
|
|
564
574
|
|
|
565
575
|
it("does not refetch an unmounted identity during a replacement commit", async () => {
|