includio-cms 0.21.0 → 0.23.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 (81) hide show
  1. package/API.md +20 -20
  2. package/CHANGELOG.md +111 -0
  3. package/DOCS.md +1169 -146
  4. package/README.md +138 -32
  5. package/ROADMAP.md +8 -330
  6. package/dist/admin/api/rest/handler.d.ts +13 -1
  7. package/dist/admin/api/rest/handler.js +13 -1
  8. package/dist/admin/api/rest/middleware/generateApiKey.d.ts +9 -0
  9. package/dist/admin/api/rest/middleware/generateApiKey.js +9 -0
  10. package/dist/admin/client/collection/collection-entries.svelte +1 -1
  11. package/dist/admin/client/collection/empty-state.svelte +1 -1
  12. package/dist/admin/client/collection/row-actions.svelte +3 -3
  13. package/dist/admin/client/collection/table-toolbar.svelte +3 -1
  14. package/dist/admin/client/entry/entry-header.svelte +3 -1
  15. package/dist/admin/client/users/create-user-dialog.svelte +4 -4
  16. package/dist/admin/client/users/delete-user-dialog.svelte +2 -0
  17. package/dist/admin/client/users/users-page.svelte +3 -2
  18. package/dist/admin/components/media/file-upload.svelte +2 -0
  19. package/dist/ai-claude/index.d.ts +9 -1
  20. package/dist/ai-claude/index.js +9 -1
  21. package/dist/ai-openai/index.d.ts +9 -1
  22. package/dist/ai-openai/index.js +9 -1
  23. package/dist/cli/index.js +115 -13
  24. package/dist/cms/runtime/schema.d.ts +2 -0
  25. package/dist/cms/runtime/schema.js +4 -0
  26. package/dist/cms/runtime/types.d.ts +1 -1
  27. package/dist/core/cms.d.ts +13 -1
  28. package/dist/core/cms.js +13 -1
  29. package/dist/core/errors.d.ts +71 -0
  30. package/dist/core/errors.js +179 -0
  31. package/dist/core/server/consentLogs/operations/create.d.ts +13 -1
  32. package/dist/core/server/consentLogs/operations/create.js +13 -1
  33. package/dist/core/server/entries/operations/create.js +6 -1
  34. package/dist/core/server/entries/operations/get.js +14 -3
  35. package/dist/core/server/entries/operations/resolveEntry.d.ts +32 -1
  36. package/dist/core/server/entries/operations/resolveEntry.js +36 -4
  37. package/dist/core/server/entries/operations/update.js +5 -1
  38. package/dist/core/server/fields/utils/resolveMedia.d.ts +18 -1
  39. package/dist/core/server/fields/utils/resolveMedia.js +13 -1
  40. package/dist/core/server/forms/submissions/operations/create.d.ts +21 -1
  41. package/dist/core/server/forms/submissions/operations/create.js +18 -2
  42. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +15 -1
  43. package/dist/core/server/forms/submissions/utils/parseMultipart.js +15 -1
  44. package/dist/db-postgres/index.d.ts +10 -0
  45. package/dist/db-postgres/index.js +10 -0
  46. package/dist/email-nodemailer/index.d.ts +13 -1
  47. package/dist/email-nodemailer/index.js +13 -1
  48. package/dist/entity/index.d.ts +16 -1
  49. package/dist/entity/index.js +16 -1
  50. package/dist/files-local/index.d.ts +12 -1
  51. package/dist/files-local/index.js +12 -1
  52. package/dist/paraglide/messages/_index.d.ts +3 -36
  53. package/dist/paraglide/messages/_index.js +3 -71
  54. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  55. package/dist/paraglide/messages/hello_world.js +33 -0
  56. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  57. package/dist/paraglide/messages/login_hello.js +34 -0
  58. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  59. package/dist/paraglide/messages/login_please_login.js +34 -0
  60. package/dist/server/auth.d.ts +11 -0
  61. package/dist/server/auth.js +11 -0
  62. package/dist/sveltekit/config.d.ts +67 -4
  63. package/dist/sveltekit/config.js +73 -4
  64. package/dist/sveltekit/server/handle.d.ts +15 -1
  65. package/dist/sveltekit/server/handle.js +15 -1
  66. package/dist/sveltekit/server/layout.d.ts +12 -1
  67. package/dist/sveltekit/server/layout.js +12 -1
  68. package/dist/sveltekit/server/preview.d.ts +21 -1
  69. package/dist/sveltekit/server/preview.js +21 -1
  70. package/dist/types/cms.schema.d.ts +452 -0
  71. package/dist/types/cms.schema.js +629 -0
  72. package/dist/updates/0.22.0/index.d.ts +2 -0
  73. package/dist/updates/0.22.0/index.js +75 -0
  74. package/dist/updates/0.23.0/index.d.ts +2 -0
  75. package/dist/updates/0.23.0/index.js +21 -0
  76. package/dist/updates/index.js +3 -1
  77. package/package.json +4 -1
  78. package/dist/paraglide/messages/en.d.ts +0 -5
  79. package/dist/paraglide/messages/en.js +0 -14
  80. package/dist/paraglide/messages/pl.d.ts +0 -5
  81. package/dist/paraglide/messages/pl.js +0 -14
@@ -0,0 +1,629 @@
1
+ import { z } from 'zod';
2
+ /* -------------------------------------------------------------------------- */
3
+ /* Localized */
4
+ /* -------------------------------------------------------------------------- */
5
+ const localizedSchema = z.union([z.string(), z.record(z.string(), z.string())]);
6
+ /* -------------------------------------------------------------------------- */
7
+ /* Slug */
8
+ /* -------------------------------------------------------------------------- */
9
+ const SLUG_RE = /^[a-z][a-z0-9-]*$/;
10
+ const slugSchema = z.string().regex(SLUG_RE, {
11
+ message: 'must be lowercase letters/digits/hyphens, starting with a letter'
12
+ });
13
+ /* -------------------------------------------------------------------------- */
14
+ /* Languages */
15
+ /* -------------------------------------------------------------------------- */
16
+ const LANG_CODE_RE = /^[a-z]{2}(-[A-Z]{2})?$/;
17
+ const languageEntrySchema = z.union([
18
+ z.string().regex(LANG_CODE_RE, {
19
+ message: "must be a 2-letter ISO code (e.g. 'en' or 'pl-PL')"
20
+ }),
21
+ z
22
+ .object({
23
+ code: z.string().regex(LANG_CODE_RE, {
24
+ message: "must be a 2-letter ISO code (e.g. 'en' or 'pl-PL')"
25
+ }),
26
+ label: localizedSchema.optional(),
27
+ default: z.boolean().optional()
28
+ })
29
+ .passthrough()
30
+ ]);
31
+ /* -------------------------------------------------------------------------- */
32
+ /* Fields */
33
+ /* -------------------------------------------------------------------------- */
34
+ const FIELD_TYPES = [
35
+ 'text',
36
+ 'content',
37
+ 'number',
38
+ 'boolean',
39
+ 'date',
40
+ 'datetime',
41
+ 'file',
42
+ 'media',
43
+ 'select',
44
+ 'radio',
45
+ 'checkboxes',
46
+ 'relation',
47
+ 'object',
48
+ 'array',
49
+ 'blocks',
50
+ 'slug',
51
+ 'seo',
52
+ 'shop',
53
+ 'url',
54
+ 'custom'
55
+ ];
56
+ const baseFieldShape = {
57
+ slug: slugSchema,
58
+ label: localizedSchema.optional(),
59
+ required: z.boolean().optional(),
60
+ description: localizedSchema.optional(),
61
+ localized: z.boolean().optional(),
62
+ defaultValue: z.unknown().optional(),
63
+ showWhen: z
64
+ .object({
65
+ field: z.string(),
66
+ equals: z.union([z.string(), z.array(z.string())]).optional(),
67
+ notEquals: z.union([z.string(), z.array(z.string())]).optional()
68
+ })
69
+ .optional()
70
+ };
71
+ const optionItemSchema = z.object({
72
+ label: localizedSchema.optional(),
73
+ value: z.string()
74
+ });
75
+ const fieldSchema = z.lazy(() => z.discriminatedUnion('type', [
76
+ // text
77
+ z
78
+ .object({
79
+ ...baseFieldShape,
80
+ type: z.literal('text'),
81
+ placeholder: localizedSchema.optional(),
82
+ minLength: z.number().int().nonnegative().optional(),
83
+ maxLength: z.number().int().positive().optional(),
84
+ pattern: z.string().optional(),
85
+ multiline: z.boolean().optional()
86
+ })
87
+ .passthrough(),
88
+ // content
89
+ z
90
+ .object({
91
+ ...baseFieldShape,
92
+ type: z.literal('content'),
93
+ inlineBlocks: z.array(fieldSchema).optional()
94
+ })
95
+ .passthrough(),
96
+ // number
97
+ z
98
+ .object({
99
+ ...baseFieldShape,
100
+ type: z.literal('number'),
101
+ min: z.number().optional(),
102
+ max: z.number().optional(),
103
+ step: z.number().optional()
104
+ })
105
+ .passthrough(),
106
+ // boolean
107
+ z
108
+ .object({ ...baseFieldShape, type: z.literal('boolean') })
109
+ .passthrough(),
110
+ // date / datetime
111
+ z
112
+ .object({
113
+ ...baseFieldShape,
114
+ type: z.literal('date'),
115
+ minDate: z.string().optional(),
116
+ maxDate: z.string().optional()
117
+ })
118
+ .passthrough(),
119
+ z
120
+ .object({
121
+ ...baseFieldShape,
122
+ type: z.literal('datetime'),
123
+ minDate: z.string().optional(),
124
+ maxDate: z.string().optional()
125
+ })
126
+ .passthrough(),
127
+ // file
128
+ z
129
+ .object({
130
+ ...baseFieldShape,
131
+ type: z.literal('file'),
132
+ accept: z.string().optional(),
133
+ maxSizeMB: z.number().positive().optional(),
134
+ multiple: z.boolean().optional()
135
+ })
136
+ .passthrough(),
137
+ // media
138
+ z
139
+ .object({
140
+ ...baseFieldShape,
141
+ type: z.literal('media'),
142
+ accept: z.string().optional(),
143
+ maxSizeMB: z.number().positive().optional(),
144
+ multiple: z.boolean().optional(),
145
+ styles: z
146
+ .array(z
147
+ .object({
148
+ name: z.string()
149
+ })
150
+ .passthrough())
151
+ .optional()
152
+ })
153
+ .passthrough(),
154
+ // select
155
+ z
156
+ .object({
157
+ ...baseFieldShape,
158
+ type: z.literal('select'),
159
+ options: z.array(optionItemSchema).min(1, {
160
+ message: 'select must declare at least one option'
161
+ }),
162
+ multiple: z.boolean().optional()
163
+ })
164
+ .passthrough(),
165
+ // radio
166
+ z
167
+ .object({
168
+ ...baseFieldShape,
169
+ type: z.literal('radio'),
170
+ options: z.array(optionItemSchema).min(1, {
171
+ message: 'radio must declare at least one option'
172
+ })
173
+ })
174
+ .passthrough(),
175
+ // checkboxes
176
+ z
177
+ .object({
178
+ ...baseFieldShape,
179
+ type: z.literal('checkboxes'),
180
+ options: z.array(optionItemSchema).min(1, {
181
+ message: 'checkboxes must declare at least one option'
182
+ })
183
+ })
184
+ .passthrough(),
185
+ // relation
186
+ z
187
+ .object({
188
+ ...baseFieldShape,
189
+ type: z.literal('relation'),
190
+ collection: z.string().min(1, { message: 'relation.collection is required' }),
191
+ multiple: z.boolean().optional(),
192
+ displayField: z.string().optional()
193
+ })
194
+ .passthrough(),
195
+ // object
196
+ z
197
+ .object({
198
+ ...baseFieldShape,
199
+ type: z.literal('object'),
200
+ fields: z.array(fieldSchema).min(1, {
201
+ message: 'object field must declare at least one nested field'
202
+ }),
203
+ accordionLabelField: z.string().optional(),
204
+ thumbnail: z.string().optional()
205
+ })
206
+ .passthrough(),
207
+ // array
208
+ z
209
+ .object({
210
+ ...baseFieldShape,
211
+ type: z.literal('array'),
212
+ of: z.enum(['text', 'number', 'url']),
213
+ minItems: z.number().int().nonnegative().optional(),
214
+ maxItems: z.number().int().positive().optional()
215
+ })
216
+ .passthrough(),
217
+ // blocks
218
+ z
219
+ .object({
220
+ ...baseFieldShape,
221
+ type: z.literal('blocks'),
222
+ of: z.array(fieldSchema).min(1, {
223
+ message: 'blocks.of must declare at least one block type'
224
+ }),
225
+ minItems: z.number().int().nonnegative().optional(),
226
+ maxItems: z.number().int().positive().optional(),
227
+ displayMode: z.enum(['simple', 'blocks']).optional()
228
+ })
229
+ .passthrough(),
230
+ // slug
231
+ z
232
+ .object({
233
+ ...baseFieldShape,
234
+ type: z.literal('slug'),
235
+ pattern: z.string().optional(),
236
+ sourceField: z.string().optional()
237
+ })
238
+ .passthrough(),
239
+ // seo
240
+ z
241
+ .object({
242
+ ...baseFieldShape,
243
+ type: z.literal('seo'),
244
+ slugSource: z.string().optional(),
245
+ titleSource: z.string().optional()
246
+ })
247
+ .passthrough(),
248
+ // shop
249
+ z
250
+ .object({ ...baseFieldShape, type: z.literal('shop') })
251
+ .passthrough(),
252
+ // url
253
+ z
254
+ .object({
255
+ ...baseFieldShape,
256
+ type: z.literal('url'),
257
+ placeholder: localizedSchema.optional(),
258
+ text: z.boolean().optional(),
259
+ newTab: z.boolean().optional(),
260
+ rel: z.boolean().optional()
261
+ })
262
+ .passthrough(),
263
+ // custom (plugin-provided)
264
+ z
265
+ .object({
266
+ ...baseFieldShape,
267
+ type: z.literal('custom'),
268
+ fieldType: z.string().min(1, { message: 'custom.fieldType is required' }),
269
+ config: z.record(z.string(), z.unknown()).optional()
270
+ })
271
+ .passthrough()
272
+ ]));
273
+ /* -------------------------------------------------------------------------- */
274
+ /* Form fields */
275
+ /* -------------------------------------------------------------------------- */
276
+ const FORM_FIELD_TYPES = ['text', 'email', 'textarea', 'checkbox', 'select', 'file'];
277
+ const formFieldBaseShape = {
278
+ slug: slugSchema,
279
+ label: localizedSchema.optional(),
280
+ required: z.boolean().optional(),
281
+ description: localizedSchema.optional(),
282
+ errorMessage: localizedSchema.optional(),
283
+ defaultValue: z.unknown().optional(),
284
+ showInDataTable: z.boolean().optional()
285
+ };
286
+ const formFieldSchema = z.discriminatedUnion('type', [
287
+ z
288
+ .object({
289
+ ...formFieldBaseShape,
290
+ type: z.literal('text'),
291
+ minLength: z.number().int().nonnegative().optional(),
292
+ maxLength: z.number().int().positive().optional()
293
+ })
294
+ .passthrough(),
295
+ z
296
+ .object({
297
+ ...formFieldBaseShape,
298
+ type: z.literal('email')
299
+ })
300
+ .passthrough(),
301
+ z
302
+ .object({
303
+ ...formFieldBaseShape,
304
+ type: z.literal('textarea'),
305
+ minLength: z.number().int().nonnegative().optional(),
306
+ maxLength: z.number().int().positive().optional()
307
+ })
308
+ .passthrough(),
309
+ z
310
+ .object({
311
+ ...formFieldBaseShape,
312
+ type: z.literal('checkbox')
313
+ })
314
+ .passthrough(),
315
+ z
316
+ .object({
317
+ ...formFieldBaseShape,
318
+ type: z.literal('select'),
319
+ options: z.array(optionItemSchema).min(1, {
320
+ message: 'form select must declare at least one option'
321
+ })
322
+ })
323
+ .passthrough(),
324
+ z
325
+ .object({
326
+ ...formFieldBaseShape,
327
+ type: z.literal('file'),
328
+ accept: z.string().optional(),
329
+ maxSize: z.number().positive().optional()
330
+ })
331
+ .passthrough()
332
+ ]);
333
+ /* -------------------------------------------------------------------------- */
334
+ /* Collections / Singles / Forms */
335
+ /* -------------------------------------------------------------------------- */
336
+ const configBaseShape = {
337
+ slug: slugSchema,
338
+ sidebarIcon: z.string().optional(),
339
+ previewUrl: z.string().optional(),
340
+ fields: z.array(fieldSchema).min(1, {
341
+ message: 'must declare at least one field'
342
+ }),
343
+ layout: z.unknown().optional(),
344
+ slugField: z.string().optional(),
345
+ pathTemplate: z.string().optional()
346
+ };
347
+ const collectionSchema = z
348
+ .object({
349
+ ...configBaseShape,
350
+ entryAdminTitle: z.string().optional(),
351
+ labels: z
352
+ .object({
353
+ singular: localizedSchema.optional(),
354
+ plural: localizedSchema.optional()
355
+ })
356
+ .optional(),
357
+ orderable: z.boolean().optional(),
358
+ listColumns: z.array(z.string()).optional()
359
+ })
360
+ .passthrough();
361
+ const singleSchema = z
362
+ .object({
363
+ ...configBaseShape,
364
+ label: localizedSchema.optional()
365
+ })
366
+ .passthrough();
367
+ const formSchema = z
368
+ .object({
369
+ slug: slugSchema,
370
+ sidebarIcon: z.string().optional(),
371
+ fields: z.array(formFieldSchema).min(1, {
372
+ message: 'form must declare at least one field'
373
+ }),
374
+ label: localizedSchema,
375
+ notificationEmailAddresses: z.array(z.string().email()).optional()
376
+ })
377
+ .passthrough();
378
+ /* -------------------------------------------------------------------------- */
379
+ /* Adapters */
380
+ /* -------------------------------------------------------------------------- */
381
+ function adapterMethodsCheck(name, methods) {
382
+ return z.custom((val) => {
383
+ if (val == null || typeof val !== 'object')
384
+ return false;
385
+ for (const m of methods) {
386
+ if (typeof val[m] !== 'function')
387
+ return false;
388
+ }
389
+ return true;
390
+ }, {
391
+ message: `${name} is missing required methods (${methods.join(', ')})`
392
+ });
393
+ }
394
+ const dbAdapterSchema = adapterMethodsCheck('DatabaseAdapter', [
395
+ 'createEntry',
396
+ 'getEntries',
397
+ 'updateEntry',
398
+ 'createEntryVersion',
399
+ 'getEntryVersions',
400
+ 'createFormSubmission'
401
+ ]);
402
+ const filesAdapterSchema = adapterMethodsCheck('FilesAdapter', [
403
+ 'uploadFile',
404
+ 'downloadFile',
405
+ 'renameFile',
406
+ 'deleteFile',
407
+ 'listFiles'
408
+ ]);
409
+ const emailAdapterSchema = adapterMethodsCheck('EmailAdapter', ['sendMail']);
410
+ const aiAdapterSchema = z
411
+ .custom((val) => val != null && typeof val === 'object', {
412
+ message: 'AIAdapter must be an object'
413
+ });
414
+ /* -------------------------------------------------------------------------- */
415
+ /* ApiKeys */
416
+ /* -------------------------------------------------------------------------- */
417
+ const apiKeySchema = z
418
+ .object({
419
+ key: z.string().min(8, { message: 'apiKey.key must be at least 8 chars' }),
420
+ name: z.string().optional(),
421
+ role: z.enum(['admin', 'editor']).optional(),
422
+ expiresAt: z.string().optional(),
423
+ rotatedAt: z.string().optional(),
424
+ permissions: z.array(z.string()).optional()
425
+ })
426
+ .passthrough();
427
+ /* -------------------------------------------------------------------------- */
428
+ /* CMSConfig */
429
+ /* -------------------------------------------------------------------------- */
430
+ const cmsConfigBaseSchema = z
431
+ .object({
432
+ languages: z
433
+ .array(languageEntrySchema)
434
+ .min(1, { message: 'CMSConfig.languages must contain at least one language' }),
435
+ collections: z.array(collectionSchema).optional(),
436
+ singles: z.array(singleSchema).optional(),
437
+ forms: z.array(formSchema).optional(),
438
+ db: dbAdapterSchema,
439
+ files: filesAdapterSchema,
440
+ email: emailAdapterSchema.optional(),
441
+ auth: z
442
+ .object({
443
+ secret: z.string().min(1, { message: 'auth.secret is required' }),
444
+ baseURL: z.string().optional()
445
+ })
446
+ .passthrough()
447
+ .optional(),
448
+ plugins: z.array(z.unknown()).optional(),
449
+ ai: aiAdapterSchema.optional(),
450
+ media: z.unknown().optional(),
451
+ apiKeys: z.array(apiKeySchema).optional(),
452
+ typography: z
453
+ .object({ fixOrphans: z.boolean().optional() })
454
+ .passthrough()
455
+ .optional(),
456
+ sidebarHelp: z.boolean().optional(),
457
+ shop: z.unknown().optional(),
458
+ cmp: z.unknown().optional()
459
+ })
460
+ .passthrough();
461
+ /* -------------------------------------------------------------------------- */
462
+ /* Cross-field invariants */
463
+ /* -------------------------------------------------------------------------- */
464
+ function getLangCode(entry) {
465
+ if (typeof entry === 'string')
466
+ return entry;
467
+ if (entry && typeof entry === 'object' && 'code' in entry) {
468
+ const c = entry.code;
469
+ return typeof c === 'string' ? c : null;
470
+ }
471
+ return null;
472
+ }
473
+ function isLangDefault(entry) {
474
+ return !!entry && typeof entry === 'object' && entry.default === true;
475
+ }
476
+ function* walkRelationFields(fields, path) {
477
+ if (!fields)
478
+ return;
479
+ for (let i = 0; i < fields.length; i++) {
480
+ const f = fields[i];
481
+ const fieldPath = [...path, i];
482
+ if (f.type === 'relation')
483
+ yield { field: f, path: fieldPath };
484
+ if (f.type === 'object' && Array.isArray(f.fields)) {
485
+ yield* walkRelationFields(f.fields, [...fieldPath, 'fields']);
486
+ }
487
+ if (f.type === 'blocks' && Array.isArray(f.of)) {
488
+ for (let j = 0; j < f.of.length; j++) {
489
+ const blockDef = f.of[j];
490
+ if (blockDef && Array.isArray(blockDef.fields)) {
491
+ yield* walkRelationFields(blockDef.fields, [...fieldPath, 'of', j, 'fields']);
492
+ }
493
+ }
494
+ }
495
+ if (f.type === 'content' && Array.isArray(f.inlineBlocks)) {
496
+ for (let j = 0; j < f.inlineBlocks.length; j++) {
497
+ const ib = f.inlineBlocks[j];
498
+ if (ib && Array.isArray(ib.fields)) {
499
+ yield* walkRelationFields(ib.fields, [...fieldPath, 'inlineBlocks', j, 'fields']);
500
+ }
501
+ }
502
+ }
503
+ }
504
+ }
505
+ /**
506
+ * Final config schema. Use `cmsConfigSchema.safeParse(input)` and pass any
507
+ * resulting `ZodError` to `formatConfigError` (in `core/errors.ts`) to get a
508
+ * human-readable {@link import('../core/errors.js').ConfigValidationError}.
509
+ *
510
+ * Cross-field invariants enforced via `superRefine`:
511
+ * - `languages` has at most one `default: true`.
512
+ * - `collections[].slug` are unique.
513
+ * - `singles[].slug` are unique.
514
+ * - `forms[].slug` are unique.
515
+ * - Each `relation` field's `collection` references a declared collection.
516
+ * - `apiKeys[].permissions` reference declared collection slugs.
517
+ */
518
+ export const cmsConfigSchema = cmsConfigBaseSchema.superRefine((data, ctx) => {
519
+ // Default language: at most one (zero is fine — fall back to languages[0]).
520
+ const defaults = (data.languages ?? []).filter(isLangDefault);
521
+ if (defaults.length > 1) {
522
+ ctx.addIssue({
523
+ code: z.ZodIssueCode.custom,
524
+ path: ['languages'],
525
+ message: `more than one language is marked default (${defaults.length})`
526
+ });
527
+ }
528
+ // Collection slug uniqueness
529
+ const collectionSlugs = new Set();
530
+ (data.collections ?? []).forEach((c, i) => {
531
+ const slug = c.slug;
532
+ if (typeof slug !== 'string')
533
+ return;
534
+ if (collectionSlugs.has(slug)) {
535
+ ctx.addIssue({
536
+ code: z.ZodIssueCode.custom,
537
+ path: ['collections', i, 'slug'],
538
+ message: `duplicate collection slug '${slug}'`
539
+ });
540
+ }
541
+ collectionSlugs.add(slug);
542
+ });
543
+ // Single slug uniqueness
544
+ const singleSlugs = new Set();
545
+ (data.singles ?? []).forEach((s, i) => {
546
+ const slug = s.slug;
547
+ if (typeof slug !== 'string')
548
+ return;
549
+ if (singleSlugs.has(slug)) {
550
+ ctx.addIssue({
551
+ code: z.ZodIssueCode.custom,
552
+ path: ['singles', i, 'slug'],
553
+ message: `duplicate single slug '${slug}'`
554
+ });
555
+ }
556
+ singleSlugs.add(slug);
557
+ });
558
+ // Form slug uniqueness
559
+ const formSlugs = new Set();
560
+ (data.forms ?? []).forEach((f, i) => {
561
+ const slug = f.slug;
562
+ if (typeof slug !== 'string')
563
+ return;
564
+ if (formSlugs.has(slug)) {
565
+ ctx.addIssue({
566
+ code: z.ZodIssueCode.custom,
567
+ path: ['forms', i, 'slug'],
568
+ message: `duplicate form slug '${slug}'`
569
+ });
570
+ }
571
+ formSlugs.add(slug);
572
+ });
573
+ // Relation targets (must reference a declared collection)
574
+ const validTargets = new Set([...collectionSlugs, ...singleSlugs]);
575
+ (data.collections ?? []).forEach((c, i) => {
576
+ const fields = c.fields;
577
+ for (const { field, path } of walkRelationFields(fields, ['collections', i, 'fields'])) {
578
+ const target = field.collection;
579
+ if (typeof target !== 'string' || !validTargets.has(target)) {
580
+ ctx.addIssue({
581
+ code: z.ZodIssueCode.custom,
582
+ path: [...path, 'collection'],
583
+ message: `relation target '${target}' does not reference a declared collection or single`
584
+ });
585
+ }
586
+ }
587
+ });
588
+ (data.singles ?? []).forEach((s, i) => {
589
+ const fields = s.fields;
590
+ for (const { field, path } of walkRelationFields(fields, ['singles', i, 'fields'])) {
591
+ const target = field.collection;
592
+ if (typeof target !== 'string' || !validTargets.has(target)) {
593
+ ctx.addIssue({
594
+ code: z.ZodIssueCode.custom,
595
+ path: [...path, 'collection'],
596
+ message: `relation target '${target}' does not reference a declared collection or single`
597
+ });
598
+ }
599
+ }
600
+ });
601
+ // apiKeys permissions
602
+ (data.apiKeys ?? []).forEach((k, i) => {
603
+ const perms = k.permissions;
604
+ if (!perms)
605
+ return;
606
+ for (let j = 0; j < perms.length; j++) {
607
+ const p = perms[j];
608
+ if (!collectionSlugs.has(p)) {
609
+ ctx.addIssue({
610
+ code: z.ZodIssueCode.custom,
611
+ path: ['apiKeys', i, 'permissions', j],
612
+ message: `apiKeys permission '${p}' does not reference a declared collection`
613
+ });
614
+ }
615
+ }
616
+ });
617
+ });
618
+ /** Exposed for tests only. Not part of the public API. */
619
+ export const _internal = {
620
+ fieldSchema,
621
+ formFieldSchema,
622
+ collectionSchema,
623
+ singleSchema,
624
+ formSchema,
625
+ cmsConfigBaseSchema,
626
+ FIELD_TYPES,
627
+ FORM_FIELD_TYPES,
628
+ getLangCode
629
+ };
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;