riverbankcms 0.19.0 → 0.21.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 +4 -4
- package/dist/cli/index.mjs +62492 -104568
- package/dist/cli/init-docs/content/getting-started.md +27 -0
- package/dist/cli/init-docs/content/workflow-content-types.md +25 -0
- package/dist/entryCommands-DSQZqznu.mjs +23110 -0
- package/dist/{eventUpsertInputSchema-Bkafrrjw.mjs → eventUpsertInputSchema-zCTx0fdF.mjs} +337 -325
- package/dist/{load-config-CErro6mj.mjs → load-config-B68gtN45.mjs} +3 -3
- package/dist/load-config-ghkXBKim.mjs +4 -0
- package/dist/{plan-DW5O751d.mjs → plan-CkMANumv.mjs} +36 -3
- package/dist/scaffold/index.mjs +2 -2
- package/dist/{sdk-event-upsert-BdvfnCyV.mjs → sdk-event-upsert-CrOuJrxo.mjs} +1033 -123
- package/dist/{src-yL0DBH7S.mjs → src-BENi4Q5k.mjs} +134 -29
- package/dist/testing.mjs +5 -4
- package/package.json +2 -2
- package/dist/load-config-rPaisrdG.mjs +0 -4
- package/dist/local-sync-CWLEhMZ-.mjs +0 -4635
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as classifySdkEventSchedule, i as sdkEventUpsertOptionalFieldSchemas, n as sdkCourseBookingSettingsSchema, o as classifySdkEventBookingOffer } from "./sdk-event-upsert-
|
|
3
|
-
import { u as EVENT_PRESET_VALUES } from "./src-
|
|
2
|
+
import { _ as normalizeLinkValue, a as classifySdkEventSchedule, g as linkSchema, i as sdkEventUpsertOptionalFieldSchemas, n as sdkCourseBookingSettingsSchema, o as classifySdkEventBookingOffer } from "./sdk-event-upsert-CrOuJrxo.mjs";
|
|
3
|
+
import { u as EVENT_PRESET_VALUES } from "./src-BENi4Q5k.mjs";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
//#region ../blocks/src/lib/typeGuards.ts
|
|
@@ -449,6 +449,294 @@ function normalizeSlugSegment(segment) {
|
|
|
449
449
|
return normalized;
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region ../content-model/src/content-types/sentinels.ts
|
|
454
|
+
/** Leaf field types that receive a placeholder sentinel (containers recurse). */
|
|
455
|
+
const SENTINEL_LEAF_TYPES = [
|
|
456
|
+
"text",
|
|
457
|
+
"textarea",
|
|
458
|
+
"richText",
|
|
459
|
+
"number",
|
|
460
|
+
"date",
|
|
461
|
+
"datetime",
|
|
462
|
+
"url",
|
|
463
|
+
"link",
|
|
464
|
+
"media",
|
|
465
|
+
"file",
|
|
466
|
+
"boolean",
|
|
467
|
+
"select",
|
|
468
|
+
"reference"
|
|
469
|
+
];
|
|
470
|
+
/** Matches the closed sentinel shape `"<REQUIRED: …>"`. */
|
|
471
|
+
const SENTINEL_PATTERN = /^<REQUIRED:.*>$/;
|
|
472
|
+
/**
|
|
473
|
+
* True when a value is an unfilled placeholder sentinel. Shared by the emitter
|
|
474
|
+
* (self-check) and the validator (rejection) so recognition can't drift.
|
|
475
|
+
*/
|
|
476
|
+
function isPlaceholderSentinel(value) {
|
|
477
|
+
return typeof value === "string" && SENTINEL_PATTERN.test(value);
|
|
478
|
+
}
|
|
479
|
+
function effectiveReferenceKind(field) {
|
|
480
|
+
const kind = field.referenceKind;
|
|
481
|
+
const legacy = field.reference?.type;
|
|
482
|
+
return kind ?? legacy;
|
|
483
|
+
}
|
|
484
|
+
function selectOptionValues$1(field) {
|
|
485
|
+
const options = field.options;
|
|
486
|
+
return options ? options.map((o) => o.value) : [];
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The placeholder sentinel for a leaf field. Container types (group/button/
|
|
490
|
+
* repeater) do not have a leaf sentinel — they expand structurally — and calling
|
|
491
|
+
* this for them returns a generic marker only for completeness.
|
|
492
|
+
*/
|
|
493
|
+
function buildSentinel(field) {
|
|
494
|
+
switch (field.type) {
|
|
495
|
+
case "text":
|
|
496
|
+
case "textarea": return "<REQUIRED: text>";
|
|
497
|
+
case "richText": return "<REQUIRED: rich-text>";
|
|
498
|
+
case "number": return "<REQUIRED: number>";
|
|
499
|
+
case "date": return "<REQUIRED: date (YYYY-MM-DD)>";
|
|
500
|
+
case "datetime": return "<REQUIRED: datetime (ISO 8601)>";
|
|
501
|
+
case "url": return "<REQUIRED: url>";
|
|
502
|
+
case "link": return "<REQUIRED: link>";
|
|
503
|
+
case "media": return "<REQUIRED: media-identifier>";
|
|
504
|
+
case "file": return "<REQUIRED: file-identifier>";
|
|
505
|
+
case "boolean": return "<REQUIRED: boolean>";
|
|
506
|
+
case "select": {
|
|
507
|
+
const values = selectOptionValues$1(field);
|
|
508
|
+
return values.length > 0 ? `<REQUIRED: select (${values.join("|")})>` : "<REQUIRED: select>";
|
|
509
|
+
}
|
|
510
|
+
case "reference": {
|
|
511
|
+
const kind = effectiveReferenceKind(field);
|
|
512
|
+
return kind ? `<REQUIRED: reference-identifier (${kind})>` : "<REQUIRED: reference-identifier>";
|
|
513
|
+
}
|
|
514
|
+
case "group": return "<REQUIRED: group>";
|
|
515
|
+
case "button": return "<REQUIRED: button>";
|
|
516
|
+
case "repeater": return "<REQUIRED: repeater>";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Deep-scan arbitrary entry data for any unfilled placeholder sentinels. Needs no
|
|
521
|
+
* schema, so it powers the CLI's pre-HTTP upsert pre-flight.
|
|
522
|
+
*/
|
|
523
|
+
function findUnfilledSentinels(data, pathPrefix = "") {
|
|
524
|
+
const found = [];
|
|
525
|
+
const walk = (value, path) => {
|
|
526
|
+
if (isPlaceholderSentinel(value)) {
|
|
527
|
+
found.push({
|
|
528
|
+
path: path || "(root)",
|
|
529
|
+
value
|
|
530
|
+
});
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (Array.isArray(value)) {
|
|
534
|
+
value.forEach((item, index) => walk(item, `${path}[${index}]`));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (value !== null && typeof value === "object") {
|
|
538
|
+
for (const [key, child] of Object.entries(value)) {
|
|
539
|
+
walk(child, path ? `${path}.${key}` : key);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
walk(data, pathPrefix);
|
|
544
|
+
return found;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region ../content-model/src/content-types/entryValidation.ts
|
|
549
|
+
/**
|
|
550
|
+
* Entry-instance validator, compiled from the content-type field DSL.
|
|
551
|
+
*
|
|
552
|
+
* `ContentTypeSchemaZ` validates schema DOCUMENTS; this validates entry DATA
|
|
553
|
+
* against a schema: required-field presence, rejection of unfilled placeholder
|
|
554
|
+
* sentinels (shared vocabulary in `sentinels.ts`), and basic per-type shape
|
|
555
|
+
* checks. It is intentionally lightweight — enough to power scaffold diagnostics
|
|
556
|
+
* and a CLI pre-flight — not a replacement for server-side write validation.
|
|
557
|
+
*/
|
|
558
|
+
function subfieldsOf$1(field) {
|
|
559
|
+
const direct = field.fields;
|
|
560
|
+
if (direct && direct.length > 0) return direct;
|
|
561
|
+
const nested = field.schema?.fields;
|
|
562
|
+
return nested ?? [];
|
|
563
|
+
}
|
|
564
|
+
function hasRepeaterVariants(field) {
|
|
565
|
+
return field.itemTypes !== undefined;
|
|
566
|
+
}
|
|
567
|
+
function resolveRepeaterVariantFields(field, item) {
|
|
568
|
+
if (!hasRepeaterVariants(field)) return subfieldsOf$1(field);
|
|
569
|
+
const itemTypes = field.itemTypes;
|
|
570
|
+
const typeId = item._type;
|
|
571
|
+
if (typeof typeId !== "string") return null;
|
|
572
|
+
return itemTypes[typeId]?.fields ?? null;
|
|
573
|
+
}
|
|
574
|
+
function selectOptionValues(field) {
|
|
575
|
+
const options = field.options;
|
|
576
|
+
return options ? options.map((o) => o.value) : [];
|
|
577
|
+
}
|
|
578
|
+
function isPlainObject(value) {
|
|
579
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
580
|
+
}
|
|
581
|
+
function isAllowedIdentifierBearingMedia(field, value) {
|
|
582
|
+
if (!isPlainObject(value) || typeof value.identifier !== "string" || value.identifier.trim().length === 0) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
if (value.type === undefined) return true;
|
|
586
|
+
const allowedKinds = field.type === "media" ? field.mediaKinds : field.fileKinds;
|
|
587
|
+
if (typeof value.type !== "string") return false;
|
|
588
|
+
const effectiveAllowedKinds = allowedKinds ?? (field.type === "media" ? ["image"] : ["document"]);
|
|
589
|
+
return effectiveAllowedKinds.some((kind) => kind === value.type);
|
|
590
|
+
}
|
|
591
|
+
function assertNever$2(type) {
|
|
592
|
+
throw new Error(`Unsupported entry field type: ${String(type)}`);
|
|
593
|
+
}
|
|
594
|
+
function scalarRule(type) {
|
|
595
|
+
switch (type) {
|
|
596
|
+
case "text":
|
|
597
|
+
case "textarea":
|
|
598
|
+
case "url":
|
|
599
|
+
case "reference":
|
|
600
|
+
case "date":
|
|
601
|
+
case "datetime":
|
|
602
|
+
case "select": return {
|
|
603
|
+
accepts: (value) => typeof value === "string",
|
|
604
|
+
expected: "a string"
|
|
605
|
+
};
|
|
606
|
+
case "link": return {
|
|
607
|
+
accepts: (value) => linkSchema.safeParse(normalizeLinkValue(value)).success,
|
|
608
|
+
expected: "a valid link"
|
|
609
|
+
};
|
|
610
|
+
case "number": return {
|
|
611
|
+
accepts: (value) => typeof value === "number" && !Number.isNaN(value),
|
|
612
|
+
expected: "a number"
|
|
613
|
+
};
|
|
614
|
+
case "boolean": return {
|
|
615
|
+
accepts: (value) => typeof value === "boolean",
|
|
616
|
+
expected: "a boolean"
|
|
617
|
+
};
|
|
618
|
+
case "richText": return {
|
|
619
|
+
accepts: (value) => typeof value === "string" || isPlainObject(value),
|
|
620
|
+
expected: "rich-text (string or document object)"
|
|
621
|
+
};
|
|
622
|
+
case "group":
|
|
623
|
+
case "button":
|
|
624
|
+
case "media":
|
|
625
|
+
case "file": return {
|
|
626
|
+
accepts: isPlainObject,
|
|
627
|
+
expected: "an object"
|
|
628
|
+
};
|
|
629
|
+
case "repeater": return {
|
|
630
|
+
accepts: Array.isArray,
|
|
631
|
+
expected: "an array"
|
|
632
|
+
};
|
|
633
|
+
default: return assertNever$2(type);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function leafTypeErrors(field, value, path) {
|
|
637
|
+
if (field.type === "media" || field.type === "file") return mediaLeafErrors(field, value, path);
|
|
638
|
+
if (field.type === "select") return selectLeafErrors(field, value, path);
|
|
639
|
+
const rule = scalarRule(field.type);
|
|
640
|
+
return rule.accepts(value) ? [] : [{
|
|
641
|
+
path,
|
|
642
|
+
fieldType: field.type,
|
|
643
|
+
code: "wrong_type",
|
|
644
|
+
message: `"${path}" must be ${rule.expected}.`
|
|
645
|
+
}];
|
|
646
|
+
}
|
|
647
|
+
function mediaLeafErrors(field, value, path) {
|
|
648
|
+
if (isPlainObject(value) && isPlaceholderSentinel(value.identifier)) {
|
|
649
|
+
return [{
|
|
650
|
+
path,
|
|
651
|
+
fieldType: field.type,
|
|
652
|
+
code: "unfilled_placeholder",
|
|
653
|
+
message: `"${path}" still contains an unfilled placeholder (${value.identifier}).`
|
|
654
|
+
}];
|
|
655
|
+
}
|
|
656
|
+
return isAllowedIdentifierBearingMedia(field, value) ? [] : [{
|
|
657
|
+
path,
|
|
658
|
+
fieldType: field.type,
|
|
659
|
+
code: "wrong_type",
|
|
660
|
+
message: `"${path}" must be a media object with an identifier.`
|
|
661
|
+
}];
|
|
662
|
+
}
|
|
663
|
+
function selectLeafErrors(field, value, path) {
|
|
664
|
+
if (typeof value !== "string") return [{
|
|
665
|
+
path,
|
|
666
|
+
fieldType: field.type,
|
|
667
|
+
code: "wrong_type",
|
|
668
|
+
message: `"${path}" must be a string.`
|
|
669
|
+
}];
|
|
670
|
+
const options = selectOptionValues(field);
|
|
671
|
+
return options.length > 0 && !options.includes(value) ? [{
|
|
672
|
+
path,
|
|
673
|
+
fieldType: field.type,
|
|
674
|
+
code: "invalid_option",
|
|
675
|
+
message: `"${path}" must be one of: ${options.join(", ")}.`
|
|
676
|
+
}] : [];
|
|
677
|
+
}
|
|
678
|
+
function validateField(field, value, path, allowIncomplete) {
|
|
679
|
+
const missing = value === undefined || value === null || value === "";
|
|
680
|
+
if (missing) return field.required === true && !allowIncomplete ? [{
|
|
681
|
+
path,
|
|
682
|
+
fieldType: field.type,
|
|
683
|
+
code: "missing_required",
|
|
684
|
+
message: `Required field "${path}" is missing.`
|
|
685
|
+
}] : [];
|
|
686
|
+
if (isPlaceholderSentinel(value)) return [{
|
|
687
|
+
path,
|
|
688
|
+
fieldType: field.type,
|
|
689
|
+
code: "unfilled_placeholder",
|
|
690
|
+
message: `"${path}" still contains an unfilled placeholder (${value}).`
|
|
691
|
+
}];
|
|
692
|
+
return validatePresentField(field, value, path, allowIncomplete);
|
|
693
|
+
}
|
|
694
|
+
function validatePresentField(field, value, path, allowIncomplete) {
|
|
695
|
+
if (field.type === "group" || field.type === "button") {
|
|
696
|
+
return isPlainObject(value) ? validateFields(subfieldsOf$1(field), value, path, allowIncomplete) : [{
|
|
697
|
+
path,
|
|
698
|
+
fieldType: field.type,
|
|
699
|
+
code: "wrong_type",
|
|
700
|
+
message: `"${path}" must be an object.`
|
|
701
|
+
}];
|
|
702
|
+
}
|
|
703
|
+
if (field.type === "repeater") {
|
|
704
|
+
return Array.isArray(value) ? value.flatMap((item, index) => validateRepeaterItem(field, item, `${path}[${index}]`, allowIncomplete)) : [{
|
|
705
|
+
path,
|
|
706
|
+
fieldType: field.type,
|
|
707
|
+
code: "wrong_type",
|
|
708
|
+
message: `"${path}" must be an array.`
|
|
709
|
+
}];
|
|
710
|
+
}
|
|
711
|
+
return leafTypeErrors(field, value, path);
|
|
712
|
+
}
|
|
713
|
+
function validateRepeaterItem(field, item, path, allowIncomplete) {
|
|
714
|
+
if (!isPlainObject(item)) return [{
|
|
715
|
+
path,
|
|
716
|
+
fieldType: "repeater",
|
|
717
|
+
code: "wrong_type",
|
|
718
|
+
message: `"${path}" must be an object.`
|
|
719
|
+
}];
|
|
720
|
+
const fields = resolveRepeaterVariantFields(field, item);
|
|
721
|
+
return fields ? validateFields(fields, item, path, allowIncomplete) : [{
|
|
722
|
+
path,
|
|
723
|
+
fieldType: "repeater",
|
|
724
|
+
code: "invalid_option",
|
|
725
|
+
message: `"${path}" has an unknown or missing repeater item type.`
|
|
726
|
+
}];
|
|
727
|
+
}
|
|
728
|
+
function validateFields(fields, data, pathPrefix, allowIncomplete) {
|
|
729
|
+
return fields.flatMap((field) => validateField(field, data[field.id], pathPrefix ? `${pathPrefix}.${field.id}` : field.id, allowIncomplete));
|
|
730
|
+
}
|
|
731
|
+
/** Validate all entry fields, returning every error so callers can fix them in one pass. */
|
|
732
|
+
function validateEntryInstance(input) {
|
|
733
|
+
const errors = validateFields(input.schema.fields, input.data, "", input.allowIncomplete ?? false);
|
|
734
|
+
return {
|
|
735
|
+
valid: errors.length === 0,
|
|
736
|
+
errors
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
452
740
|
//#endregion
|
|
453
741
|
//#region ../content-model/src/referenceFields.ts
|
|
454
742
|
const BUILT_IN_REFERENCE_KINDS = [
|
|
@@ -549,6 +837,9 @@ const DISPLAY_MEDIA_KINDS$1 = [
|
|
|
549
837
|
"video",
|
|
550
838
|
"audio"
|
|
551
839
|
];
|
|
840
|
+
function mapContentFields(fields, context) {
|
|
841
|
+
return fields.map((field) => toFieldDefinition(field, context)).filter((field) => field !== null);
|
|
842
|
+
}
|
|
552
843
|
function toFieldDefinition(field, context = {}) {
|
|
553
844
|
const label = field.label ?? startCase(field.id);
|
|
554
845
|
const base = {
|
|
@@ -672,18 +963,26 @@ function toFieldDefinition(field, context = {}) {
|
|
|
672
963
|
};
|
|
673
964
|
}
|
|
674
965
|
case "repeater": {
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
return {
|
|
678
|
-
id: field.id,
|
|
679
|
-
label,
|
|
680
|
-
description: field.description ?? undefined,
|
|
681
|
-
required: field.required ?? false,
|
|
966
|
+
const repeater = {
|
|
967
|
+
...base,
|
|
682
968
|
type: "repeater",
|
|
683
|
-
|
|
684
|
-
itemLabel: field.itemLabel ?? "Item",
|
|
685
|
-
schema: { fields: nestedFields$1 }
|
|
969
|
+
itemLabel: field.itemLabel ?? "Item"
|
|
686
970
|
};
|
|
971
|
+
if (hasRepeaterVariants(field)) {
|
|
972
|
+
return {
|
|
973
|
+
...repeater,
|
|
974
|
+
polymorphic: true,
|
|
975
|
+
itemTypes: Object.fromEntries(Object.entries(field.itemTypes).map(([typeId, variant]) => [typeId, {
|
|
976
|
+
label: typeof variant.label === "string" ? variant.label : startCase(typeId),
|
|
977
|
+
fields: mapContentFields(variant.fields ?? [], context)
|
|
978
|
+
}]))
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
const fields = mapContentFields(field.fields ?? field.schema?.fields ?? [], context);
|
|
982
|
+
return fields.length ? {
|
|
983
|
+
...repeater,
|
|
984
|
+
schema: { fields }
|
|
985
|
+
} : null;
|
|
687
986
|
}
|
|
688
987
|
default: return null;
|
|
689
988
|
}
|
|
@@ -706,14 +1005,7 @@ function startCase(value) {
|
|
|
706
1005
|
* @returns Array of field definitions for form rendering
|
|
707
1006
|
*/
|
|
708
1007
|
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;
|
|
1008
|
+
return mapContentFields(schema.fields ?? [], context);
|
|
717
1009
|
}
|
|
718
1010
|
function expandButtonFieldGroup(fieldId, fieldLabel, context = {}) {
|
|
719
1011
|
const variants = context.variants ?? [...DEFAULT_BUTTON_VARIANTS];
|
|
@@ -1233,26 +1525,7 @@ function getPublishedMetadataFromSnapshot(snapshot, routing) {
|
|
|
1233
1525
|
};
|
|
1234
1526
|
}
|
|
1235
1527
|
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
|
-
});
|
|
1528
|
+
const publishedMessage = formatPublishMessage(snapshot, options.formatPublicationDate);
|
|
1256
1529
|
const publishLabel = routeLifecycleContext(snapshot)?.draftStatus === "scheduled" ? "Publish now" : `Publish ${options.entityLabel.toLowerCase()}`;
|
|
1257
1530
|
return {
|
|
1258
1531
|
currentStatus: snapshot.status,
|
|
@@ -1263,6 +1536,15 @@ function buildToolbarState(snapshot, context, options) {
|
|
|
1263
1536
|
values: context.values
|
|
1264
1537
|
};
|
|
1265
1538
|
}
|
|
1539
|
+
/** Sentence adapter only; date admission and typography belong to the shared formatter. */
|
|
1540
|
+
function formatPublishMessage(snapshot, formatDate) {
|
|
1541
|
+
if (snapshot.status === "published") {
|
|
1542
|
+
const date = snapshot.published.publishAt ? formatDate(snapshot.published.publishAt) : null;
|
|
1543
|
+
return date ? `Published ${date}` : "Published";
|
|
1544
|
+
}
|
|
1545
|
+
const scheduled = snapshot.status === "scheduled" && snapshot.draft.publishAt ? formatDate(snapshot.draft.publishAt) : null;
|
|
1546
|
+
return scheduled ? `Scheduled for ${scheduled}` : "Not yet published";
|
|
1547
|
+
}
|
|
1266
1548
|
|
|
1267
1549
|
//#endregion
|
|
1268
1550
|
//#region ../content-model/src/content-types/schema.ts
|
|
@@ -1403,6 +1685,10 @@ const BaseContentFieldSchema = z.object({
|
|
|
1403
1685
|
}
|
|
1404
1686
|
}).passthrough();
|
|
1405
1687
|
const ContentFieldSchema = BaseContentFieldSchema.extend({
|
|
1688
|
+
itemTypes: z.lazy(() => z.record(z.string(), z.object({
|
|
1689
|
+
label: z.string().optional(),
|
|
1690
|
+
fields: z.array(ContentFieldSchema).optional()
|
|
1691
|
+
}).passthrough())).optional(),
|
|
1406
1692
|
fields: z.lazy(() => z.array(ContentFieldSchema)).optional(),
|
|
1407
1693
|
schema: z.lazy(() => z.object({ fields: z.array(ContentFieldSchema).optional() }).passthrough()).optional()
|
|
1408
1694
|
});
|
|
@@ -1516,284 +1802,6 @@ function isFileFieldKind(value) {
|
|
|
1516
1802
|
return FILE_FIELD_KINDS.includes(value);
|
|
1517
1803
|
}
|
|
1518
1804
|
|
|
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
1805
|
//#endregion
|
|
1798
1806
|
//#region ../content-model/src/content-types/scaffold.ts
|
|
1799
1807
|
/** Nested subfields for group/repeater/button-style fields (both DSL shapes). */
|
|
@@ -4324,13 +4332,14 @@ const FIELD_TYPE_CLASSIFICATION = {
|
|
|
4324
4332
|
repeater: "whole",
|
|
4325
4333
|
presetOrCustom: "opaque",
|
|
4326
4334
|
contentTypeSelect: "opaque",
|
|
4327
|
-
entryPicker: "
|
|
4335
|
+
entryPicker: "leaf",
|
|
4328
4336
|
time: "opaque"
|
|
4329
4337
|
};
|
|
4330
4338
|
function classify(type) {
|
|
4331
4339
|
return FIELD_TYPE_CLASSIFICATION[type] ?? "unknown";
|
|
4332
4340
|
}
|
|
4333
4341
|
function leafToBlockFieldType(type) {
|
|
4342
|
+
if (type === "entryPicker") return "reference";
|
|
4334
4343
|
return classify(type) === "leaf" ? type : null;
|
|
4335
4344
|
}
|
|
4336
4345
|
/**
|
|
@@ -4342,10 +4351,11 @@ function collectLeaves(fields, prefix, out) {
|
|
|
4342
4351
|
for (const field of fields) {
|
|
4343
4352
|
const id = prefix ? `${prefix}.${field.id}` : field.id;
|
|
4344
4353
|
const cls = classify(field.type);
|
|
4345
|
-
|
|
4354
|
+
const leafType = leafToBlockFieldType(field.type);
|
|
4355
|
+
if (leafType) {
|
|
4346
4356
|
out.push({
|
|
4347
4357
|
id,
|
|
4348
|
-
type:
|
|
4358
|
+
type: leafType,
|
|
4349
4359
|
required: Boolean(field.required)
|
|
4350
4360
|
});
|
|
4351
4361
|
continue;
|
|
@@ -4366,7 +4376,6 @@ function collectLeaves(fields, prefix, out) {
|
|
|
4366
4376
|
if (tab.fields) collectLeaves(tab.fields, id, out);
|
|
4367
4377
|
}
|
|
4368
4378
|
}
|
|
4369
|
-
void leafToBlockFieldType;
|
|
4370
4379
|
}
|
|
4371
4380
|
}
|
|
4372
4381
|
function buildManifestRepeaterShape(field) {
|
|
@@ -4754,15 +4763,14 @@ const eventBookingOfferInputSchema = classifiedSchema((input) => {
|
|
|
4754
4763
|
}]
|
|
4755
4764
|
} : result;
|
|
4756
4765
|
});
|
|
4757
|
-
const
|
|
4766
|
+
const eventRunCommonInputSchema = z.object({
|
|
4758
4767
|
identifier: z.string().min(1),
|
|
4759
4768
|
eventRef: z.string().min(1),
|
|
4760
|
-
name: z.string().nullable().optional(),
|
|
4761
4769
|
status: sdkEventUpsertOptionalFieldSchemas.status,
|
|
4762
4770
|
visibility: sdkEventUpsertOptionalFieldSchemas.visibility,
|
|
4763
|
-
bookingScope: z.enum(["single_occurrence", "whole_series"]),
|
|
4764
4771
|
occurrences: sdkEventUpsertOptionalFieldSchemas.occurrences.unwrap(),
|
|
4765
4772
|
schedule: eventScheduleInputSchema.optional(),
|
|
4773
|
+
attendeeMigration: sdkEventUpsertOptionalFieldSchemas.attendeeMigration,
|
|
4766
4774
|
bookingOffer: eventBookingOfferInputSchema.optional(),
|
|
4767
4775
|
capacity: sdkEventUpsertOptionalFieldSchemas.capacity,
|
|
4768
4776
|
venueRef: z.string().nullable().optional(),
|
|
@@ -4770,6 +4778,10 @@ const eventRunInputSchema = z.object({
|
|
|
4770
4778
|
settings: z.record(z.string(), z.unknown()).optional(),
|
|
4771
4779
|
tags: z.array(z.string()).optional()
|
|
4772
4780
|
}).strict();
|
|
4781
|
+
const eventRunInputSchema = z.discriminatedUnion("bookingScope", [eventRunCommonInputSchema.extend({ bookingScope: z.literal("single_occurrence") }).strict(), eventRunCommonInputSchema.extend({
|
|
4782
|
+
bookingScope: z.literal("whole_series"),
|
|
4783
|
+
name: z.string().nullable().optional()
|
|
4784
|
+
}).strict()]);
|
|
4773
4785
|
const eventUpsertInputSchema = z.object({
|
|
4774
4786
|
identifier: z.string().min(1).optional(),
|
|
4775
4787
|
courseBooking: sdkCourseBookingSettingsSchema.optional(),
|