@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.
Files changed (113) hide show
  1. package/dist/api/index.d.cts +2 -2
  2. package/dist/api/index.d.mts +2 -2
  3. package/dist/api/index.d.ts +2 -2
  4. package/dist/client/index.d.cts +2 -2
  5. package/dist/client/index.d.mts +2 -2
  6. package/dist/client/index.d.ts +2 -2
  7. package/dist/context/index.d.cts +1 -1
  8. package/dist/context/index.d.mts +1 -1
  9. package/dist/context/index.d.ts +1 -1
  10. package/dist/index.d.cts +1 -1
  11. package/dist/index.d.mts +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/packages/stack/src/plugins/cms/api/getters.cjs +142 -6
  14. package/dist/packages/stack/src/plugins/cms/api/getters.mjs +139 -8
  15. package/dist/packages/stack/src/plugins/cms/api/operations.cjs +67 -61
  16. package/dist/packages/stack/src/plugins/cms/api/operations.mjs +69 -63
  17. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +26 -8
  18. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +27 -9
  19. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.cjs +15 -1
  20. package/dist/packages/stack/src/plugins/cms/api/query-key-defs.mjs +14 -2
  21. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.cjs +45 -24
  22. package/dist/packages/stack/src/plugins/cms/client/hooks/cms-hooks.mjs +45 -24
  23. package/dist/packages/stack/src/plugins/cms/schemas.cjs +22 -0
  24. package/dist/packages/stack/src/plugins/cms/schemas.mjs +20 -1
  25. package/dist/plugins/api/index.d.cts +2 -2
  26. package/dist/plugins/api/index.d.mts +2 -2
  27. package/dist/plugins/api/index.d.ts +2 -2
  28. package/dist/plugins/blog/api/index.d.cts +2 -2
  29. package/dist/plugins/blog/api/index.d.mts +2 -2
  30. package/dist/plugins/blog/api/index.d.ts +2 -2
  31. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  32. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  33. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  34. package/dist/plugins/blog/client/index.d.cts +2 -2
  35. package/dist/plugins/blog/client/index.d.mts +2 -2
  36. package/dist/plugins/blog/client/index.d.ts +2 -2
  37. package/dist/plugins/blog/query-keys.d.cts +2 -2
  38. package/dist/plugins/blog/query-keys.d.mts +2 -2
  39. package/dist/plugins/blog/query-keys.d.ts +2 -2
  40. package/dist/plugins/client/index.d.cts +2 -2
  41. package/dist/plugins/client/index.d.mts +2 -2
  42. package/dist/plugins/client/index.d.ts +2 -2
  43. package/dist/plugins/cms/api/index.cjs +1 -0
  44. package/dist/plugins/cms/api/index.d.cts +43 -11
  45. package/dist/plugins/cms/api/index.d.mts +43 -11
  46. package/dist/plugins/cms/api/index.d.ts +43 -11
  47. package/dist/plugins/cms/api/index.mjs +1 -1
  48. package/dist/plugins/cms/client/hooks/index.d.cts +90 -45
  49. package/dist/plugins/cms/client/hooks/index.d.mts +90 -45
  50. package/dist/plugins/cms/client/hooks/index.d.ts +90 -45
  51. package/dist/plugins/cms/client/index.d.cts +2 -1
  52. package/dist/plugins/cms/client/index.d.mts +2 -1
  53. package/dist/plugins/cms/client/index.d.ts +2 -1
  54. package/dist/plugins/cms/query-keys.cjs +69 -13
  55. package/dist/plugins/cms/query-keys.d.cts +89 -212
  56. package/dist/plugins/cms/query-keys.d.mts +89 -212
  57. package/dist/plugins/cms/query-keys.d.ts +89 -212
  58. package/dist/plugins/cms/query-keys.mjs +70 -14
  59. package/dist/plugins/kanban/client/index.d.cts +1 -1
  60. package/dist/plugins/kanban/client/index.d.mts +1 -1
  61. package/dist/plugins/kanban/client/index.d.ts +1 -1
  62. package/dist/plugins/route-docs/client/index.d.cts +1 -1
  63. package/dist/plugins/route-docs/client/index.d.mts +1 -1
  64. package/dist/plugins/route-docs/client/index.d.ts +1 -1
  65. package/dist/plugins/ui-builder/client/hooks/index.d.cts +3 -3
  66. package/dist/plugins/ui-builder/client/hooks/index.d.mts +3 -3
  67. package/dist/plugins/ui-builder/client/hooks/index.d.ts +3 -3
  68. package/dist/plugins/ui-builder/client/index.d.cts +3 -3
  69. package/dist/plugins/ui-builder/client/index.d.mts +3 -3
  70. package/dist/plugins/ui-builder/client/index.d.ts +3 -3
  71. package/dist/plugins/ui-builder/query-keys.d.cts +4 -3
  72. package/dist/plugins/ui-builder/query-keys.d.mts +4 -3
  73. package/dist/plugins/ui-builder/query-keys.d.ts +4 -3
  74. package/dist/shared/{stack.DMj68qY0.d.mts → stack.B2jlVVhD.d.mts} +5 -5
  75. package/dist/shared/{stack.Cvnj99cs.d.ts → stack.BB9tubJJ.d.ts} +9 -3
  76. package/dist/shared/{stack.Bt3WG2yb.d.cts → stack.BWduIMKG.d.cts} +35 -35
  77. package/dist/shared/stack.Bw09Lg2z.d.ts +1420 -0
  78. package/dist/shared/{stack.CkEz7vSt.d.mts → stack.C2Pvl-RS.d.mts} +9 -3
  79. package/dist/shared/{stack.C2l2wd_X.d.ts → stack.Ch9-ppmQ.d.ts} +35 -35
  80. package/dist/shared/{stack.Ht8mhDj5.d.cts → stack.D17yFglU.d.cts} +5 -5
  81. package/dist/shared/{stack.CiD115Ov.d.mts → stack.D6fPBfYe.d.mts} +35 -35
  82. package/dist/shared/stack.DFGj0gb7.d.mts +1420 -0
  83. package/dist/shared/{stack.pi0FxoEa.d.ts → stack.Dcfg4GNN.d.ts} +5 -5
  84. package/dist/shared/{stack.3rxARhgS.d.cts → stack.DrB3L7Ih.d.cts} +52 -3
  85. package/dist/shared/{stack.3rxARhgS.d.mts → stack.DrB3L7Ih.d.mts} +52 -3
  86. package/dist/shared/{stack.3rxARhgS.d.ts → stack.DrB3L7Ih.d.ts} +52 -3
  87. package/dist/shared/stack.NNrBKBkM.d.cts +1420 -0
  88. package/dist/shared/{stack.mRRyZBUg.d.cts → stack.Rb87lHqK.d.cts} +9 -3
  89. package/package.json +1 -1
  90. package/src/__tests__/cms-query-keys.test.ts +57 -0
  91. package/src/plugins/api/index.ts +1 -0
  92. package/src/plugins/api/operation.ts +16 -3
  93. package/src/plugins/cms/__tests__/authorization.test.ts +652 -187
  94. package/src/plugins/cms/__tests__/client-runtime.browser.test.tsx +655 -0
  95. package/src/plugins/cms/__tests__/content-read.typecheck.ts +351 -0
  96. package/src/plugins/cms/__tests__/getters.test.ts +286 -0
  97. package/src/plugins/cms/api/getters.ts +270 -12
  98. package/src/plugins/cms/api/index.ts +10 -0
  99. package/src/plugins/cms/api/operations.ts +163 -75
  100. package/src/plugins/cms/api/plugin.ts +50 -12
  101. package/src/plugins/cms/api/query-key-defs.ts +29 -2
  102. package/src/plugins/cms/client/hooks/cms-hooks.tsx +400 -91
  103. package/src/plugins/cms/client/index.ts +9 -0
  104. package/src/plugins/cms/query-keys.ts +171 -16
  105. package/src/plugins/cms/schemas.ts +30 -0
  106. package/src/plugins/cms/types.ts +49 -2
  107. package/src/plugins/media/__tests__/identity-partition.test.tsx +26 -16
  108. package/dist/shared/stack.CAmVUwjV.d.cts +0 -764
  109. package/dist/shared/stack.CQK02ujt.d.ts +0 -764
  110. package/dist/shared/stack.D9x1fR5a.d.mts +0 -764
  111. package/dist/shared/{stack.CKc_Bais.d.cts → stack.Csi-4Mgz.d.cts} +7 -7
  112. package/dist/shared/{stack.CKc_Bais.d.mts → stack.Csi-4Mgz.d.mts} +7 -7
  113. package/dist/shared/{stack.CKc_Bais.d.ts → stack.Csi-4Mgz.d.ts} +7 -7
@@ -6,6 +6,7 @@ import {
6
6
  OperationHttpError,
7
7
  type OperationContext,
8
8
  type OperationData,
9
+ type OperationCallSignature,
9
10
  } from "@btst/stack/plugins/api";
10
11
  import { z } from "zod";
11
12
  import { formSchemaToZod } from "@workspace/ui/lib/schema-converter";
@@ -13,9 +14,13 @@ import { cmsPermissions } from "../permissions";
13
14
  import {
14
15
  createListContentQuerySchema,
15
16
  DEFAULT_MAX_PAGE_SIZE,
17
+ contentReadOptionsSchema,
16
18
  } from "../schemas";
17
19
  import type {
18
20
  CMSBackendHooks,
21
+ CMSReadOptions,
22
+ CMSContentFields,
23
+ CMSSelectedData,
19
24
  CMSCreateOperationContext,
20
25
  CMSDeleteOperationContext,
21
26
  CMSOperationData,
@@ -32,6 +37,10 @@ import type {
32
37
  import { slugify } from "../utils";
33
38
  import {
34
39
  getAllContentItems,
40
+ validateContentReadOptions,
41
+ CMSContentSelectionError,
42
+ applyContentReadOptions,
43
+ getContentItemsByRelationForType,
35
44
  getContentItemBySlug,
36
45
  serializeContentItemWithType,
37
46
  serializeContentType,
@@ -66,16 +75,50 @@ function operationContentItem(item: SerializedContentItemWithType) {
66
75
  }
67
76
 
68
77
  function operationContentList(
69
- result: Awaited<ReturnType<typeof getAllContentItems>>,
78
+ result: Awaited<ReturnType<typeof getAllContentItems<undefined>>>,
79
+ options?: CMSReadOptions,
70
80
  ) {
71
81
  return {
72
- items: result.items.map(operationContentItem),
82
+ items: result.items.map((item) =>
83
+ applyContentReadOptions(operationContentItem(item), options),
84
+ ),
73
85
  total: result.total,
74
86
  ...(result.limit !== undefined ? { limit: result.limit } : {}),
75
87
  ...(result.offset !== undefined ? { offset: result.offset } : {}),
76
88
  };
77
89
  }
78
90
 
91
+ type ContentReadOperationContent<TOptions> = Omit<
92
+ Pick<
93
+ ReturnType<typeof operationContentItem>,
94
+ "id" | "slug" | "parsedData" | "createdAt" | "updatedAt"
95
+ >,
96
+ "parsedData"
97
+ > & {
98
+ parsedData: TOptions extends {
99
+ fields?: infer TFields extends CMSContentFields;
100
+ }
101
+ ? CMSSelectedData<Record<string, unknown>, TFields>
102
+ : ReturnType<typeof operationContentItem>["parsedData"];
103
+ };
104
+
105
+ type ContentReadOperationItem<TOptions> = TOptions extends { view: "content" }
106
+ ? ContentReadOperationContent<TOptions>
107
+ : TOptions extends { view?: undefined }
108
+ ? ReturnType<typeof operationContentItem>
109
+ : "view" extends keyof TOptions
110
+ ?
111
+ | ReturnType<typeof operationContentItem>
112
+ | ContentReadOperationContent<TOptions>
113
+ : ReturnType<typeof operationContentItem>;
114
+
115
+ type ContentReadOperationPage<TOptions> = DeepReadonly<{
116
+ items: ContentReadOperationItem<TOptions>[];
117
+ total: number;
118
+ limit: number;
119
+ offset: number;
120
+ }>;
121
+
79
122
  const JSONValueSchema: z.ZodType<OperationData> = z.lazy(() =>
80
123
  z.union([
81
124
  z.string(),
@@ -114,7 +157,9 @@ export const CMSUpdateContentItemBodySchema = z.object({
114
157
 
115
158
  const ListContentTypesInputSchema = EmptyInputSchema;
116
159
  const GetContentTypeInputSchema = CMSContentTypeParamsSchema;
117
- const GetContentItemInputSchema = CMSContentItemParamsSchema;
160
+ const GetContentItemInputSchema = CMSContentItemParamsSchema.extend(
161
+ contentReadOptionsSchema.shape,
162
+ );
118
163
  const CreateContentItemInputSchema = z.object({
119
164
  typeSlug: z.string().min(1),
120
165
  body: CMSCreateContentItemBodySchema,
@@ -161,6 +206,25 @@ export class CMSOperationError extends OperationHttpError {
161
206
  }
162
207
  }
163
208
 
209
+ function validateOperationContentReadOptions(
210
+ contentType: Pick<ContentType, "jsonSchema"> | undefined,
211
+ options: CMSReadOptions,
212
+ ) {
213
+ try {
214
+ validateContentReadOptions(contentType, options);
215
+ } catch (error) {
216
+ if (error instanceof CMSContentSelectionError) {
217
+ throw new CMSOperationError(
218
+ 400,
219
+ error.message,
220
+ "INVALID_CONTENT_FIELDS",
221
+ [{ path: ["fields"], message: error.message }],
222
+ );
223
+ }
224
+ throw error;
225
+ }
226
+ }
227
+
164
228
  function normalizeOperationError(error: unknown, fallback: string): Error {
165
229
  if (error instanceof Error) return error;
166
230
  return new Error(typeof error === "string" ? error : fallback, {
@@ -930,49 +994,6 @@ async function deriveInverseSourceAuthorization(
930
994
  };
931
995
  }
932
996
 
933
- async function listRecordsForRelation(
934
- adapter: Adapter,
935
- contentType: ContentType,
936
- fieldName: string,
937
- targetId: string,
938
- limit: number,
939
- offset: number,
940
- ) {
941
- const relations = await adapter.findMany<ContentRelation>({
942
- model: "contentRelation",
943
- where: [
944
- { field: "targetId", value: targetId, operator: "eq" as const },
945
- { field: "fieldName", value: fieldName, operator: "eq" as const },
946
- ],
947
- });
948
- const ids = [...new Set(relations.map((relation) => relation.sourceId))];
949
- const items: ContentItemWithType[] = [];
950
- for (const id of ids) {
951
- const item = await adapter.findOne<ContentItemWithType>({
952
- model: "contentItem",
953
- where: [
954
- { field: "id", value: id, operator: "eq" as const },
955
- {
956
- field: "contentTypeId",
957
- value: contentType.id,
958
- operator: "eq" as const,
959
- },
960
- ],
961
- join: { contentType: true },
962
- });
963
- if (item) items.push(item);
964
- }
965
- items.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
966
- return {
967
- items: items
968
- .slice(offset, offset + limit)
969
- .map(serializeContentItemWithType),
970
- total: items.length,
971
- limit,
972
- offset,
973
- };
974
- }
975
-
976
997
  type EnsureSynced = () => Promise<void>;
977
998
  type GetContentTypesWithCounts = () => Promise<
978
999
  Array<ReturnType<typeof serializeContentType> & { itemCount: number }>
@@ -1214,7 +1235,8 @@ export function createCMSOperations(
1214
1235
  typeSlug: z.string().min(1),
1215
1236
  query: z
1216
1237
  .object({ field: z.string().min(1), targetId: z.string().min(1) })
1217
- .merge(paginationSchema),
1238
+ .merge(paginationSchema)
1239
+ .merge(contentReadOptionsSchema),
1218
1240
  });
1219
1241
  const GetInverseRelationsInputSchema = z.object({
1220
1242
  slug: z.string().min(1),
@@ -1228,7 +1250,8 @@ export function createCMSOperations(
1228
1250
  itemId: z.string().min(1),
1229
1251
  fieldName: z.string().min(1),
1230
1252
  })
1231
- .merge(paginationSchema),
1253
+ .merge(paginationSchema)
1254
+ .merge(contentReadOptionsSchema),
1232
1255
  });
1233
1256
 
1234
1257
  const notifyError = async (
@@ -1338,11 +1361,17 @@ export function createCMSOperations(
1338
1361
  cmsPermissions.contentType.read({ contentType: facts.contentType }),
1339
1362
  ],
1340
1363
  execute: async ({ input, facts }) => {
1341
- const result = await getAllContentItems(
1342
- adapter,
1343
- facts.contentType,
1344
- input.query,
1345
- );
1364
+ if (input.query.fields !== undefined) {
1365
+ validateOperationContentReadOptions(
1366
+ await getContentTypeOrThrow(adapter, ensureSynced, facts.contentType),
1367
+ input.query,
1368
+ );
1369
+ }
1370
+ const result = await getAllContentItems(adapter, facts.contentType, {
1371
+ ...input.query,
1372
+ view: undefined,
1373
+ fields: undefined,
1374
+ });
1346
1375
  if (facts.scope === "record") {
1347
1376
  if (!facts.recordId) {
1348
1377
  if (result.items.length > 0) throw staleRecordError();
@@ -1355,7 +1384,7 @@ export function createCMSOperations(
1355
1384
  ) {
1356
1385
  throw staleRecordError();
1357
1386
  }
1358
- return operationContentList(result);
1387
+ return operationContentList(result, input.query);
1359
1388
  }
1360
1389
  if (result.items.length > 1) throw staleRecordError();
1361
1390
  if (result.items.length === 0) {
@@ -1370,7 +1399,7 @@ export function createCMSOperations(
1370
1399
  assertRecordFacts(result.items[0]!, facts);
1371
1400
  }
1372
1401
  }
1373
- return operationContentList(result);
1402
+ return operationContentList(result, input.query);
1374
1403
  },
1375
1404
  onError: ({ error, ...context }) =>
1376
1405
  notifyError(
@@ -1390,7 +1419,7 @@ export function createCMSOperations(
1390
1419
  additionalPermissions: ({ facts }) => [
1391
1420
  cmsPermissions.contentType.read({ contentType: facts.contentType }),
1392
1421
  ],
1393
- execute: async ({ facts }) => {
1422
+ execute: async ({ input, facts }) => {
1394
1423
  const item = await getRecordOrThrow(
1395
1424
  adapter,
1396
1425
  ensureSynced,
@@ -1398,7 +1427,11 @@ export function createCMSOperations(
1398
1427
  facts.recordId ?? "",
1399
1428
  );
1400
1429
  assertRecordFacts(item, facts);
1401
- return operationContentItem(serializeContentItemWithType(item));
1430
+ validateOperationContentReadOptions(item.contentType, input);
1431
+ return applyContentReadOptions(
1432
+ operationContentItem(serializeContentItemWithType(item)),
1433
+ input,
1434
+ );
1402
1435
  },
1403
1436
  onError: ({ error, ...context }) =>
1404
1437
  notifyError(
@@ -1866,7 +1899,7 @@ export function createCMSOperations(
1866
1899
  });
1867
1900
 
1868
1901
  const getContentItemPopulated = defineOperation({
1869
- input: GetContentItemInputSchema,
1902
+ input: CMSContentItemParamsSchema,
1870
1903
  permission: cmsPermissions.record.read,
1871
1904
  facts: async ({ input }) =>
1872
1905
  recordFacts(
@@ -1973,17 +2006,17 @@ export function createCMSOperations(
1973
2006
  input.query.field,
1974
2007
  input.query.targetId,
1975
2008
  );
1976
- const result = await listRecordsForRelation(
2009
+ validateOperationContentReadOptions(sourceContentType, input.query);
2010
+ const result = await getContentItemsByRelationForType(
1977
2011
  adapter,
1978
2012
  sourceContentType,
1979
- input.query.field,
1980
- input.query.targetId,
1981
- input.query.limit,
1982
- input.query.offset,
2013
+ input.query,
1983
2014
  );
1984
2015
  return {
1985
2016
  ...result,
1986
- items: result.items.map(operationContentItem),
2017
+ items: result.items.map((item) =>
2018
+ applyContentReadOptions(operationContentItem(item), input.query),
2019
+ ),
1987
2020
  };
1988
2021
  },
1989
2022
  onError: ({ error, ...context }) =>
@@ -2178,17 +2211,27 @@ export function createCMSOperations(
2178
2211
  input.slug,
2179
2212
  input.query.itemId,
2180
2213
  );
2181
- const result = await listRecordsForRelation(
2214
+ const sourceContentType = await getContentTypeOrThrow(
2182
2215
  adapter,
2183
- await getContentTypeOrThrow(adapter, ensureSynced, facts.contentType),
2184
- source.fieldName,
2185
- input.query.itemId,
2186
- input.query.limit,
2187
- input.query.offset,
2216
+ ensureSynced,
2217
+ facts.contentType,
2218
+ );
2219
+ validateOperationContentReadOptions(sourceContentType, input.query);
2220
+ const result = await getContentItemsByRelationForType(
2221
+ adapter,
2222
+ sourceContentType,
2223
+ {
2224
+ field: source.fieldName,
2225
+ targetId: input.query.itemId,
2226
+ limit: input.query.limit,
2227
+ offset: input.query.offset,
2228
+ },
2188
2229
  );
2189
2230
  return {
2190
2231
  ...result,
2191
- items: result.items.map(operationContentItem),
2232
+ items: result.items.map((item) =>
2233
+ applyContentReadOptions(operationContentItem(item), input.query),
2234
+ ),
2192
2235
  };
2193
2236
  } finally {
2194
2237
  inverseTargetAuthorizationSnapshots.delete(input as object);
@@ -2205,14 +2248,59 @@ export function createCMSOperations(
2205
2248
  return {
2206
2249
  listContentTypes,
2207
2250
  getContentTypeBySlug,
2208
- listContentItems,
2209
- getContentItem,
2251
+ listContentItems: listContentItems as typeof listContentItems &
2252
+ OperationCallSignature<
2253
+ <
2254
+ const TInput extends DeepReadonly<
2255
+ z.input<typeof ListContentItemsInputSchema>
2256
+ >,
2257
+ >(
2258
+ input: TInput,
2259
+ ) => Promise<
2260
+ DeepReadonly<{
2261
+ items: ContentReadOperationItem<TInput["query"]>[];
2262
+ total: number;
2263
+ limit: number;
2264
+ offset: number;
2265
+ }>
2266
+ >
2267
+ >,
2268
+ getContentItem: getContentItem as typeof getContentItem &
2269
+ OperationCallSignature<
2270
+ <
2271
+ const TInput extends DeepReadonly<
2272
+ z.input<typeof GetContentItemInputSchema>
2273
+ >,
2274
+ >(
2275
+ input: TInput,
2276
+ ) => Promise<DeepReadonly<ContentReadOperationItem<TInput>>>
2277
+ >,
2210
2278
  createContentItem,
2211
2279
  updateContentItem,
2212
2280
  deleteContentItem,
2213
2281
  getContentItemPopulated,
2214
- listContentByRelation,
2282
+ listContentByRelation:
2283
+ listContentByRelation as typeof listContentByRelation &
2284
+ OperationCallSignature<
2285
+ <
2286
+ const TInput extends DeepReadonly<
2287
+ z.input<typeof ListContentByRelationInputSchema>
2288
+ >,
2289
+ >(
2290
+ input: TInput,
2291
+ ) => Promise<ContentReadOperationPage<TInput["query"]>>
2292
+ >,
2215
2293
  getInverseRelations,
2216
- listInverseRelationItems,
2294
+ listInverseRelationItems:
2295
+ listInverseRelationItems as typeof listInverseRelationItems &
2296
+ OperationCallSignature<
2297
+ <
2298
+ const TInput extends DeepReadonly<
2299
+ z.input<typeof ListInverseRelationItemsInputSchema>
2300
+ >,
2301
+ >(
2302
+ input: TInput,
2303
+ ) => Promise<ContentReadOperationPage<TInput["query"]>>
2304
+ >,
2217
2305
  } as const;
2218
2306
  }
@@ -7,8 +7,15 @@ import { cmsSchema as dbSchema } from "../db";
7
7
  import {
8
8
  createListContentQuerySchema,
9
9
  DEFAULT_MAX_PAGE_SIZE,
10
+ contentReadOptionsSchema,
11
+ contentReadQuerySchema,
10
12
  } from "../schemas";
11
- import type { CMSBackendConfig, CMSBackendHooks, ContentType } from "../types";
13
+ import type {
14
+ CMSBackendConfig,
15
+ CMSBackendHooks,
16
+ ContentType,
17
+ CMSReadOptions,
18
+ } from "../types";
12
19
  export { CMS_LIFECYCLE_HOOK_MIGRATIONS } from "./lifecycle-migrations";
13
20
  import {
14
21
  getAllContentItems,
@@ -54,12 +61,16 @@ interface CMSPrefetchForRoute {
54
61
  (
55
62
  key: "contentList",
56
63
  qc: QueryClient,
57
- params: { typeSlug: string },
64
+ params: {
65
+ typeSlug: string;
66
+ limit?: number;
67
+ search?: string;
68
+ } & CMSReadOptions,
58
69
  ): Promise<void>;
59
70
  (
60
71
  key: "editContent",
61
72
  qc: QueryClient,
62
- params: { typeSlug: string; id: string },
73
+ params: { typeSlug: string; id: string } & CMSReadOptions,
63
74
  ): Promise<void>;
64
75
  }
65
76
 
@@ -187,7 +198,12 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
187
198
  async function prefetchForRoute(
188
199
  key: CMSRouteKey,
189
200
  queryClient: QueryClient,
190
- params?: Record<string, string>,
201
+ params?: {
202
+ typeSlug?: string;
203
+ id?: string;
204
+ limit?: number;
205
+ search?: string;
206
+ } & CMSReadOptions,
191
207
  ): Promise<void> {
192
208
  await ensureSynced(adapter);
193
209
  switch (key) {
@@ -200,13 +216,21 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
200
216
  break;
201
217
  case "contentList": {
202
218
  const typeSlug = params?.typeSlug ?? "";
219
+ const options = createListContentQuerySchema(
220
+ config.maxPageSize,
221
+ ).parse({
222
+ limit: params?.limit,
223
+ search: params?.search,
224
+ view: params?.view,
225
+ fields: params?.fields,
226
+ });
203
227
  const [contentTypes, contentItems] = await Promise.all([
204
228
  getContentTypesWithCounts(adapter),
205
- getAllContentItems(adapter, typeSlug, { limit: 20, offset: 0 }),
229
+ getAllContentItems(adapter, typeSlug, options),
206
230
  ]);
207
231
  queryClient.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
208
232
  queryClient.setQueryData(
209
- CMS_QUERY_KEYS.contentList({ typeSlug, limit: 20, offset: 0 }),
233
+ CMS_QUERY_KEYS.contentList({ typeSlug, ...options }),
210
234
  {
211
235
  pages: [
212
236
  {
@@ -224,14 +248,20 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
224
248
  case "editContent": {
225
249
  const typeSlug = params?.typeSlug ?? "";
226
250
  const id = params?.id ?? "";
251
+ const options = contentReadOptionsSchema.parse({
252
+ view: params?.view,
253
+ fields: params?.fields,
254
+ });
227
255
  const [contentTypes, item] = await Promise.all([
228
256
  getContentTypesWithCounts(adapter),
229
- id ? getContentItemById(adapter, id) : Promise.resolve(null),
257
+ id
258
+ ? getContentItemById(adapter, id, options)
259
+ : Promise.resolve(null),
230
260
  ]);
231
261
  queryClient.setQueryData(CMS_QUERY_KEYS.typesList(), contentTypes);
232
262
  if (id) {
233
263
  queryClient.setQueryData(
234
- CMS_QUERY_KEYS.contentDetail(typeSlug, id),
264
+ CMS_QUERY_KEYS.contentDetail(typeSlug, id, options),
235
265
  item,
236
266
  );
237
267
  }
@@ -259,7 +289,9 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
259
289
  }),
260
290
 
261
291
  routes: (_adapter: Adapter, _context, operations) => {
262
- const listQuerySchema = createListContentQuerySchema(config.maxPageSize);
292
+ const listQuerySchema = createListContentQuerySchema(
293
+ config.maxPageSize,
294
+ ).extend(contentReadQuerySchema.shape);
263
295
  const paginationSchema = z.object({
264
296
  limit: z.coerce
265
297
  .number()
@@ -301,9 +333,13 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
301
333
  {
302
334
  method: "GET",
303
335
  params: CMSContentItemParamsSchema,
336
+ query: contentReadQuerySchema,
304
337
  requireRequest: true,
305
338
  },
306
- operations.getContentItem.route((ctx) => ctx.params),
339
+ operations.getContentItem.route((ctx) => ({
340
+ ...ctx.params,
341
+ ...ctx.query,
342
+ })),
307
343
  );
308
344
  const createContentItem = createEndpoint(
309
345
  "/content/:typeSlug",
@@ -356,7 +392,8 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
356
392
  params: z.object({ typeSlug: z.string().min(1) }),
357
393
  query: z
358
394
  .object({ field: z.string(), targetId: z.string() })
359
- .merge(paginationSchema),
395
+ .merge(paginationSchema)
396
+ .merge(contentReadQuerySchema),
360
397
  requireRequest: true,
361
398
  },
362
399
  operations.listContentByRelation.route((ctx) => ({
@@ -387,7 +424,8 @@ export const cmsBackendPlugin = (config: CMSBackendConfig) => {
387
424
  }),
388
425
  query: z
389
426
  .object({ itemId: z.string(), fieldName: z.string() })
390
- .merge(paginationSchema),
427
+ .merge(paginationSchema)
428
+ .merge(contentReadQuerySchema),
391
429
  requireRequest: true,
392
430
  },
393
431
  operations.listInverseRelationItems.route((ctx) => ({
@@ -3,12 +3,34 @@
3
3
  * Shared between query-keys.ts (HTTP path) and prefetchForRoute (DB path)
4
4
  * to prevent key drift between SSR loaders and SSG prefetching.
5
5
  */
6
+ import type { CMSReadOptions } from "../types";
7
+
8
+ import { normalizeContentFields } from "../schemas";
9
+
10
+ /** Shared output discriminator for HTTP resources and database prefetch. */
11
+ export function contentReadDiscriminator(options?: CMSReadOptions) {
12
+ return {
13
+ ...(options?.view ? { view: options.view } : {}),
14
+ ...(options?.fields !== undefined
15
+ ? { fields: normalizeContentFields(options.fields) }
16
+ : {}),
17
+ };
18
+ }
19
+
20
+ /** Preserve default detail identities while separating every output shape. */
21
+ export function contentReadKey(options?: CMSReadOptions) {
22
+ return options?.view || options?.fields !== undefined
23
+ ? [contentReadDiscriminator(options)]
24
+ : [];
25
+ }
6
26
 
7
27
  export interface ContentListDiscriminator {
8
28
  typeSlug: string;
9
29
  limit: number;
10
30
  offset: number;
11
31
  search: string | undefined;
32
+ view?: "content";
33
+ fields?: readonly string[];
12
34
  }
13
35
 
14
36
  /**
@@ -22,8 +44,11 @@ export function contentListDiscriminator(params: {
22
44
  limit?: number;
23
45
  offset?: number;
24
46
  search?: string;
47
+ view?: "content";
48
+ fields?: readonly string[];
25
49
  }): ContentListDiscriminator {
26
50
  return {
51
+ ...contentReadDiscriminator(params),
27
52
  typeSlug: params.typeSlug,
28
53
  limit: params.limit ?? 20,
29
54
  offset: params.offset ?? 0,
@@ -51,12 +76,14 @@ export const CMS_QUERY_KEYS = {
51
76
  limit?: number;
52
77
  offset?: number;
53
78
  search?: string;
79
+ view?: "content";
80
+ fields?: readonly string[];
54
81
  }) => ["cmsContent", "list", contentListDiscriminator(params)] as const,
55
82
 
56
83
  /**
57
84
  * Key for the cmsContent.detail(typeSlug, id) query.
58
85
  * Full key: ["cmsContent", "detail", typeSlug, id]
59
86
  */
60
- contentDetail: (typeSlug: string, id: string) =>
61
- ["cmsContent", "detail", typeSlug, id] as const,
87
+ contentDetail: (typeSlug: string, id: string, options?: CMSReadOptions) =>
88
+ ["cmsContent", "detail", typeSlug, id, ...contentReadKey(options)] as const,
62
89
  };