riverbankcms 0.18.0 → 0.20.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.
@@ -1,233 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { a as classifySdkEventSchedule, i as sdkEventUpsertOptionalFieldSchemas, n as sdkCourseBookingSettingsSchema, o as classifySdkEventBookingOffer } from "./sdk-event-upsert-BamyD2d6.mjs";
3
- import { u as EVENT_PRESET_VALUES } from "./src-BhrSG8A4.mjs";
2
+ import { S as normalizeLinkValue, d as classifySdkEventBookingOffer, l as sdkEventUpsertOptionalFieldSchemas, n as createRichTextDoc, s as sdkCourseBookingSettingsSchema, u as classifySdkEventSchedule, x as linkSchema } from "./richTextSchema-DTvrDDF5.mjs";
3
+ import { u as EVENT_PRESET_VALUES } from "./src-Dixcv4PB.mjs";
4
4
  import { z } from "zod";
5
5
 
6
- //#region ../blocks/src/lib/typeGuards.ts
7
- /**
8
- * Strict record guard: excludes null, primitives, and arrays.
9
- */
10
- function isRecord$3(value) {
11
- return typeof value === "object" && value !== null && !Array.isArray(value);
12
- }
13
- /**
14
- * Non-null object guard: includes arrays for behavior parity with legacy helpers.
15
- *
16
- * NOTE: This intentionally narrows to UnknownRecord for compatibility with existing
17
- * callsites even though arrays pass at runtime. Follow-up cleanup should replace
18
- * this API with a more precise predicate type and migrate consumers.
19
- */
20
- function isObjectRecord(value) {
21
- return typeof value === "object" && value !== null;
22
- }
23
- /**
24
- * Coerces unknown input into a strict record, returning an empty object fallback.
25
- */
26
- function asRecord(value) {
27
- return isRecord$3(value) ? value : {};
28
- }
29
-
30
- //#endregion
31
- //#region ../blocks/src/system/manifest/richTextSchema.ts
32
- /**
33
- * Shared Rich Text Schema
34
- *
35
- * This module provides a single source of truth for TipTap rich text validation
36
- * across both the block-form editor and the block rendering system.
37
- *
38
- * Rich-text fields use one canonical root shape:
39
- * `{ type: 'doc', content: [...] }`.
40
- */
41
- /**
42
- * TipTap document node schema (recursive structure)
43
- *
44
- * Supports the full TipTap document structure including:
45
- * - type: Node type (e.g., 'doc', 'paragraph', 'text')
46
- * - content: Nested array of child nodes
47
- * - text: Text content for text nodes
48
- * - attrs: Node attributes (e.g., alignment, color)
49
- * - marks: Text formatting marks (e.g., bold, italic)
50
- */
51
- const TipTapMarkSchema = z.object({
52
- type: z.string().min(1),
53
- attrs: z.record(z.string(), z.unknown()).optional()
54
- }).strict();
55
- const TipTapNodeSchema = z.lazy(() => z.object({
56
- type: z.string().min(1),
57
- content: z.array(TipTapNodeSchema).optional(),
58
- text: z.string().optional(),
59
- attrs: z.record(z.string(), z.unknown()).optional(),
60
- marks: z.array(TipTapMarkSchema).optional()
61
- }).strict().superRefine((node, ctx) => {
62
- if (node.type === "doc") {
63
- ctx.addIssue({
64
- code: "custom",
65
- path: ["type"],
66
- message: "Document nodes are only valid at the rich-text root"
67
- });
68
- }
69
- if (node.type === "text") {
70
- if (node.text === undefined || node.text.length === 0) {
71
- ctx.addIssue({
72
- code: "custom",
73
- path: ["text"],
74
- message: "Text nodes require a non-empty text value"
75
- });
76
- }
77
- if (node.content !== undefined) {
78
- ctx.addIssue({
79
- code: "custom",
80
- path: ["content"],
81
- message: "Text nodes cannot contain child nodes"
82
- });
83
- }
84
- return;
85
- }
86
- if (node.text !== undefined) {
87
- ctx.addIssue({
88
- code: "custom",
89
- path: ["text"],
90
- message: "Only text nodes can contain a text value"
91
- });
92
- }
93
- }));
94
- /**
95
- * Canonical schema for a TipTap rich-text document root.
96
- *
97
- * Stricter than {@link TipTapNodeSchema}: `type` is literally `'doc'` and
98
- * `content` is required (possibly empty). This is the shape every producer
99
- * (dummy content, editor output, migrations) must emit, and the shape every
100
- * non-legacy consumer can rely on.
101
- *
102
- */
103
- const RichTextDocSchema = z.object({
104
- type: z.literal("doc"),
105
- content: z.array(TipTapNodeSchema)
106
- }).strict();
107
- /**
108
- * Type guard for {@link RichTextDoc}.
109
- *
110
- * Returns `true` only for the canonical unwrapped form. The shared structural
111
- * contract lives in `@riverbankcms/core`; parity tests keep its dependency-free
112
- * guard aligned with this detailed Zod validator.
113
- */
114
- function isRichTextDoc(value) {
115
- return RichTextDocSchema.safeParse(value).success;
116
- }
117
- /**
118
- * Build a canonical {@link RichTextDoc} from plain-text paragraphs.
119
- *
120
- * Single source of truth for constructing a TipTap doc from strings.
121
- * Used by:
122
- * - `@riverbankcms/dummy-content` (fixture body generators)
123
- * - `@riverbankcms/content-model` (default template `cta-full` content)
124
- *
125
- * Empty strings produce a paragraph with no `content` array — matching
126
- * the TipTap convention for empty paragraphs. The shape is exactly what
127
- * {@link RichTextDocSchema} validates, so the type and runtime check
128
- * never drift from the constructor.
129
- */
130
- function createRichTextDoc(paragraphs) {
131
- return {
132
- type: "doc",
133
- content: paragraphs.map((text) => text ? {
134
- type: "paragraph",
135
- content: [{
136
- type: "text",
137
- text
138
- }]
139
- } : { type: "paragraph" })
140
- };
141
- }
142
- function createHardBreakNode() {
143
- return { type: "hardBreak" };
144
- }
145
- /**
146
- * Parse a small markdown subset into TipTap inline nodes.
147
- *
148
- * This intentionally supports only bold (`**text**`) because the SDK/content
149
- * import paths use it for lightweight default content, not full markdown.
150
- */
151
- function parseRichTextInlineMarkdown(text) {
152
- const out = [];
153
- const pushText = (value, marks) => {
154
- if (!value) return;
155
- out.push(marks && marks.length > 0 ? {
156
- type: "text",
157
- text: value,
158
- marks
159
- } : {
160
- type: "text",
161
- text: value
162
- });
163
- };
164
- let rest = text;
165
- while (rest.length > 0) {
166
- const start = rest.indexOf("**");
167
- if (start === -1) {
168
- pushText(rest);
169
- break;
170
- }
171
- const end = rest.indexOf("**", start + 2);
172
- if (end === -1) {
173
- pushText(rest);
174
- break;
175
- }
176
- pushText(rest.slice(0, start));
177
- pushText(rest.slice(start + 2, end), [{ type: "bold" }]);
178
- rest = rest.slice(end + 2);
179
- }
180
- return out;
181
- }
182
- function parseRichTextParagraphInlineMarkdown(text) {
183
- return text.split("\n").flatMap((line, index) => {
184
- const lineNodes = parseRichTextInlineMarkdown(line.trim());
185
- return index === 0 ? lineNodes : [createHardBreakNode(), ...lineNodes];
186
- });
187
- }
188
- /**
189
- * Convert a plain string with a tiny markdown subset to canonical TipTap JSON.
190
- *
191
- * Double newlines create semantic paragraphs. Single newlines remain inside the
192
- * current paragraph as TipTap `hardBreak` nodes for line-oriented content such
193
- * as addresses, opening hours, and contact details.
194
- */
195
- function createRichTextDocFromMarkdown(markdown) {
196
- const normalized = markdown.replace(/\r\n/g, "\n").trim();
197
- if (!normalized) return {
198
- type: "doc",
199
- content: []
200
- };
201
- const paragraphs = normalized.split(/\n{2,}/).map((paragraph) => paragraph.trim()).filter(Boolean);
202
- return {
203
- type: "doc",
204
- content: paragraphs.map((paragraph) => {
205
- const inline = parseRichTextParagraphInlineMarkdown(paragraph);
206
- return inline.length > 0 ? {
207
- type: "paragraph",
208
- content: inline
209
- } : { type: "paragraph" };
210
- })
211
- };
212
- }
213
- function buildRichTextSchema(options) {
214
- let schema = RichTextDocSchema;
215
- if (options?.required) {
216
- const label = options.label ?? "This field";
217
- schema = schema.refine((val) => {
218
- const content = val.content;
219
- if (!Array.isArray(content) || content.length === 0) return false;
220
- const first = content[0];
221
- if (content.length === 1 && isRecord$3(first) && first.type === "paragraph" && (!("content" in first) || first.content == null)) {
222
- return false;
223
- }
224
- return true;
225
- }, { message: `${label} is required` });
226
- }
227
- return schema;
228
- }
229
-
230
- //#endregion
231
6
  //#region ../content-model/src/title-resolution.ts
232
7
  /**
233
8
  * Content title resolution utilities.
@@ -449,6 +224,294 @@ function normalizeSlugSegment(segment) {
449
224
  return normalized;
450
225
  }
451
226
 
227
+ //#endregion
228
+ //#region ../content-model/src/content-types/sentinels.ts
229
+ /** Leaf field types that receive a placeholder sentinel (containers recurse). */
230
+ const SENTINEL_LEAF_TYPES = [
231
+ "text",
232
+ "textarea",
233
+ "richText",
234
+ "number",
235
+ "date",
236
+ "datetime",
237
+ "url",
238
+ "link",
239
+ "media",
240
+ "file",
241
+ "boolean",
242
+ "select",
243
+ "reference"
244
+ ];
245
+ /** Matches the closed sentinel shape `"<REQUIRED: …>"`. */
246
+ const SENTINEL_PATTERN = /^<REQUIRED:.*>$/;
247
+ /**
248
+ * True when a value is an unfilled placeholder sentinel. Shared by the emitter
249
+ * (self-check) and the validator (rejection) so recognition can't drift.
250
+ */
251
+ function isPlaceholderSentinel(value) {
252
+ return typeof value === "string" && SENTINEL_PATTERN.test(value);
253
+ }
254
+ function effectiveReferenceKind(field) {
255
+ const kind = field.referenceKind;
256
+ const legacy = field.reference?.type;
257
+ return kind ?? legacy;
258
+ }
259
+ function selectOptionValues$1(field) {
260
+ const options = field.options;
261
+ return options ? options.map((o) => o.value) : [];
262
+ }
263
+ /**
264
+ * The placeholder sentinel for a leaf field. Container types (group/button/
265
+ * repeater) do not have a leaf sentinel — they expand structurally — and calling
266
+ * this for them returns a generic marker only for completeness.
267
+ */
268
+ function buildSentinel(field) {
269
+ switch (field.type) {
270
+ case "text":
271
+ case "textarea": return "<REQUIRED: text>";
272
+ case "richText": return "<REQUIRED: rich-text>";
273
+ case "number": return "<REQUIRED: number>";
274
+ case "date": return "<REQUIRED: date (YYYY-MM-DD)>";
275
+ case "datetime": return "<REQUIRED: datetime (ISO 8601)>";
276
+ case "url": return "<REQUIRED: url>";
277
+ case "link": return "<REQUIRED: link>";
278
+ case "media": return "<REQUIRED: media-identifier>";
279
+ case "file": return "<REQUIRED: file-identifier>";
280
+ case "boolean": return "<REQUIRED: boolean>";
281
+ case "select": {
282
+ const values = selectOptionValues$1(field);
283
+ return values.length > 0 ? `<REQUIRED: select (${values.join("|")})>` : "<REQUIRED: select>";
284
+ }
285
+ case "reference": {
286
+ const kind = effectiveReferenceKind(field);
287
+ return kind ? `<REQUIRED: reference-identifier (${kind})>` : "<REQUIRED: reference-identifier>";
288
+ }
289
+ case "group": return "<REQUIRED: group>";
290
+ case "button": return "<REQUIRED: button>";
291
+ case "repeater": return "<REQUIRED: repeater>";
292
+ }
293
+ }
294
+ /**
295
+ * Deep-scan arbitrary entry data for any unfilled placeholder sentinels. Needs no
296
+ * schema, so it powers the CLI's pre-HTTP upsert pre-flight.
297
+ */
298
+ function findUnfilledSentinels(data, pathPrefix = "") {
299
+ const found = [];
300
+ const walk = (value, path) => {
301
+ if (isPlaceholderSentinel(value)) {
302
+ found.push({
303
+ path: path || "(root)",
304
+ value
305
+ });
306
+ return;
307
+ }
308
+ if (Array.isArray(value)) {
309
+ value.forEach((item, index) => walk(item, `${path}[${index}]`));
310
+ return;
311
+ }
312
+ if (value !== null && typeof value === "object") {
313
+ for (const [key, child] of Object.entries(value)) {
314
+ walk(child, path ? `${path}.${key}` : key);
315
+ }
316
+ }
317
+ };
318
+ walk(data, pathPrefix);
319
+ return found;
320
+ }
321
+
322
+ //#endregion
323
+ //#region ../content-model/src/content-types/entryValidation.ts
324
+ /**
325
+ * Entry-instance validator, compiled from the content-type field DSL.
326
+ *
327
+ * `ContentTypeSchemaZ` validates schema DOCUMENTS; this validates entry DATA
328
+ * against a schema: required-field presence, rejection of unfilled placeholder
329
+ * sentinels (shared vocabulary in `sentinels.ts`), and basic per-type shape
330
+ * checks. It is intentionally lightweight — enough to power scaffold diagnostics
331
+ * and a CLI pre-flight — not a replacement for server-side write validation.
332
+ */
333
+ function subfieldsOf$1(field) {
334
+ const direct = field.fields;
335
+ if (direct && direct.length > 0) return direct;
336
+ const nested = field.schema?.fields;
337
+ return nested ?? [];
338
+ }
339
+ function hasRepeaterVariants(field) {
340
+ return field.itemTypes !== undefined;
341
+ }
342
+ function resolveRepeaterVariantFields(field, item) {
343
+ if (!hasRepeaterVariants(field)) return subfieldsOf$1(field);
344
+ const itemTypes = field.itemTypes;
345
+ const typeId = item._type;
346
+ if (typeof typeId !== "string") return null;
347
+ return itemTypes[typeId]?.fields ?? null;
348
+ }
349
+ function selectOptionValues(field) {
350
+ const options = field.options;
351
+ return options ? options.map((o) => o.value) : [];
352
+ }
353
+ function isPlainObject(value) {
354
+ return value !== null && typeof value === "object" && !Array.isArray(value);
355
+ }
356
+ function isAllowedIdentifierBearingMedia(field, value) {
357
+ if (!isPlainObject(value) || typeof value.identifier !== "string" || value.identifier.trim().length === 0) {
358
+ return false;
359
+ }
360
+ if (value.type === undefined) return true;
361
+ const allowedKinds = field.type === "media" ? field.mediaKinds : field.fileKinds;
362
+ if (typeof value.type !== "string") return false;
363
+ const effectiveAllowedKinds = allowedKinds ?? (field.type === "media" ? ["image"] : ["document"]);
364
+ return effectiveAllowedKinds.some((kind) => kind === value.type);
365
+ }
366
+ function assertNever$2(type) {
367
+ throw new Error(`Unsupported entry field type: ${String(type)}`);
368
+ }
369
+ function scalarRule(type) {
370
+ switch (type) {
371
+ case "text":
372
+ case "textarea":
373
+ case "url":
374
+ case "reference":
375
+ case "date":
376
+ case "datetime":
377
+ case "select": return {
378
+ accepts: (value) => typeof value === "string",
379
+ expected: "a string"
380
+ };
381
+ case "link": return {
382
+ accepts: (value) => linkSchema.safeParse(normalizeLinkValue(value)).success,
383
+ expected: "a valid link"
384
+ };
385
+ case "number": return {
386
+ accepts: (value) => typeof value === "number" && !Number.isNaN(value),
387
+ expected: "a number"
388
+ };
389
+ case "boolean": return {
390
+ accepts: (value) => typeof value === "boolean",
391
+ expected: "a boolean"
392
+ };
393
+ case "richText": return {
394
+ accepts: (value) => typeof value === "string" || isPlainObject(value),
395
+ expected: "rich-text (string or document object)"
396
+ };
397
+ case "group":
398
+ case "button":
399
+ case "media":
400
+ case "file": return {
401
+ accepts: isPlainObject,
402
+ expected: "an object"
403
+ };
404
+ case "repeater": return {
405
+ accepts: Array.isArray,
406
+ expected: "an array"
407
+ };
408
+ default: return assertNever$2(type);
409
+ }
410
+ }
411
+ function leafTypeErrors(field, value, path) {
412
+ if (field.type === "media" || field.type === "file") return mediaLeafErrors(field, value, path);
413
+ if (field.type === "select") return selectLeafErrors(field, value, path);
414
+ const rule = scalarRule(field.type);
415
+ return rule.accepts(value) ? [] : [{
416
+ path,
417
+ fieldType: field.type,
418
+ code: "wrong_type",
419
+ message: `"${path}" must be ${rule.expected}.`
420
+ }];
421
+ }
422
+ function mediaLeafErrors(field, value, path) {
423
+ if (isPlainObject(value) && isPlaceholderSentinel(value.identifier)) {
424
+ return [{
425
+ path,
426
+ fieldType: field.type,
427
+ code: "unfilled_placeholder",
428
+ message: `"${path}" still contains an unfilled placeholder (${value.identifier}).`
429
+ }];
430
+ }
431
+ return isAllowedIdentifierBearingMedia(field, value) ? [] : [{
432
+ path,
433
+ fieldType: field.type,
434
+ code: "wrong_type",
435
+ message: `"${path}" must be a media object with an identifier.`
436
+ }];
437
+ }
438
+ function selectLeafErrors(field, value, path) {
439
+ if (typeof value !== "string") return [{
440
+ path,
441
+ fieldType: field.type,
442
+ code: "wrong_type",
443
+ message: `"${path}" must be a string.`
444
+ }];
445
+ const options = selectOptionValues(field);
446
+ return options.length > 0 && !options.includes(value) ? [{
447
+ path,
448
+ fieldType: field.type,
449
+ code: "invalid_option",
450
+ message: `"${path}" must be one of: ${options.join(", ")}.`
451
+ }] : [];
452
+ }
453
+ function validateField(field, value, path, allowIncomplete) {
454
+ const missing = value === undefined || value === null || value === "";
455
+ if (missing) return field.required === true && !allowIncomplete ? [{
456
+ path,
457
+ fieldType: field.type,
458
+ code: "missing_required",
459
+ message: `Required field "${path}" is missing.`
460
+ }] : [];
461
+ if (isPlaceholderSentinel(value)) return [{
462
+ path,
463
+ fieldType: field.type,
464
+ code: "unfilled_placeholder",
465
+ message: `"${path}" still contains an unfilled placeholder (${value}).`
466
+ }];
467
+ return validatePresentField(field, value, path, allowIncomplete);
468
+ }
469
+ function validatePresentField(field, value, path, allowIncomplete) {
470
+ if (field.type === "group" || field.type === "button") {
471
+ return isPlainObject(value) ? validateFields(subfieldsOf$1(field), value, path, allowIncomplete) : [{
472
+ path,
473
+ fieldType: field.type,
474
+ code: "wrong_type",
475
+ message: `"${path}" must be an object.`
476
+ }];
477
+ }
478
+ if (field.type === "repeater") {
479
+ return Array.isArray(value) ? value.flatMap((item, index) => validateRepeaterItem(field, item, `${path}[${index}]`, allowIncomplete)) : [{
480
+ path,
481
+ fieldType: field.type,
482
+ code: "wrong_type",
483
+ message: `"${path}" must be an array.`
484
+ }];
485
+ }
486
+ return leafTypeErrors(field, value, path);
487
+ }
488
+ function validateRepeaterItem(field, item, path, allowIncomplete) {
489
+ if (!isPlainObject(item)) return [{
490
+ path,
491
+ fieldType: "repeater",
492
+ code: "wrong_type",
493
+ message: `"${path}" must be an object.`
494
+ }];
495
+ const fields = resolveRepeaterVariantFields(field, item);
496
+ return fields ? validateFields(fields, item, path, allowIncomplete) : [{
497
+ path,
498
+ fieldType: "repeater",
499
+ code: "invalid_option",
500
+ message: `"${path}" has an unknown or missing repeater item type.`
501
+ }];
502
+ }
503
+ function validateFields(fields, data, pathPrefix, allowIncomplete) {
504
+ return fields.flatMap((field) => validateField(field, data[field.id], pathPrefix ? `${pathPrefix}.${field.id}` : field.id, allowIncomplete));
505
+ }
506
+ /** Validate all entry fields, returning every error so callers can fix them in one pass. */
507
+ function validateEntryInstance(input) {
508
+ const errors = validateFields(input.schema.fields, input.data, "", input.allowIncomplete ?? false);
509
+ return {
510
+ valid: errors.length === 0,
511
+ errors
512
+ };
513
+ }
514
+
452
515
  //#endregion
453
516
  //#region ../content-model/src/referenceFields.ts
454
517
  const BUILT_IN_REFERENCE_KINDS = [
@@ -549,6 +612,9 @@ const DISPLAY_MEDIA_KINDS$1 = [
549
612
  "video",
550
613
  "audio"
551
614
  ];
615
+ function mapContentFields(fields, context) {
616
+ return fields.map((field) => toFieldDefinition(field, context)).filter((field) => field !== null);
617
+ }
552
618
  function toFieldDefinition(field, context = {}) {
553
619
  const label = field.label ?? startCase(field.id);
554
620
  const base = {
@@ -672,18 +738,26 @@ function toFieldDefinition(field, context = {}) {
672
738
  };
673
739
  }
674
740
  case "repeater": {
675
- const nestedFields$1 = (field.fields ?? field.schema?.fields ?? []).map((f) => toFieldDefinition(f, context)).filter((f) => f !== null);
676
- if (!nestedFields$1.length) return null;
677
- return {
678
- id: field.id,
679
- label,
680
- description: field.description ?? undefined,
681
- required: field.required ?? false,
741
+ const repeater = {
742
+ ...base,
682
743
  type: "repeater",
683
- ui: normalizeFieldUi(field.ui),
684
- itemLabel: field.itemLabel ?? "Item",
685
- schema: { fields: nestedFields$1 }
744
+ itemLabel: field.itemLabel ?? "Item"
686
745
  };
746
+ if (hasRepeaterVariants(field)) {
747
+ return {
748
+ ...repeater,
749
+ polymorphic: true,
750
+ itemTypes: Object.fromEntries(Object.entries(field.itemTypes).map(([typeId, variant]) => [typeId, {
751
+ label: typeof variant.label === "string" ? variant.label : startCase(typeId),
752
+ fields: mapContentFields(variant.fields ?? [], context)
753
+ }]))
754
+ };
755
+ }
756
+ const fields = mapContentFields(field.fields ?? field.schema?.fields ?? [], context);
757
+ return fields.length ? {
758
+ ...repeater,
759
+ schema: { fields }
760
+ } : null;
687
761
  }
688
762
  default: return null;
689
763
  }
@@ -706,14 +780,7 @@ function startCase(value) {
706
780
  * @returns Array of field definitions for form rendering
707
781
  */
708
782
  function mapContentSchemaToFormFields(schema, context = {}) {
709
- const fields = [];
710
- for (const field of schema.fields ?? []) {
711
- const definition = toFieldDefinition(field, context);
712
- if (definition) {
713
- fields.push(definition);
714
- }
715
- }
716
- return fields;
783
+ return mapContentFields(schema.fields ?? [], context);
717
784
  }
718
785
  function expandButtonFieldGroup(fieldId, fieldLabel, context = {}) {
719
786
  const variants = context.variants ?? [...DEFAULT_BUTTON_VARIANTS];
@@ -1233,26 +1300,7 @@ function getPublishedMetadataFromSnapshot(snapshot, routing) {
1233
1300
  };
1234
1301
  }
1235
1302
  function buildToolbarState(snapshot, context, options) {
1236
- const formatPublishMessage = ({ status, publishedAt, scheduledAt }) => {
1237
- if (status === "published") {
1238
- if (!publishedAt) return "Published";
1239
- const date = new Date(publishedAt);
1240
- if (Number.isNaN(date.getTime())) return "Published";
1241
- return `Published ${date.toLocaleString()}`;
1242
- }
1243
- if (status === "scheduled" && scheduledAt) {
1244
- const date = new Date(scheduledAt);
1245
- if (!Number.isNaN(date.getTime())) {
1246
- return `Scheduled for ${date.toLocaleString()}`;
1247
- }
1248
- }
1249
- return "Not yet published";
1250
- };
1251
- const publishedMessage = formatPublishMessage({
1252
- status: snapshot.status,
1253
- publishedAt: snapshot.published.publishAt,
1254
- scheduledAt: snapshot.draft.publishAt
1255
- });
1303
+ const publishedMessage = formatPublishMessage(snapshot, options.formatPublicationDate);
1256
1304
  const publishLabel = routeLifecycleContext(snapshot)?.draftStatus === "scheduled" ? "Publish now" : `Publish ${options.entityLabel.toLowerCase()}`;
1257
1305
  return {
1258
1306
  currentStatus: snapshot.status,
@@ -1263,6 +1311,15 @@ function buildToolbarState(snapshot, context, options) {
1263
1311
  values: context.values
1264
1312
  };
1265
1313
  }
1314
+ /** Sentence adapter only; date admission and typography belong to the shared formatter. */
1315
+ function formatPublishMessage(snapshot, formatDate) {
1316
+ if (snapshot.status === "published") {
1317
+ const date = snapshot.published.publishAt ? formatDate(snapshot.published.publishAt) : null;
1318
+ return date ? `Published ${date}` : "Published";
1319
+ }
1320
+ const scheduled = snapshot.status === "scheduled" && snapshot.draft.publishAt ? formatDate(snapshot.draft.publishAt) : null;
1321
+ return scheduled ? `Scheduled for ${scheduled}` : "Not yet published";
1322
+ }
1266
1323
 
1267
1324
  //#endregion
1268
1325
  //#region ../content-model/src/content-types/schema.ts
@@ -1403,6 +1460,10 @@ const BaseContentFieldSchema = z.object({
1403
1460
  }
1404
1461
  }).passthrough();
1405
1462
  const ContentFieldSchema = BaseContentFieldSchema.extend({
1463
+ itemTypes: z.lazy(() => z.record(z.string(), z.object({
1464
+ label: z.string().optional(),
1465
+ fields: z.array(ContentFieldSchema).optional()
1466
+ }).passthrough())).optional(),
1406
1467
  fields: z.lazy(() => z.array(ContentFieldSchema)).optional(),
1407
1468
  schema: z.lazy(() => z.object({ fields: z.array(ContentFieldSchema).optional() }).passthrough()).optional()
1408
1469
  });
@@ -1516,284 +1577,6 @@ function isFileFieldKind(value) {
1516
1577
  return FILE_FIELD_KINDS.includes(value);
1517
1578
  }
1518
1579
 
1519
- //#endregion
1520
- //#region ../content-model/src/content-types/sentinels.ts
1521
- /** Leaf field types that receive a placeholder sentinel (containers recurse). */
1522
- const SENTINEL_LEAF_TYPES = [
1523
- "text",
1524
- "textarea",
1525
- "richText",
1526
- "number",
1527
- "date",
1528
- "datetime",
1529
- "url",
1530
- "link",
1531
- "media",
1532
- "file",
1533
- "boolean",
1534
- "select",
1535
- "reference"
1536
- ];
1537
- /** Matches the closed sentinel shape `"<REQUIRED: …>"`. */
1538
- const SENTINEL_PATTERN = /^<REQUIRED:.*>$/;
1539
- /**
1540
- * True when a value is an unfilled placeholder sentinel. Shared by the emitter
1541
- * (self-check) and the validator (rejection) so recognition can't drift.
1542
- */
1543
- function isPlaceholderSentinel(value) {
1544
- return typeof value === "string" && SENTINEL_PATTERN.test(value);
1545
- }
1546
- function effectiveReferenceKind(field) {
1547
- const kind = field.referenceKind;
1548
- const legacy = field.reference?.type;
1549
- return kind ?? legacy;
1550
- }
1551
- function selectOptionValues$1(field) {
1552
- const options = field.options;
1553
- return options ? options.map((o) => o.value) : [];
1554
- }
1555
- /**
1556
- * The placeholder sentinel for a leaf field. Container types (group/button/
1557
- * repeater) do not have a leaf sentinel — they expand structurally — and calling
1558
- * this for them returns a generic marker only for completeness.
1559
- */
1560
- function buildSentinel(field) {
1561
- switch (field.type) {
1562
- case "text":
1563
- case "textarea": return "<REQUIRED: text>";
1564
- case "richText": return "<REQUIRED: rich-text>";
1565
- case "number": return "<REQUIRED: number>";
1566
- case "date": return "<REQUIRED: date (YYYY-MM-DD)>";
1567
- case "datetime": return "<REQUIRED: datetime (ISO 8601)>";
1568
- case "url": return "<REQUIRED: url>";
1569
- case "link": return "<REQUIRED: link>";
1570
- case "media": return "<REQUIRED: media-identifier>";
1571
- case "file": return "<REQUIRED: file-identifier>";
1572
- case "boolean": return "<REQUIRED: boolean>";
1573
- case "select": {
1574
- const values = selectOptionValues$1(field);
1575
- return values.length > 0 ? `<REQUIRED: select (${values.join("|")})>` : "<REQUIRED: select>";
1576
- }
1577
- case "reference": {
1578
- const kind = effectiveReferenceKind(field);
1579
- return kind ? `<REQUIRED: reference-identifier (${kind})>` : "<REQUIRED: reference-identifier>";
1580
- }
1581
- case "group": return "<REQUIRED: group>";
1582
- case "button": return "<REQUIRED: button>";
1583
- case "repeater": return "<REQUIRED: repeater>";
1584
- }
1585
- }
1586
- /**
1587
- * Deep-scan arbitrary entry data for any unfilled placeholder sentinels. Needs no
1588
- * schema, so it powers the CLI's pre-HTTP upsert pre-flight.
1589
- */
1590
- function findUnfilledSentinels(data, pathPrefix = "") {
1591
- const found = [];
1592
- const walk = (value, path) => {
1593
- if (isPlaceholderSentinel(value)) {
1594
- found.push({
1595
- path: path || "(root)",
1596
- value
1597
- });
1598
- return;
1599
- }
1600
- if (Array.isArray(value)) {
1601
- value.forEach((item, index) => walk(item, `${path}[${index}]`));
1602
- return;
1603
- }
1604
- if (value !== null && typeof value === "object") {
1605
- for (const [key, child] of Object.entries(value)) {
1606
- walk(child, path ? `${path}.${key}` : key);
1607
- }
1608
- }
1609
- };
1610
- walk(data, pathPrefix);
1611
- return found;
1612
- }
1613
-
1614
- //#endregion
1615
- //#region ../content-model/src/content-types/entryValidation.ts
1616
- function subfieldsOf$1(field) {
1617
- const direct = field.fields;
1618
- if (direct && direct.length > 0) return direct;
1619
- const nested = field.schema?.fields;
1620
- return nested ?? [];
1621
- }
1622
- function repeaterVariantFields(field, item) {
1623
- const itemTypes = field.itemTypes;
1624
- if (!itemTypes) return subfieldsOf$1(field);
1625
- const typeId = item._type;
1626
- if (typeof typeId !== "string") return null;
1627
- return itemTypes[typeId]?.fields ?? null;
1628
- }
1629
- function selectOptionValues(field) {
1630
- const options = field.options;
1631
- return options ? options.map((o) => o.value) : [];
1632
- }
1633
- function isPlainObject(value) {
1634
- return value !== null && typeof value === "object" && !Array.isArray(value);
1635
- }
1636
- const STRING_TYPES = new Set([
1637
- "text",
1638
- "textarea",
1639
- "url",
1640
- "link",
1641
- "reference",
1642
- "date",
1643
- "datetime",
1644
- "select"
1645
- ]);
1646
- function isAllowedIdentifierBearingMedia(field, value) {
1647
- if (!isPlainObject(value) || typeof value.identifier !== "string" || value.identifier.trim().length === 0) {
1648
- return false;
1649
- }
1650
- if (value.type === undefined) return true;
1651
- const allowedKinds = field.type === "media" ? field.mediaKinds : field.fileKinds;
1652
- if (typeof value.type !== "string") return false;
1653
- const effectiveAllowedKinds = allowedKinds ?? (field.type === "media" ? ["image"] : ["document"]);
1654
- return effectiveAllowedKinds.some((kind) => kind === value.type);
1655
- }
1656
- function checkLeafType(field, value, path, errors) {
1657
- const push = (code, message) => {
1658
- errors.push({
1659
- path,
1660
- fieldType: field.type,
1661
- code,
1662
- message
1663
- });
1664
- };
1665
- if (field.type === "number") {
1666
- if (typeof value !== "number" || Number.isNaN(value)) {
1667
- push("wrong_type", `"${path}" must be a number.`);
1668
- }
1669
- return;
1670
- }
1671
- if (field.type === "boolean") {
1672
- if (typeof value !== "boolean") push("wrong_type", `"${path}" must be a boolean.`);
1673
- return;
1674
- }
1675
- if (field.type === "richText") {
1676
- if (typeof value !== "string" && !isPlainObject(value)) {
1677
- push("wrong_type", `"${path}" must be rich-text (string or document object).`);
1678
- }
1679
- return;
1680
- }
1681
- if (field.type === "media" || field.type === "file") {
1682
- if (isPlainObject(value) && isPlaceholderSentinel(value.identifier)) {
1683
- push("unfilled_placeholder", `"${path}" still contains an unfilled placeholder (${value.identifier}).`);
1684
- return;
1685
- }
1686
- if (!isAllowedIdentifierBearingMedia(field, value)) {
1687
- push("wrong_type", `"${path}" must be a media object with an identifier.`);
1688
- }
1689
- return;
1690
- }
1691
- if (field.type === "select") {
1692
- if (typeof value !== "string") {
1693
- push("wrong_type", `"${path}" must be a string.`);
1694
- return;
1695
- }
1696
- const options = selectOptionValues(field);
1697
- if (options.length > 0 && !options.includes(value)) {
1698
- push("invalid_option", `"${path}" must be one of: ${options.join(", ")}.`);
1699
- }
1700
- return;
1701
- }
1702
- if (STRING_TYPES.has(field.type)) {
1703
- if (typeof value !== "string") push("wrong_type", `"${path}" must be a string.`);
1704
- }
1705
- }
1706
- function validateField(field, value, path, errors, allowIncomplete) {
1707
- const missing = value === undefined || value === null || value === "";
1708
- if (missing) {
1709
- if (field.required === true && !allowIncomplete) {
1710
- errors.push({
1711
- path,
1712
- fieldType: field.type,
1713
- code: "missing_required",
1714
- message: `Required field "${path}" is missing.`
1715
- });
1716
- }
1717
- return;
1718
- }
1719
- if (isPlaceholderSentinel(value)) {
1720
- errors.push({
1721
- path,
1722
- fieldType: field.type,
1723
- code: "unfilled_placeholder",
1724
- message: `"${path}" still contains an unfilled placeholder (${value}).`
1725
- });
1726
- return;
1727
- }
1728
- if (field.type === "group" || field.type === "button") {
1729
- if (!isPlainObject(value)) {
1730
- errors.push({
1731
- path,
1732
- fieldType: field.type,
1733
- code: "wrong_type",
1734
- message: `"${path}" must be an object.`
1735
- });
1736
- return;
1737
- }
1738
- validateFields(subfieldsOf$1(field), value, path, errors, allowIncomplete);
1739
- return;
1740
- }
1741
- if (field.type === "repeater") {
1742
- if (!Array.isArray(value)) {
1743
- errors.push({
1744
- path,
1745
- fieldType: field.type,
1746
- code: "wrong_type",
1747
- message: `"${path}" must be an array.`
1748
- });
1749
- return;
1750
- }
1751
- value.forEach((item, index) => {
1752
- const itemPath = `${path}[${index}]`;
1753
- if (!isPlainObject(item)) {
1754
- errors.push({
1755
- path: itemPath,
1756
- fieldType: "repeater",
1757
- code: "wrong_type",
1758
- message: `"${itemPath}" must be an object.`
1759
- });
1760
- return;
1761
- }
1762
- const itemFields = repeaterVariantFields(field, item);
1763
- if (!itemFields) {
1764
- errors.push({
1765
- path: itemPath,
1766
- fieldType: "repeater",
1767
- code: "invalid_option",
1768
- message: `"${itemPath}" has an unknown or missing repeater item type.`
1769
- });
1770
- return;
1771
- }
1772
- validateFields(itemFields, item, itemPath, errors, allowIncomplete);
1773
- });
1774
- return;
1775
- }
1776
- checkLeafType(field, value, path, errors);
1777
- }
1778
- function validateFields(fields, data, pathPrefix, errors, allowIncomplete) {
1779
- for (const field of fields) {
1780
- const path = pathPrefix ? `${pathPrefix}.${field.id}` : field.id;
1781
- validateField(field, data[field.id], path, errors, allowIncomplete);
1782
- }
1783
- }
1784
- /**
1785
- * Validate entry data against a content-type schema. Returns all errors so
1786
- * callers can list everything an agent must fix in one pass.
1787
- */
1788
- function validateEntryInstance(input) {
1789
- const errors = [];
1790
- validateFields(input.schema.fields, input.data, "", errors, input.allowIncomplete ?? false);
1791
- return {
1792
- valid: errors.length === 0,
1793
- errors
1794
- };
1795
- }
1796
-
1797
1580
  //#endregion
1798
1581
  //#region ../content-model/src/content-types/scaffold.ts
1799
1582
  /** Nested subfields for group/repeater/button-style fields (both DSL shapes). */
@@ -4754,15 +4537,14 @@ const eventBookingOfferInputSchema = classifiedSchema((input) => {
4754
4537
  }]
4755
4538
  } : result;
4756
4539
  });
4757
- const eventRunInputSchema = z.object({
4540
+ const eventRunCommonInputSchema = z.object({
4758
4541
  identifier: z.string().min(1),
4759
4542
  eventRef: z.string().min(1),
4760
- name: z.string().nullable().optional(),
4761
4543
  status: sdkEventUpsertOptionalFieldSchemas.status,
4762
4544
  visibility: sdkEventUpsertOptionalFieldSchemas.visibility,
4763
- bookingScope: z.enum(["single_occurrence", "whole_series"]),
4764
4545
  occurrences: sdkEventUpsertOptionalFieldSchemas.occurrences.unwrap(),
4765
4546
  schedule: eventScheduleInputSchema.optional(),
4547
+ attendeeMigration: sdkEventUpsertOptionalFieldSchemas.attendeeMigration,
4766
4548
  bookingOffer: eventBookingOfferInputSchema.optional(),
4767
4549
  capacity: sdkEventUpsertOptionalFieldSchemas.capacity,
4768
4550
  venueRef: z.string().nullable().optional(),
@@ -4770,6 +4552,10 @@ const eventRunInputSchema = z.object({
4770
4552
  settings: z.record(z.string(), z.unknown()).optional(),
4771
4553
  tags: z.array(z.string()).optional()
4772
4554
  }).strict();
4555
+ const eventRunInputSchema = z.discriminatedUnion("bookingScope", [eventRunCommonInputSchema.extend({ bookingScope: z.literal("single_occurrence") }).strict(), eventRunCommonInputSchema.extend({
4556
+ bookingScope: z.literal("whole_series"),
4557
+ name: z.string().nullable().optional()
4558
+ }).strict()]);
4773
4559
  const eventUpsertInputSchema = z.object({
4774
4560
  identifier: z.string().min(1).optional(),
4775
4561
  courseBooking: sdkCourseBookingSettingsSchema.optional(),
@@ -4795,4 +4581,4 @@ const eventUpsertInputSchema = z.object({
4795
4581
  }));
4796
4582
 
4797
4583
  //#endregion
4798
- export { RichTextDocSchema as a, isRecord$3 as c, findUnfilledSentinels as i, buildEntryScaffold as n, isRichTextDoc as o, validateEntryInstance as r, isObjectRecord as s, eventUpsertInputSchema as t };
4584
+ export { findUnfilledSentinels as i, buildEntryScaffold as n, validateEntryInstance as r, eventUpsertInputSchema as t };