@btst/stack 2.12.0 → 2.12.1

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 (46) hide show
  1. package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +14 -11
  2. package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +14 -11
  3. package/dist/packages/stack/src/plugins/cms/client/components/forms/relation-field.cjs +87 -15
  4. package/dist/packages/stack/src/plugins/cms/client/components/forms/relation-field.mjs +87 -15
  5. package/dist/packages/stack/src/plugins/cms/schemas.cjs +11 -6
  6. package/dist/packages/stack/src/plugins/cms/schemas.mjs +10 -6
  7. package/dist/packages/ui/src/components/auto-form/fields/array.cjs +181 -4
  8. package/dist/packages/ui/src/components/auto-form/fields/array.mjs +184 -7
  9. package/dist/packages/ui/src/components/auto-form/index.cjs +16 -1
  10. package/dist/packages/ui/src/components/auto-form/index.mjs +17 -2
  11. package/dist/plugins/cms/api/index.cjs +2 -0
  12. package/dist/plugins/cms/api/index.d.cts +3 -2
  13. package/dist/plugins/cms/api/index.d.mts +3 -2
  14. package/dist/plugins/cms/api/index.d.ts +3 -2
  15. package/dist/plugins/cms/api/index.mjs +1 -0
  16. package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
  17. package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
  18. package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
  19. package/dist/plugins/cms/query-keys.d.cts +398 -7
  20. package/dist/plugins/cms/query-keys.d.mts +398 -7
  21. package/dist/plugins/cms/query-keys.d.ts +398 -7
  22. package/dist/plugins/kanban/api/index.d.cts +1 -1
  23. package/dist/plugins/kanban/api/index.d.mts +1 -1
  24. package/dist/plugins/kanban/api/index.d.ts +1 -1
  25. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  26. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  27. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  28. package/dist/plugins/ui-builder/index.d.cts +1 -1
  29. package/dist/plugins/ui-builder/index.d.mts +1 -1
  30. package/dist/plugins/ui-builder/index.d.ts +1 -1
  31. package/dist/shared/{stack.B8QD11QU.d.cts → stack.DFtpFS00.d.cts} +10 -0
  32. package/dist/shared/{stack.B8QD11QU.d.mts → stack.DFtpFS00.d.mts} +10 -0
  33. package/dist/shared/{stack.B8QD11QU.d.ts → stack.DFtpFS00.d.ts} +10 -0
  34. package/package.json +1 -1
  35. package/src/plugins/cms/__tests__/schema-roundtrip.test.ts +203 -0
  36. package/src/plugins/cms/api/index.ts +1 -0
  37. package/src/plugins/cms/api/plugin.ts +33 -16
  38. package/src/plugins/cms/client/components/forms/relation-field.tsx +121 -36
  39. package/src/plugins/cms/schemas.ts +26 -6
  40. package/src/plugins/cms/types.ts +10 -0
  41. package/dist/shared/stack.DbnCLdNF.d.mts +0 -399
  42. package/dist/shared/stack.fXlShnhv.d.ts +0 -399
  43. package/dist/shared/stack.r6HohbyX.d.cts +0 -399
  44. package/dist/shared/{stack.B6S3cgwN.d.cts → stack.CuI_-z-_.d.cts} +16 -16
  45. package/dist/shared/{stack.Bzfx-_lq.d.mts → stack.D2RVkQD8.d.mts} +16 -16
  46. package/dist/shared/{stack.j5SFLC1d.d.ts → stack.DXrOtuek.d.ts} +16 -16
@@ -297,6 +297,13 @@ const cmsBackendPlugin = (config) => {
297
297
  }
298
298
  }),
299
299
  routes: (adapter) => {
300
+ const listContentQuerySchema = schemas.createListContentQuerySchema(
301
+ config.maxPageSize
302
+ );
303
+ const paginationQuerySchema = z.z.object({
304
+ limit: z.z.coerce.number().min(1).max(config.maxPageSize ?? schemas.DEFAULT_MAX_PAGE_SIZE).optional().default(20),
305
+ offset: z.z.coerce.number().min(0).optional().default(0)
306
+ });
300
307
  const getContentType = async (slug) => {
301
308
  await ensureSynced(adapter);
302
309
  return adapter.findOne({
@@ -319,7 +326,7 @@ const cmsBackendPlugin = (config) => {
319
326
  });
320
327
  const typesWithCounts = await Promise.all(
321
328
  contentTypes.map(async (ct) => {
322
- const items = await adapter.findMany({
329
+ const itemCount = await adapter.count({
323
330
  model: "contentItem",
324
331
  where: [
325
332
  {
@@ -331,7 +338,7 @@ const cmsBackendPlugin = (config) => {
331
338
  });
332
339
  return {
333
340
  ...getters.serializeContentType(ct),
334
- itemCount: items.length
341
+ itemCount
335
342
  };
336
343
  })
337
344
  );
@@ -358,7 +365,7 @@ const cmsBackendPlugin = (config) => {
358
365
  {
359
366
  method: "GET",
360
367
  params: z.z.object({ typeSlug: z.z.string() }),
361
- query: schemas.listContentQuerySchema
368
+ query: listContentQuerySchema
362
369
  },
363
370
  async (ctx) => {
364
371
  const { typeSlug } = ctx.params;
@@ -663,10 +670,8 @@ const cmsBackendPlugin = (config) => {
663
670
  params: z.z.object({ typeSlug: z.z.string() }),
664
671
  query: z.z.object({
665
672
  field: z.z.string(),
666
- targetId: z.z.string(),
667
- limit: z.z.coerce.number().min(1).max(100).optional().default(20),
668
- offset: z.z.coerce.number().min(0).optional().default(0)
669
- })
673
+ targetId: z.z.string()
674
+ }).merge(paginationQuerySchema)
670
675
  },
671
676
  async (ctx) => {
672
677
  const { typeSlug } = ctx.params;
@@ -810,10 +815,8 @@ const cmsBackendPlugin = (config) => {
810
815
  }),
811
816
  query: z.z.object({
812
817
  itemId: z.z.string(),
813
- fieldName: z.z.string(),
814
- limit: z.z.coerce.number().min(1).max(100).optional().default(20),
815
- offset: z.z.coerce.number().min(0).optional().default(0)
816
- })
818
+ fieldName: z.z.string()
819
+ }).merge(paginationQuerySchema)
817
820
  },
818
821
  async (ctx) => {
819
822
  const { slug, sourceType } = ctx.params;
@@ -2,7 +2,7 @@ import { defineBackendPlugin, createEndpoint } from '@btst/stack/plugins/api';
2
2
  import { z } from 'zod';
3
3
  import { formSchemaToZod, zodToFormSchema } from '../../../../../ui/src/lib/schema-converter.mjs';
4
4
  import { cmsSchema } from '../db.mjs';
5
- import { listContentQuerySchema } from '../schemas.mjs';
5
+ import { createListContentQuerySchema, DEFAULT_MAX_PAGE_SIZE } from '../schemas.mjs';
6
6
  import { slugify } from '../utils.mjs';
7
7
  import { serializeContentType, getAllContentItems, serializeContentItemWithType, serializeContentItem, getContentItemById, getContentItemBySlug, getAllContentTypes } from './getters.mjs';
8
8
  import { createCMSContentItem } from './mutations.mjs';
@@ -295,6 +295,13 @@ const cmsBackendPlugin = (config) => {
295
295
  }
296
296
  }),
297
297
  routes: (adapter) => {
298
+ const listContentQuerySchema = createListContentQuerySchema(
299
+ config.maxPageSize
300
+ );
301
+ const paginationQuerySchema = z.object({
302
+ limit: z.coerce.number().min(1).max(config.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE).optional().default(20),
303
+ offset: z.coerce.number().min(0).optional().default(0)
304
+ });
298
305
  const getContentType = async (slug) => {
299
306
  await ensureSynced(adapter);
300
307
  return adapter.findOne({
@@ -317,7 +324,7 @@ const cmsBackendPlugin = (config) => {
317
324
  });
318
325
  const typesWithCounts = await Promise.all(
319
326
  contentTypes.map(async (ct) => {
320
- const items = await adapter.findMany({
327
+ const itemCount = await adapter.count({
321
328
  model: "contentItem",
322
329
  where: [
323
330
  {
@@ -329,7 +336,7 @@ const cmsBackendPlugin = (config) => {
329
336
  });
330
337
  return {
331
338
  ...serializeContentType(ct),
332
- itemCount: items.length
339
+ itemCount
333
340
  };
334
341
  })
335
342
  );
@@ -661,10 +668,8 @@ const cmsBackendPlugin = (config) => {
661
668
  params: z.object({ typeSlug: z.string() }),
662
669
  query: z.object({
663
670
  field: z.string(),
664
- targetId: z.string(),
665
- limit: z.coerce.number().min(1).max(100).optional().default(20),
666
- offset: z.coerce.number().min(0).optional().default(0)
667
- })
671
+ targetId: z.string()
672
+ }).merge(paginationQuerySchema)
668
673
  },
669
674
  async (ctx) => {
670
675
  const { typeSlug } = ctx.params;
@@ -808,10 +813,8 @@ const cmsBackendPlugin = (config) => {
808
813
  }),
809
814
  query: z.object({
810
815
  itemId: z.string(),
811
- fieldName: z.string(),
812
- limit: z.coerce.number().min(1).max(100).optional().default(20),
813
- offset: z.coerce.number().min(0).optional().default(0)
814
- })
816
+ fieldName: z.string()
817
+ }).merge(paginationQuerySchema)
815
818
  },
816
819
  async (ctx) => {
817
820
  const { slug, sourceType } = ctx.params;
@@ -3,6 +3,10 @@
3
3
 
4
4
  const jsxRuntime = require('react/jsx-runtime');
5
5
  const React = require('react');
6
+ const reactQuery = require('@tanstack/react-query');
7
+ const client = require('@btst/stack/plugins/client');
8
+ const context = require('@btst/stack/context');
9
+ const plugins_cms_queryKeys = require('../../../../../../../../plugins/cms/query-keys.cjs');
6
10
  const multiSelect = require('../../../../../../../ui/src/components/multi-select.cjs');
7
11
  const button = require('../../../../../../../ui/src/components/button.cjs');
8
12
  const LucideIcons = require('lucide-react');
@@ -12,6 +16,14 @@ const label = require('../../../../../../../ui/src/components/label.cjs');
12
16
  const textarea = require('../../../../../../../ui/src/components/textarea.cjs');
13
17
  const cmsHooks = require('../../hooks/cms-hooks.cjs');
14
18
 
19
+ const RELATION_DETAIL_QUERY_OPTS = {
20
+ retry: false,
21
+ refetchOnWindowFocus: false,
22
+ refetchOnMount: false,
23
+ refetchOnReconnect: false,
24
+ staleTime: 1e3 * 60 * 5,
25
+ gcTime: 1e3 * 60 * 10
26
+ };
15
27
  function RelationField({
16
28
  field,
17
29
  fieldConfigItem,
@@ -23,12 +35,19 @@ function RelationField({
23
35
  const [newItemName, setNewItemName] = React.useState("");
24
36
  const [newItemDescription, setNewItemDescription] = React.useState("");
25
37
  const [createError, setCreateError] = React.useState(null);
38
+ const { apiBaseURL, apiBasePath, headers } = context.usePluginOverrides("cms");
39
+ const listClient = React.useMemo(
40
+ () => client.createApiClient({
41
+ baseURL: apiBaseURL,
42
+ basePath: apiBasePath
43
+ }),
44
+ [apiBaseURL, apiBasePath]
45
+ );
46
+ const cmsQueries = React.useMemo(
47
+ () => plugins_cms_queryKeys.createCMSQueryKeys(listClient, headers),
48
+ [listClient, headers]
49
+ );
26
50
  const isSingleSelect = relation.type === "belongsTo";
27
- const { items: availableItems, isLoading } = cmsHooks.useContent(relation.targetType, {
28
- limit: 100
29
- // Load a good chunk for the dropdown
30
- });
31
- const createMutation = cmsHooks.useCreateContent(relation.targetType);
32
51
  const normalizedValue = React.useMemo(() => {
33
52
  if (!field.value) return [];
34
53
  if (isSingleSelect) {
@@ -40,8 +59,42 @@ function RelationField({
40
59
  }
41
60
  return field.value || [];
42
61
  }, [field.value, isSingleSelect]);
62
+ const { items: availableItems, isLoading } = cmsHooks.useContent(relation.targetType, {
63
+ limit: 500
64
+ });
65
+ const missingDetailIds = React.useMemo(() => {
66
+ const loadedIds = new Set(availableItems.map((i) => i.id));
67
+ return normalizedValue.map((v) => v.id).filter((id) => id.length > 0 && !loadedIds.has(id));
68
+ }, [availableItems, normalizedValue]);
69
+ const hydrationResult = reactQuery.useQueries({
70
+ queries: missingDetailIds.map((id) => ({
71
+ ...cmsQueries.cmsContent.detail(relation.targetType, id),
72
+ ...RELATION_DETAIL_QUERY_OPTS,
73
+ enabled: Boolean(relation.targetType && id)
74
+ })),
75
+ combine: (results) => ({
76
+ data: results.map(
77
+ (r) => r.data
78
+ ),
79
+ isHydrating: results.some((r) => r.isFetching)
80
+ })
81
+ });
82
+ const isHydratingLabels = hydrationResult.isHydrating;
83
+ const itemById = React.useMemo(() => {
84
+ const m = /* @__PURE__ */ new Map();
85
+ for (const it of availableItems) {
86
+ m.set(it.id, it);
87
+ }
88
+ for (let i = 0; i < missingDetailIds.length; i++) {
89
+ const row = hydrationResult.data[i];
90
+ if (row?.id) {
91
+ m.set(row.id, row);
92
+ }
93
+ }
94
+ return m;
95
+ }, [availableItems, missingDetailIds, hydrationResult.data]);
43
96
  const selectedOptions = normalizedValue.map((v) => {
44
- const item = availableItems.find((item2) => item2.id === v.id);
97
+ const item = itemById.get(v.id);
45
98
  if (item) {
46
99
  const displayValue = item.parsedData?.[relation.displayField] || item.slug;
47
100
  return {
@@ -50,14 +103,33 @@ function RelationField({
50
103
  };
51
104
  }
52
105
  return { value: v.id, label: `ID: ${v.id.slice(0, 8)}...` };
53
- }).filter(Boolean);
54
- const options = availableItems.map((item) => {
55
- const displayValue = item.parsedData?.[relation.displayField] || item.slug;
56
- return {
57
- value: item.id,
58
- label: String(displayValue)
59
- };
60
106
  });
107
+ const options = React.useMemo(() => {
108
+ const merged = [
109
+ ...availableItems
110
+ ];
111
+ const seen = new Set(merged.map((x) => x.id));
112
+ for (let i = 0; i < missingDetailIds.length; i++) {
113
+ const row = hydrationResult.data[i];
114
+ if (row?.id && !seen.has(row.id)) {
115
+ merged.push(row);
116
+ seen.add(row.id);
117
+ }
118
+ }
119
+ return merged.map((item) => {
120
+ const displayValue = item.parsedData?.[relation.displayField] || item.slug;
121
+ return {
122
+ value: item.id,
123
+ label: String(displayValue)
124
+ };
125
+ });
126
+ }, [
127
+ availableItems,
128
+ hydrationResult.data,
129
+ missingDetailIds,
130
+ relation.displayField
131
+ ]);
132
+ const createMutation = cmsHooks.useCreateContent(relation.targetType);
61
133
  const handleChange = React.useCallback(
62
134
  (newOptions) => {
63
135
  if (isSingleSelect) {
@@ -121,8 +193,8 @@ function RelationField({
121
193
  value: selectedOptions,
122
194
  onChange: handleChange,
123
195
  options,
124
- placeholder: isLoading ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
125
- disabled: isLoading,
196
+ placeholder: isLoading || isHydratingLabels ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
197
+ disabled: isLoading || isHydratingLabels,
126
198
  hidePlaceholderWhenSelected: true,
127
199
  emptyIndicator: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-center text-sm text-muted-foreground py-4", children: [
128
200
  "No ",
@@ -1,6 +1,10 @@
1
1
  "use client";
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import { useState, useMemo, useCallback } from 'react';
4
+ import { useQueries } from '@tanstack/react-query';
5
+ import { createApiClient } from '@btst/stack/plugins/client';
6
+ import { usePluginOverrides } from '@btst/stack/context';
7
+ import { createCMSQueryKeys } from '../../../../../../../../plugins/cms/query-keys.mjs';
4
8
  import MultipleSelector from '../../../../../../../ui/src/components/multi-select.mjs';
5
9
  import { Button } from '../../../../../../../ui/src/components/button.mjs';
6
10
  import { Plus, X } from 'lucide-react';
@@ -10,6 +14,14 @@ import { Label } from '../../../../../../../ui/src/components/label.mjs';
10
14
  import { Textarea } from '../../../../../../../ui/src/components/textarea.mjs';
11
15
  import { useContent, useCreateContent } from '../../hooks/cms-hooks.mjs';
12
16
 
17
+ const RELATION_DETAIL_QUERY_OPTS = {
18
+ retry: false,
19
+ refetchOnWindowFocus: false,
20
+ refetchOnMount: false,
21
+ refetchOnReconnect: false,
22
+ staleTime: 1e3 * 60 * 5,
23
+ gcTime: 1e3 * 60 * 10
24
+ };
13
25
  function RelationField({
14
26
  field,
15
27
  fieldConfigItem,
@@ -21,12 +33,19 @@ function RelationField({
21
33
  const [newItemName, setNewItemName] = useState("");
22
34
  const [newItemDescription, setNewItemDescription] = useState("");
23
35
  const [createError, setCreateError] = useState(null);
36
+ const { apiBaseURL, apiBasePath, headers } = usePluginOverrides("cms");
37
+ const listClient = useMemo(
38
+ () => createApiClient({
39
+ baseURL: apiBaseURL,
40
+ basePath: apiBasePath
41
+ }),
42
+ [apiBaseURL, apiBasePath]
43
+ );
44
+ const cmsQueries = useMemo(
45
+ () => createCMSQueryKeys(listClient, headers),
46
+ [listClient, headers]
47
+ );
24
48
  const isSingleSelect = relation.type === "belongsTo";
25
- const { items: availableItems, isLoading } = useContent(relation.targetType, {
26
- limit: 100
27
- // Load a good chunk for the dropdown
28
- });
29
- const createMutation = useCreateContent(relation.targetType);
30
49
  const normalizedValue = useMemo(() => {
31
50
  if (!field.value) return [];
32
51
  if (isSingleSelect) {
@@ -38,8 +57,42 @@ function RelationField({
38
57
  }
39
58
  return field.value || [];
40
59
  }, [field.value, isSingleSelect]);
60
+ const { items: availableItems, isLoading } = useContent(relation.targetType, {
61
+ limit: 500
62
+ });
63
+ const missingDetailIds = useMemo(() => {
64
+ const loadedIds = new Set(availableItems.map((i) => i.id));
65
+ return normalizedValue.map((v) => v.id).filter((id) => id.length > 0 && !loadedIds.has(id));
66
+ }, [availableItems, normalizedValue]);
67
+ const hydrationResult = useQueries({
68
+ queries: missingDetailIds.map((id) => ({
69
+ ...cmsQueries.cmsContent.detail(relation.targetType, id),
70
+ ...RELATION_DETAIL_QUERY_OPTS,
71
+ enabled: Boolean(relation.targetType && id)
72
+ })),
73
+ combine: (results) => ({
74
+ data: results.map(
75
+ (r) => r.data
76
+ ),
77
+ isHydrating: results.some((r) => r.isFetching)
78
+ })
79
+ });
80
+ const isHydratingLabels = hydrationResult.isHydrating;
81
+ const itemById = useMemo(() => {
82
+ const m = /* @__PURE__ */ new Map();
83
+ for (const it of availableItems) {
84
+ m.set(it.id, it);
85
+ }
86
+ for (let i = 0; i < missingDetailIds.length; i++) {
87
+ const row = hydrationResult.data[i];
88
+ if (row?.id) {
89
+ m.set(row.id, row);
90
+ }
91
+ }
92
+ return m;
93
+ }, [availableItems, missingDetailIds, hydrationResult.data]);
41
94
  const selectedOptions = normalizedValue.map((v) => {
42
- const item = availableItems.find((item2) => item2.id === v.id);
95
+ const item = itemById.get(v.id);
43
96
  if (item) {
44
97
  const displayValue = item.parsedData?.[relation.displayField] || item.slug;
45
98
  return {
@@ -48,14 +101,33 @@ function RelationField({
48
101
  };
49
102
  }
50
103
  return { value: v.id, label: `ID: ${v.id.slice(0, 8)}...` };
51
- }).filter(Boolean);
52
- const options = availableItems.map((item) => {
53
- const displayValue = item.parsedData?.[relation.displayField] || item.slug;
54
- return {
55
- value: item.id,
56
- label: String(displayValue)
57
- };
58
104
  });
105
+ const options = useMemo(() => {
106
+ const merged = [
107
+ ...availableItems
108
+ ];
109
+ const seen = new Set(merged.map((x) => x.id));
110
+ for (let i = 0; i < missingDetailIds.length; i++) {
111
+ const row = hydrationResult.data[i];
112
+ if (row?.id && !seen.has(row.id)) {
113
+ merged.push(row);
114
+ seen.add(row.id);
115
+ }
116
+ }
117
+ return merged.map((item) => {
118
+ const displayValue = item.parsedData?.[relation.displayField] || item.slug;
119
+ return {
120
+ value: item.id,
121
+ label: String(displayValue)
122
+ };
123
+ });
124
+ }, [
125
+ availableItems,
126
+ hydrationResult.data,
127
+ missingDetailIds,
128
+ relation.displayField
129
+ ]);
130
+ const createMutation = useCreateContent(relation.targetType);
59
131
  const handleChange = useCallback(
60
132
  (newOptions) => {
61
133
  if (isSingleSelect) {
@@ -119,8 +191,8 @@ function RelationField({
119
191
  value: selectedOptions,
120
192
  onChange: handleChange,
121
193
  options,
122
- placeholder: isLoading ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
123
- disabled: isLoading,
194
+ placeholder: isLoading || isHydratingLabels ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
195
+ disabled: isLoading || isHydratingLabels,
124
196
  hidePlaceholderWhenSelected: true,
125
197
  emptyIndicator: /* @__PURE__ */ jsxs("p", { className: "text-center text-sm text-muted-foreground py-4", children: [
126
198
  "No ",
@@ -2,11 +2,15 @@
2
2
 
3
3
  const z = require('zod');
4
4
 
5
- const listContentQuerySchema = z.z.object({
6
- slug: z.z.string().optional(),
7
- limit: z.z.coerce.number().min(1).max(100).optional().default(20),
8
- offset: z.z.coerce.number().min(0).optional().default(0)
9
- });
5
+ const DEFAULT_MAX_PAGE_SIZE = 1e3;
6
+ function createListContentQuerySchema(maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
7
+ return z.z.object({
8
+ slug: z.z.string().optional(),
9
+ limit: z.z.coerce.number().min(1).max(maxPageSize).optional().default(20),
10
+ offset: z.z.coerce.number().min(0).optional().default(0)
11
+ });
12
+ }
13
+ createListContentQuerySchema();
10
14
  z.z.object({
11
15
  slug: z.z.string().min(1, "Slug is required"),
12
16
  // Use passthrough object instead of z.record(z.unknown()) due to Zod v4 bug
@@ -47,7 +51,8 @@ z.z.object({
47
51
  offset: z.z.number()
48
52
  });
49
53
 
54
+ exports.DEFAULT_MAX_PAGE_SIZE = DEFAULT_MAX_PAGE_SIZE;
50
55
  exports.contentItemResponseSchema = contentItemResponseSchema;
51
56
  exports.contentItemWithDataResponseSchema = contentItemWithDataResponseSchema;
52
57
  exports.contentTypeResponseSchema = contentTypeResponseSchema;
53
- exports.listContentQuerySchema = listContentQuerySchema;
58
+ exports.createListContentQuerySchema = createListContentQuerySchema;
@@ -1,10 +1,14 @@
1
1
  import { z } from 'zod';
2
2
 
3
- const listContentQuerySchema = z.object({
4
- slug: z.string().optional(),
5
- limit: z.coerce.number().min(1).max(100).optional().default(20),
6
- offset: z.coerce.number().min(0).optional().default(0)
7
- });
3
+ const DEFAULT_MAX_PAGE_SIZE = 1e3;
4
+ function createListContentQuerySchema(maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
5
+ return z.object({
6
+ slug: z.string().optional(),
7
+ limit: z.coerce.number().min(1).max(maxPageSize).optional().default(20),
8
+ offset: z.coerce.number().min(0).optional().default(0)
9
+ });
10
+ }
11
+ createListContentQuerySchema();
8
12
  z.object({
9
13
  slug: z.string().min(1, "Slug is required"),
10
14
  // Use passthrough object instead of z.record(z.unknown()) due to Zod v4 bug
@@ -45,4 +49,4 @@ z.object({
45
49
  offset: z.number()
46
50
  });
47
51
 
48
- export { contentItemResponseSchema, contentItemWithDataResponseSchema, contentTypeResponseSchema, listContentQuerySchema };
52
+ export { DEFAULT_MAX_PAGE_SIZE, contentItemResponseSchema, contentItemWithDataResponseSchema, contentTypeResponseSchema, createListContentQuerySchema };