riverbankcms 0.19.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.
- package/README.md +23 -0
- package/dist/acceptance/matrix.mjs +3 -3
- package/dist/cli/index.mjs +2291 -105791
- package/dist/cli/init-docs/content/workflow-content-types.md +25 -0
- package/dist/entryCommands-8kDjOex0.mjs +111695 -0
- package/dist/{eventUpsertInputSchema-Bkafrrjw.mjs → eventUpsertInputSchema-VVBVGA6x.mjs} +333 -547
- package/dist/load-config-Cut7_6t_.mjs +4 -0
- package/dist/{load-config-CErro6mj.mjs → load-config-gqUEjIyB.mjs} +2 -2
- package/dist/{plan-DW5O751d.mjs → plan-HAEMHc5T.mjs} +1 -1
- package/dist/{sdk-event-upsert-BdvfnCyV.mjs → richTextSchema-DTvrDDF5.mjs} +1300 -165
- package/dist/scaffold/index.mjs +2 -2
- package/dist/{src-yL0DBH7S.mjs → src-Dixcv4PB.mjs} +109 -15
- package/dist/testing.mjs +5 -4
- package/package.json +1 -1
- package/dist/load-config-rPaisrdG.mjs +0 -4
- package/dist/local-sync-CWLEhMZ-.mjs +0 -4635
|
@@ -1,233 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { u as EVENT_PRESET_VALUES } from "./src-
|
|
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
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
4584
|
+
export { findUnfilledSentinels as i, buildEntryScaffold as n, validateEntryInstance as r, eventUpsertInputSchema as t };
|