@shapio/schema 0.5.8 → 0.5.10

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { _ as STABLE_ID_PATTERN, a as MAX_RICHTEXT_DEPTH, c as BIGINTEGER_PATTERN, d as DECIMAL_PATTERN, f as RICHTEXT_FORMAT, g as UID_PATTERN, h as TIME_PATTERN, i as MARK_RULES, l as DATETIME_PATTERN, m as SLUG_PATTERN, n as richTextMediaIds, o as NODE_RULES, p as RICHTEXT_FORMAT_VERSION, r as validateRichText, s as SAFE_LINK_PATTERN, t as isEmptyRichText, u as DATE_PATTERN, v as createStableId, y as isStableId } from "./validate-BQGutaBo.js";
2
- import { a as isValidTitleTemplate, c as SEO_COMPONENT_ID, d as SEO_FIELD_IDS, f as SEO_TITLE_PLACEHOLDER, h as SEO_TWITTER_HANDLE_PATTERN, i as applyTitleTemplate, l as SEO_DESCRIPTION_SOFT_MAX, m as SEO_TITLE_TEMPLATE_PATTERN, n as resolveSeo, o as seoDefaultsForLocale, p as SEO_TITLE_SOFT_MAX, r as EMPTY_SEO_DEFAULTS, s as SEO_COMPONENT_API_KEY, t as DEFAULT_SEO_KEYS, u as SEO_EDITOR_ID } from "./resolve-viddCEYd.js";
2
+ import { a as isValidTitleTemplate, c as SEO_COMPONENT_ID, d as SEO_FIELD_IDS, f as SEO_TITLE_PLACEHOLDER, h as SEO_TWITTER_HANDLE_PATTERN, i as applyTitleTemplate, l as SEO_DESCRIPTION_SOFT_MAX, m as SEO_TITLE_TEMPLATE_PATTERN, n as resolveSeo, o as seoDefaultsForLocale, p as SEO_TITLE_SOFT_MAX, r as EMPTY_SEO_DEFAULTS, s as SEO_COMPONENT_API_KEY, t as DEFAULT_SEO_KEYS, u as SEO_EDITOR_ID } from "./resolve-Csjpkpue.js";
3
3
  import { Type } from "typebox";
4
4
  import pluralize from "pluralize";
5
5
  import { Value } from "typebox/value";
@@ -224,6 +224,11 @@ const ModelDefinitionInputSchema = Type.Object({
224
224
  localized: Type.Optional(Type.Boolean()),
225
225
  /** Entries have separate draft and published states. Default true. */
226
226
  draftAndPublish: Type.Optional(Type.Boolean()),
227
+ /**
228
+ * Collections only: the stable ID of the collection this one extends (its parent). The model inherits
229
+ * the parent's fields, locked, and adds its own (plan polymorphic-types).
230
+ */
231
+ extends: Type.Optional(Type.String()),
227
232
  display: Type.Optional(ModelDisplayInputSchema)
228
233
  }, closed$3);
229
234
  const ComponentDefinitionInputSchema = Type.Object({
@@ -409,7 +414,7 @@ const SETTINGS_SCHEMAS = {
409
414
  /** Whether a field holds the built-in SEO component (by stable ID, whatever its API ID or editor). */
410
415
  const isSeoField = (field) => field.type === "component" && field.settings.component === "f3905a00-d2a3-4c62-899a-c894bfd6cb9b" && !field.settings.repeatable;
411
416
  //#endregion
412
- //#region src/editors/catalogue.ts
417
+ //#region src/editors/catalog.ts
413
418
  const closed$1 = { additionalProperties: false };
414
419
  const Placeholder = Type.Optional(Type.String({ maxLength: 200 }));
415
420
  const isMultipleEnum = (field) => field.type === "enum" && field.settings.multiple;
@@ -563,7 +568,7 @@ const entries = [
563
568
  optionsSchema: Type.Object({ collapsed: Type.Optional(Type.Boolean()) }, closed$1)
564
569
  }
565
570
  ];
566
- const EDITOR_CATALOGUE = new Map(entries.map((entry) => [entry.id, entry]));
571
+ const EDITOR_CATALOG = new Map(entries.map((entry) => [entry.id, entry]));
567
572
  /** The editor a field gets when its definition names none. */
568
573
  const DEFAULT_EDITORS = {
569
574
  string: "textInput",
@@ -731,7 +736,7 @@ const ungroupedSectionId = (first, groupIds) => {
731
736
  /**
732
737
  * The entry form's layout (`display.layout: 'form'`): every live field, the title included, in sections.
733
738
  * Field order decides everything: a group's section sits where its first field appears, with its fields in
734
- * field order, and every run of consecutive ungrouped fields is an unlabelled section of its own at its
739
+ * field order, and every run of consecutive ungrouped fields is an unlabeled section of its own at its
735
740
  * place. So name, price, sku, description with a Pricing group (price, sku) reads [name], Pricing [price,
736
741
  * sku], [description]. Unknown and deprecated ids are skipped. Pure: no values, no I/O.
737
742
  */
@@ -828,159 +833,148 @@ const suggestPlural = (apiKey) => {
828
833
  return `${apiKey.slice(0, 59)}${PLURAL_FALLBACK_SUFFIX}`;
829
834
  };
830
835
  //#endregion
831
- //#region src/normalize.ts
832
- const SETTINGS_DEFAULTS = {
833
- code: { language: "plain" },
834
- richtext: { formatVersion: 1 },
835
- enum: { multiple: false },
836
- media: { multiple: false },
837
- component: { repeatable: false }
838
- };
839
- /** Optional text properties where an empty string means "not set". */
840
- const OPTIONAL_TEXT_KEYS = /* @__PURE__ */ new Set([
841
- "description",
842
- "category",
843
- "icon",
844
- "titleFieldId",
845
- "coverFieldId",
846
- "pluralApiKey"
847
- ]);
848
- /** Optional list properties where an empty list means "not set". `fields` is never optional. */
849
- const OPTIONAL_LIST_KEYS = /* @__PURE__ */ new Set([
850
- "listFieldIds",
851
- "groups",
852
- "canvasFieldIds",
853
- "stripFieldIds"
854
- ]);
855
- const withoutEmpty = (value) => {
856
- const result = {};
857
- for (const [key, entry] of Object.entries(value)) {
858
- const isEmptyText = OPTIONAL_TEXT_KEYS.has(key) && entry === "";
859
- const isEmptyList = OPTIONAL_LIST_KEYS.has(key) && Array.isArray(entry) && entry.length === 0;
860
- if (entry === void 0 || isEmptyText || isEmptyList) continue;
861
- result[key] = entry;
836
+ //#region src/inheritance/resolve.ts
837
+ /**
838
+ * Model inheritance (plan polymorphic-types). A collection may extend another (`extends`, the parent's stable
839
+ * ID). Stored definitions hold their own fields only; the effective definition is resolved at read time:
840
+ * every ancestor's fields (root first, each marked `inheritedFrom`), then the model's own, and the display
841
+ * merged down the chain. Nothing is copied into children, so a parent edit changes one definition.
842
+ */
843
+ /** Most levels a family may have, root included (`Products > Physical > Apparel` is three). */
844
+ const MAX_INHERITANCE_DEPTH = 8;
845
+ /** A definition that may be a parent: a collection. */
846
+ const canBeParent = (definition) => definition !== void 0 && definition.kind === "collection";
847
+ /**
848
+ * The ancestors of a definition, root first, walking `extends` up. Stops at the first problem (missing,
849
+ * invalid or cyclic parent, or more than MAX_INHERITANCE_DEPTH levels) and returns what resolved so far.
850
+ */
851
+ const ancestorChain = (definition, lookup) => {
852
+ const upward = [];
853
+ const seen = /* @__PURE__ */ new Set([definition.id]);
854
+ let current = definition;
855
+ while (isModelDefinition(current) && current.extends !== void 0) {
856
+ const parentId = current.extends;
857
+ const at = current.id;
858
+ const stop = (problem) => ({
859
+ ids: upward.reverse(),
860
+ problem,
861
+ problemAt: at
862
+ });
863
+ if (parentId === current.id) return stop("invalid");
864
+ if (seen.has(parentId)) return stop("cycle");
865
+ const parent = lookup(parentId);
866
+ if (!parent) return stop("missing");
867
+ if (!canBeParent(parent)) return stop("invalid");
868
+ if (upward.length + 1 >= 8) return stop("tooDeep");
869
+ upward.push(parentId);
870
+ seen.add(parentId);
871
+ current = parent;
862
872
  }
863
- return result;
864
- };
865
- const normalizeField = (field) => {
866
- const normalized = {
867
- id: field.id,
868
- apiKey: field.apiKey,
869
- label: field.label,
870
- description: field.description,
871
- type: field.type,
872
- required: field.required ?? false,
873
- localized: field.localized ?? false,
874
- public: field.public ?? true,
875
- unique: field.unique ?? false,
876
- filterable: field.filterable ?? false,
877
- sortable: field.sortable ?? false,
878
- deprecated: field.deprecated ?? false,
879
- defaultValue: field.defaultValue,
880
- settings: {
881
- ...SETTINGS_DEFAULTS[field.type],
882
- ...field.settings
883
- },
884
- editor: {
885
- id: field.editor?.id ?? DEFAULT_EDITORS[field.type],
886
- options: field.editor?.options ?? {}
887
- },
888
- width: field.width === "full" ? void 0 : field.width
889
- };
890
- return withoutEmpty(normalized);
891
- };
892
- const normalizeModelDisplay = (display) => withoutEmpty({
893
- ...display,
894
- layout: display?.layout === "document" ? void 0 : display?.layout
895
- });
896
- const normalizeComponentDisplay = (display) => withoutEmpty({ ...display });
873
+ return { ids: upward.reverse() };
874
+ };
875
+ /** Fields a model sees from one ancestor: its own fields, marked with the ancestor's ID. */
876
+ const inheritedFieldsOf = (ancestor) => ancestor.fields.map((field) => ({
877
+ ...field,
878
+ inheritedFrom: ancestor.id
879
+ }));
880
+ /** IDs of the fields in `fields` that `pick` keeps, in field order. */
881
+ const fieldIdsWhere = (fields, pick) => fields.filter(pick).map((field) => field.id);
897
882
  /**
898
- * Applies defaults so that two definitions meaning the same thing are byte-identical once serialized
899
- * canonically: a hand-written file that omits `public` hashes the same as one that says `public: true`.
900
- * Empty optional strings and arrays are dropped, and so are the defaults of keys added after definitions
901
- * were first hashed (`width: 'full'`, `display.layout: 'document'`). A collection without a plural API ID
902
- * gets the suggested one (`suggestPlural`), so files and definitions from before plural API IDs keep working.
883
+ * A child's display over its parent's (already merged) one. Scalars and `listFieldIds` take the child's value
884
+ * when set, else the parent's. Groups are the parent's, then the child's. Canvas and strip take the child's
885
+ * list when set; else the parent's list (when set) with the child's own fields appended where the automatic
886
+ * rule would put them: canvas-eligible fields to the canvas, the other fields (not the cover) to the strip.
903
887
  */
904
- const normalizeDefinition = (input) => {
905
- const fields = input.fields.map(normalizeField);
906
- if (input.kind === "component") return withoutEmpty({
907
- id: input.id,
908
- kind: "component",
909
- apiKey: input.apiKey,
910
- label: input.label,
911
- description: input.description,
912
- category: input.category,
913
- icon: input.icon,
914
- fields,
915
- display: normalizeComponentDisplay(input.display)
916
- });
917
- return withoutEmpty({
918
- id: input.id,
919
- kind: input.kind,
920
- apiKey: input.apiKey,
921
- pluralApiKey: input.kind === "collection" ? input.pluralApiKey || suggestPlural(input.apiKey) : void 0,
922
- label: input.label,
923
- description: input.description,
924
- localized: input.localized ?? false,
925
- draftAndPublish: input.draftAndPublish ?? true,
926
- fields,
927
- display: normalizeModelDisplay(input.display)
928
- });
888
+ const mergeDisplay = (parent, child, ownFields) => {
889
+ const merged = {};
890
+ const layout = child.layout ?? parent.layout;
891
+ const titleFieldId = child.titleFieldId ?? parent.titleFieldId;
892
+ const listFieldIds = child.listFieldIds ?? parent.listFieldIds;
893
+ const defaultSort = child.defaultSort ?? parent.defaultSort;
894
+ const coverFieldId = child.coverFieldId ?? parent.coverFieldId;
895
+ const groups = [...parent.groups ?? [], ...child.groups ?? []];
896
+ const ownCanvas = fieldIdsWhere(ownFields, isCanvasEligible);
897
+ const canvasFieldIds = child.canvasFieldIds ?? (parent.canvasFieldIds ? [...parent.canvasFieldIds, ...ownCanvas] : void 0);
898
+ const canvas = new Set(canvasFieldIds ?? []);
899
+ const stripFieldIds = child.stripFieldIds ?? (parent.stripFieldIds ? [...parent.stripFieldIds, ...fieldIdsWhere(ownFields, (field) => !isCanvasEligible(field) && !canvas.has(field.id) && field.id !== coverFieldId)] : void 0);
900
+ if (layout !== void 0) merged.layout = layout;
901
+ if (titleFieldId !== void 0) merged.titleFieldId = titleFieldId;
902
+ if (listFieldIds !== void 0) merged.listFieldIds = listFieldIds;
903
+ if (defaultSort !== void 0) merged.defaultSort = defaultSort;
904
+ if (groups.length > 0) merged.groups = groups;
905
+ if (canvasFieldIds !== void 0) merged.canvasFieldIds = canvasFieldIds;
906
+ if (coverFieldId !== void 0) merged.coverFieldId = coverFieldId;
907
+ if (stripFieldIds !== void 0) merged.stripFieldIds = stripFieldIds;
908
+ return merged;
929
909
  };
930
- /** Normalizes a definition that is already normalized-shaped (e.g. read back from the database). */
931
- const renormalizeDefinition = (definition) => normalizeDefinition(definition);
932
- //#endregion
933
- //#region src/validators/defaultValue.ts
934
910
  /**
935
- * Checks that a field's `defaultValue` has the right JSON shape for its type. Full content rules
936
- * (lengths, ranges) are applied by the content validator when the default is used.
937
- * Returns a message, or null when the default is acceptable.
911
+ * The effective definition of a model: ancestors' fields first (root first, `inheritedFrom` set, otherwise the
912
+ * same field objects' values and IDs), then its own fields; display merged root → model. A broken chain
913
+ * (see `ancestorChain`) resolves over the part that resolved; the validator reports the problem.
938
914
  */
939
- const checkDefaultValue = (field) => {
940
- const value = field.defaultValue;
941
- if (value === void 0) return null;
942
- const isString = typeof value === "string";
943
- const matches = (pattern) => isString && pattern.test(value);
944
- switch (field.type) {
945
- case "string":
946
- case "text":
947
- case "code":
948
- case "email":
949
- case "url": return isString ? null : "must be a string";
950
- case "slug": return matches(SLUG_PATTERN) ? null : "must be a lower-case slug";
951
- case "uid": return matches(UID_PATTERN) ? null : "must contain only URL-safe characters";
952
- case "number": return typeof value === "number" && Number.isFinite(value) ? null : "must be a number";
953
- case "integer": return Number.isSafeInteger(value) ? null : "must be an integer";
954
- case "decimal": return matches(DECIMAL_PATTERN) ? null : "must be a decimal string, e.g. \"12.50\"";
955
- case "biginteger": return matches(BIGINTEGER_PATTERN) ? null : "must be an integer string, e.g. \"9007199254740993\"";
956
- case "boolean": return typeof value === "boolean" ? null : "must be true or false";
957
- case "date": return matches(DATE_PATTERN) ? null : "must be a date, YYYY-MM-DD";
958
- case "datetime": return matches(DATETIME_PATTERN) ? null : "must be a UTC date-time, YYYY-MM-DDTHH:mm:ssZ";
959
- case "time": return matches(TIME_PATTERN) ? null : "must be a time, HH:mm[:ss]";
960
- case "enum": {
961
- const allowed = new Set(field.settings.values.map((entry) => entry.value));
962
- if (field.settings.multiple) return Array.isArray(value) && value.every((item) => typeof item === "string" && allowed.has(item)) ? null : "must be a list of the enum values";
963
- return isString && allowed.has(value) ? null : "must be one of the enum values";
964
- }
965
- case "json": return null;
966
- default: return `${field.type} fields cannot have a default value`;
915
+ const resolveModelDefinition = (definition, lookup) => {
916
+ const { ids } = ancestorChain(definition, lookup);
917
+ const ancestors = ids.map((id) => lookup(id));
918
+ let display;
919
+ const fields = [];
920
+ for (const ancestor of ancestors) {
921
+ display = display === void 0 ? ancestor.display : mergeDisplay(display, ancestor.display, ancestor.fields);
922
+ fields.push(...inheritedFieldsOf(ancestor));
967
923
  }
924
+ return {
925
+ ...definition,
926
+ fields: [...fields, ...definition.fields],
927
+ display: display === void 0 ? definition.display : mergeDisplay(display, definition.display, definition.fields),
928
+ ancestorIds: ids
929
+ };
968
930
  };
969
- //#endregion
970
- //#region src/validators/issues.ts
971
- var SchemaValidationError = class extends Error {
972
- issues;
973
- constructor(issues) {
974
- super(`Schema definition is invalid: ${issues.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`);
975
- this.name = "SchemaValidationError";
976
- this.issues = issues;
977
- }
931
+ /** The fields a resolved model declares itself (not inherited). */
932
+ const ownFields = (resolved) => resolved.fields.filter((field) => field.inheritedFrom === void 0);
933
+ /** Definitions whose parent is `id`, in the given order. */
934
+ const findChildDefinitions = (definitions, id) => definitions.filter((definition) => isModelDefinition(definition) && definition.extends === id && definition.id !== id);
935
+ const memo = (compute) => {
936
+ const cache = /* @__PURE__ */ new Map();
937
+ return (id) => {
938
+ if (!cache.has(id)) cache.set(id, compute(id));
939
+ return cache.get(id);
940
+ };
941
+ };
942
+ const createDefinitionIndex = (definitions) => {
943
+ const byId = new Map(definitions.map((definition) => [definition.id, definition]));
944
+ const lookup = (id) => byId.get(id);
945
+ const children = /* @__PURE__ */ new Map();
946
+ for (const definition of definitions) if (isModelDefinition(definition) && definition.extends !== void 0 && definition.extends !== definition.id) children.set(definition.extends, [...children.get(definition.extends) ?? [], definition.id]);
947
+ const ancestors = memo((id) => {
948
+ const definition = byId.get(id);
949
+ return definition ? ancestorChain(definition, lookup).ids : [];
950
+ });
951
+ const childrenOf = (id) => children.get(id) ?? [];
952
+ const descendants = memo((id) => {
953
+ const found = [];
954
+ const seen = /* @__PURE__ */ new Set([id]);
955
+ const walk = (current) => {
956
+ for (const child of childrenOf(current)) if (!seen.has(child)) {
957
+ seen.add(child);
958
+ found.push(child);
959
+ walk(child);
960
+ }
961
+ };
962
+ walk(id);
963
+ return found;
964
+ });
965
+ return {
966
+ get: lookup,
967
+ resolve: memo((id) => {
968
+ const definition = byId.get(id);
969
+ return definition && isModelDefinition(definition) ? resolveModelDefinition(definition, lookup) : void 0;
970
+ }),
971
+ ancestors,
972
+ childrenOf,
973
+ descendants,
974
+ family: memo((id) => [id, ...descendants(id)]),
975
+ rootOf: (id) => ancestors(id)[0] ?? id
976
+ };
978
977
  };
979
- const issue = (path, code, message) => ({
980
- path,
981
- code,
982
- message
983
- });
984
978
  //#endregion
985
979
  //#region src/validators/naming.ts
986
980
  /**
@@ -1000,6 +994,11 @@ const MODEL_TYPE_SUFFIXES = [
1000
994
  "Sort"
1001
995
  ];
1002
996
  const COMPONENT_TYPE_SUFFIXES = ["", "Input"];
997
+ /**
998
+ * Suffix of the GraphQL interface a model with children gets (`ProductInterface`, plan polymorphic-types).
999
+ * Claimed only by models that have children, so existing schemas keep validating.
1000
+ */
1001
+ const INTERFACE_TYPE_SUFFIX = "Interface";
1003
1002
  const MUTATION_PREFIXES = [
1004
1003
  "create",
1005
1004
  "update",
@@ -1124,11 +1123,15 @@ const RESERVED_QUERY_NAMES = [
1124
1123
  "_snapshot",
1125
1124
  "_site"
1126
1125
  ];
1127
- /** Every name a definition contributes to the GraphQL schema. */
1128
- const generatedNames = (definition) => {
1126
+ /**
1127
+ * Every name a definition contributes to the GraphQL schema. Fields are its own: a model that extends another
1128
+ * reuses the enum and zone types of inherited fields, which belong to the declaring model. `hasChildren` adds
1129
+ * the interface name of a model other models extend.
1130
+ */
1131
+ const generatedNames = (definition, options = {}) => {
1129
1132
  const typeName = toTypeName(definition.apiKey);
1130
1133
  const definitionId = definition.id;
1131
- const names = (definition.kind === "component" ? COMPONENT_TYPE_SUFFIXES : MODEL_TYPE_SUFFIXES).map((suffix) => ({
1134
+ const names = [...definition.kind === "component" ? COMPONENT_TYPE_SUFFIXES : MODEL_TYPE_SUFFIXES, ...options.hasChildren && definition.kind !== "component" ? [INTERFACE_TYPE_SUFFIX] : []].map((suffix) => ({
1132
1135
  namespace: "type",
1133
1136
  name: `${typeName}${suffix}`,
1134
1137
  definitionId
@@ -1177,6 +1180,198 @@ const generatedNames = (definition) => {
1177
1180
  return names;
1178
1181
  };
1179
1182
  //#endregion
1183
+ //#region src/naming/routePath.ts
1184
+ /**
1185
+ * Nested delivery paths (plan polymorphic-types). A model that extends another is addressed under its
1186
+ * ancestors: `/api/content/products/digital` is the route key `products/digital`, the ancestors' route keys
1187
+ * (`routeKeyOf`) and its own, joined by `/`. `routeKeyOf` alone stays the plural API ID that GraphQL names
1188
+ * and the API ID namespace use.
1189
+ */
1190
+ const SEGMENT = API_KEY_PATTERN.source.slice(1, -1);
1191
+ /** A route key: one to MAX_INHERITANCE_DEPTH API IDs joined by `/` (`products/digital`). */
1192
+ const ROUTE_KEY_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT}){0,7}$`);
1193
+ /** Longest route key: every level at the longest API ID, plus the separators. */
1194
+ const MAX_ROUTE_KEY_LENGTH = 520;
1195
+ /** The route key of a model under its ancestors (root first): `products/digital`. A root's is `routeKeyOf`. */
1196
+ const routePathOf = (definition, ancestors) => [...ancestors, definition].map(routeKeyOf).join("/");
1197
+ /**
1198
+ * Splits the path segments after a content prefix (`/api/content/`) into the route key and the rest: the
1199
+ * leading segments that are API IDs (at most MAX_INHERITANCE_DEPTH) form the key; the first one that is not
1200
+ * (an entry ID is a UUID, which always has a hyphen) starts the rest. A single segment that already holds a
1201
+ * whole route key (`products%2Fdigital`, decoded) is taken as it is. Undefined when no segment is a key.
1202
+ * Purely syntactic: shared by the server's URL rewrite and the in-process delivery matcher.
1203
+ */
1204
+ const splitRoutePath = (segments) => {
1205
+ const [first] = segments;
1206
+ if (first === void 0) return;
1207
+ if (!API_KEY_PATTERN.test(first)) return ROUTE_KEY_PATTERN.test(first) ? {
1208
+ routeKey: first,
1209
+ rest: segments.slice(1)
1210
+ } : void 0;
1211
+ let count = 1;
1212
+ while (count < segments.length && count < 8 && API_KEY_PATTERN.test(segments[count])) count += 1;
1213
+ return {
1214
+ routeKey: segments.slice(0, count).join("/"),
1215
+ rest: segments.slice(count)
1216
+ };
1217
+ };
1218
+ //#endregion
1219
+ //#region src/normalize.ts
1220
+ const SETTINGS_DEFAULTS = {
1221
+ code: { language: "plain" },
1222
+ richtext: { formatVersion: 1 },
1223
+ enum: { multiple: false },
1224
+ media: { multiple: false },
1225
+ component: { repeatable: false }
1226
+ };
1227
+ /** Optional text properties where an empty string means "not set". */
1228
+ const OPTIONAL_TEXT_KEYS = /* @__PURE__ */ new Set([
1229
+ "description",
1230
+ "category",
1231
+ "icon",
1232
+ "titleFieldId",
1233
+ "coverFieldId",
1234
+ "pluralApiKey",
1235
+ "extends"
1236
+ ]);
1237
+ /** Optional list properties where an empty list means "not set". `fields` is never optional. */
1238
+ const OPTIONAL_LIST_KEYS = /* @__PURE__ */ new Set([
1239
+ "listFieldIds",
1240
+ "groups",
1241
+ "canvasFieldIds",
1242
+ "stripFieldIds"
1243
+ ]);
1244
+ const withoutEmpty = (value) => {
1245
+ const result = {};
1246
+ for (const [key, entry] of Object.entries(value)) {
1247
+ const isEmptyText = OPTIONAL_TEXT_KEYS.has(key) && entry === "";
1248
+ const isEmptyList = OPTIONAL_LIST_KEYS.has(key) && Array.isArray(entry) && entry.length === 0;
1249
+ if (entry === void 0 || isEmptyText || isEmptyList) continue;
1250
+ result[key] = entry;
1251
+ }
1252
+ return result;
1253
+ };
1254
+ const normalizeField = (field) => {
1255
+ const normalized = {
1256
+ id: field.id,
1257
+ apiKey: field.apiKey,
1258
+ label: field.label,
1259
+ description: field.description,
1260
+ type: field.type,
1261
+ required: field.required ?? false,
1262
+ localized: field.localized ?? false,
1263
+ public: field.public ?? true,
1264
+ unique: field.unique ?? false,
1265
+ filterable: field.filterable ?? false,
1266
+ sortable: field.sortable ?? false,
1267
+ deprecated: field.deprecated ?? false,
1268
+ defaultValue: field.defaultValue,
1269
+ settings: {
1270
+ ...SETTINGS_DEFAULTS[field.type],
1271
+ ...field.settings
1272
+ },
1273
+ editor: {
1274
+ id: field.editor?.id ?? DEFAULT_EDITORS[field.type],
1275
+ options: field.editor?.options ?? {}
1276
+ },
1277
+ width: field.width === "full" ? void 0 : field.width
1278
+ };
1279
+ return withoutEmpty(normalized);
1280
+ };
1281
+ const normalizeModelDisplay = (display) => withoutEmpty({
1282
+ ...display,
1283
+ layout: display?.layout === "document" ? void 0 : display?.layout
1284
+ });
1285
+ const normalizeComponentDisplay = (display) => withoutEmpty({ ...display });
1286
+ /**
1287
+ * Applies defaults so that two definitions meaning the same thing are byte-identical once serialized
1288
+ * canonically: a hand-written file that omits `public` hashes the same as one that says `public: true`.
1289
+ * Empty optional strings and arrays are dropped, and so are the defaults of keys added after definitions
1290
+ * were first hashed (`width: 'full'`, `display.layout: 'document'`). A collection without a plural API ID
1291
+ * gets the suggested one (`suggestPlural`), so files and definitions from before plural API IDs keep working.
1292
+ */
1293
+ const normalizeDefinition = (input) => {
1294
+ const fields = input.fields.map(normalizeField);
1295
+ if (input.kind === "component") return withoutEmpty({
1296
+ id: input.id,
1297
+ kind: "component",
1298
+ apiKey: input.apiKey,
1299
+ label: input.label,
1300
+ description: input.description,
1301
+ category: input.category,
1302
+ icon: input.icon,
1303
+ fields,
1304
+ display: normalizeComponentDisplay(input.display)
1305
+ });
1306
+ return withoutEmpty({
1307
+ id: input.id,
1308
+ kind: input.kind,
1309
+ apiKey: input.apiKey,
1310
+ pluralApiKey: input.kind === "collection" ? input.pluralApiKey || suggestPlural(input.apiKey) : void 0,
1311
+ label: input.label,
1312
+ description: input.description,
1313
+ localized: input.localized ?? false,
1314
+ draftAndPublish: input.draftAndPublish ?? true,
1315
+ extends: input.extends,
1316
+ fields,
1317
+ display: normalizeModelDisplay(input.display)
1318
+ });
1319
+ };
1320
+ /** Normalizes a definition that is already normalized-shaped (e.g. read back from the database). */
1321
+ const renormalizeDefinition = (definition) => normalizeDefinition(definition);
1322
+ //#endregion
1323
+ //#region src/validators/defaultValue.ts
1324
+ /**
1325
+ * Checks that a field's `defaultValue` has the right JSON shape for its type. Full content rules
1326
+ * (lengths, ranges) are applied by the content validator when the default is used.
1327
+ * Returns a message, or null when the default is acceptable.
1328
+ */
1329
+ const checkDefaultValue = (field) => {
1330
+ const value = field.defaultValue;
1331
+ if (value === void 0) return null;
1332
+ const isString = typeof value === "string";
1333
+ const matches = (pattern) => isString && pattern.test(value);
1334
+ switch (field.type) {
1335
+ case "string":
1336
+ case "text":
1337
+ case "code":
1338
+ case "email":
1339
+ case "url": return isString ? null : "must be a string";
1340
+ case "slug": return matches(SLUG_PATTERN) ? null : "must be a lower-case slug";
1341
+ case "uid": return matches(UID_PATTERN) ? null : "must contain only URL-safe characters";
1342
+ case "number": return typeof value === "number" && Number.isFinite(value) ? null : "must be a number";
1343
+ case "integer": return Number.isSafeInteger(value) ? null : "must be an integer";
1344
+ case "decimal": return matches(DECIMAL_PATTERN) ? null : "must be a decimal string, e.g. \"12.50\"";
1345
+ case "biginteger": return matches(BIGINTEGER_PATTERN) ? null : "must be an integer string, e.g. \"9007199254740993\"";
1346
+ case "boolean": return typeof value === "boolean" ? null : "must be true or false";
1347
+ case "date": return matches(DATE_PATTERN) ? null : "must be a date, YYYY-MM-DD";
1348
+ case "datetime": return matches(DATETIME_PATTERN) ? null : "must be a UTC date-time, YYYY-MM-DDTHH:mm:ssZ";
1349
+ case "time": return matches(TIME_PATTERN) ? null : "must be a time, HH:mm[:ss]";
1350
+ case "enum": {
1351
+ const allowed = new Set(field.settings.values.map((entry) => entry.value));
1352
+ if (field.settings.multiple) return Array.isArray(value) && value.every((item) => typeof item === "string" && allowed.has(item)) ? null : "must be a list of the enum values";
1353
+ return isString && allowed.has(value) ? null : "must be one of the enum values";
1354
+ }
1355
+ case "json": return null;
1356
+ default: return `${field.type} fields cannot have a default value`;
1357
+ }
1358
+ };
1359
+ //#endregion
1360
+ //#region src/validators/issues.ts
1361
+ var SchemaValidationError = class extends Error {
1362
+ issues;
1363
+ constructor(issues) {
1364
+ super(`Schema definition is invalid: ${issues.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`);
1365
+ this.name = "SchemaValidationError";
1366
+ this.issues = issues;
1367
+ }
1368
+ };
1369
+ const issue = (path, code, message) => ({
1370
+ path,
1371
+ code,
1372
+ message
1373
+ });
1374
+ //#endregion
1180
1375
  //#region src/validators/text.ts
1181
1376
  /**
1182
1377
  * Text no database can store as is: PostgreSQL `text` and `jsonb` reject U+0000, and `jsonb` rejects an
@@ -1360,7 +1555,7 @@ const checkFlags = (field, path, isComponent) => {
1360
1555
  const checkEditor = (field, path) => {
1361
1556
  const { id, options } = field.editor;
1362
1557
  if (isCustomEditorId(id)) return [];
1363
- const entry = EDITOR_CATALOGUE.get(id);
1558
+ const entry = EDITOR_CATALOG.get(id);
1364
1559
  if (!entry) return [issue(`${path}/editor/id`, "UNKNOWN_EDITOR", `"${id}" is not a built-in editor (custom editors use a vendor.name ID)`)];
1365
1560
  if (!entry.dataTypes.includes(field.type) || !(entry.supports?.(field) ?? true)) return [issue(`${path}/editor/id`, "INCOMPATIBLE_EDITOR", `the ${id} editor cannot edit this ${field.type} field`)];
1366
1561
  return collectSchemaErrors(entry.optionsSchema, options, `${path}/editor/options`).map((error) => issue(error.path, "INVALID_EDITOR_OPTIONS", error.message));
@@ -1451,6 +1646,10 @@ const checkDocumentLayout = (model, fields) => {
1451
1646
  }));
1452
1647
  return issues;
1453
1648
  };
1649
+ /**
1650
+ * The display's references to fields. Also run by the inheritance check on a child's resolved definition,
1651
+ * whose display may name inherited fields.
1652
+ */
1454
1653
  const checkDisplay = (definition) => {
1455
1654
  const fields = new Map(definition.fields.map((field) => [field.id, field]));
1456
1655
  const fieldIds = new Set(fields.keys());
@@ -1499,10 +1698,94 @@ const validateDefinition = (definition) => {
1499
1698
  if (!syntax) issues.push(...checkPluralApiKey(definition));
1500
1699
  definition.fields.forEach((field, index) => issues.push(...checkField(field, index, definition)));
1501
1700
  issues.push(...checkFieldUniqueness(definition));
1502
- issues.push(...checkDisplay(definition));
1701
+ if (definition.kind === "component" || definition.extends === void 0) issues.push(...checkDisplay(definition));
1503
1702
  return issues;
1504
1703
  };
1505
1704
  //#endregion
1705
+ //#region src/validators/inheritance.ts
1706
+ /**
1707
+ * Cross-definition checks of model inheritance (plan polymorphic-types): the parent exists and is a
1708
+ * collection, no cycles, at most MAX_INHERITANCE_DEPTH levels, a child's flags equal its parent's, its own
1709
+ * fields' API IDs differ from every inherited one, and its display is valid against its resolved fields.
1710
+ * Issues are reported on the definition that has to change, on its own paths.
1711
+ */
1712
+ const withDefinition$1 = (definition, found) => ({
1713
+ ...found,
1714
+ definitionId: definition.id
1715
+ });
1716
+ /** Whether following `extends` from the definition comes back to it. */
1717
+ const isInCycle = (definition, index) => {
1718
+ const seen = /* @__PURE__ */ new Set();
1719
+ let current = definition;
1720
+ while (current && isModelDefinition(current) && current.extends !== void 0 && !seen.has(current.id)) {
1721
+ seen.add(current.id);
1722
+ if (current.extends === definition.id) return true;
1723
+ current = index.get(current.extends);
1724
+ }
1725
+ return false;
1726
+ };
1727
+ const parentIssues = (definition, index) => {
1728
+ if (definition.extends === void 0) return [];
1729
+ if (definition.kind !== "collection") return [issue("/extends", "INVALID_PARENT", `only a collection can extend another; this is a ${definition.kind}`)];
1730
+ const chain = ancestorChain(definition, index.get);
1731
+ const parent = index.get(definition.extends);
1732
+ switch (chain.problem) {
1733
+ case "missing": return chain.problemAt === definition.id ? [issue("/extends", "UNKNOWN_PARENT", "extends a type that does not exist")] : [];
1734
+ case "invalid":
1735
+ if (chain.problemAt !== definition.id) return [];
1736
+ return [issue("/extends", "INVALID_PARENT", definition.extends === definition.id ? "a type cannot extend itself" : `can only extend a collection; "${parent?.apiKey ?? definition.extends}" is a ${parent?.kind ?? "definition"}`)];
1737
+ case "cycle": return isInCycle(definition, index) ? [issue("/extends", "INHERITANCE_CYCLE", "types extend each other in a cycle")] : [];
1738
+ case "tooDeep": return [issue("/extends", "INHERITANCE_TOO_DEEP", `a family can have at most 8 levels of types`)];
1739
+ default: return [];
1740
+ }
1741
+ };
1742
+ const FLAGS = ["localized", "draftAndPublish"];
1743
+ const flagIssues = (definition, parent) => FLAGS.filter((flag) => definition[flag] !== parent[flag]).map((flag) => issue(`/${flag}`, "INHERITED_FLAG_MISMATCH", `must equal the parent's: set "${flag}" to ${String(parent[flag])}, as "${parent.apiKey}" has it`));
1744
+ const collisionIssues = (definition, index) => {
1745
+ const resolved = index.resolve(definition.id);
1746
+ const inherited = /* @__PURE__ */ new Map();
1747
+ for (const field of resolved?.fields ?? []) if (field.inheritedFrom !== void 0) inherited.set(foldApiKey(field.apiKey), index.get(field.inheritedFrom)?.apiKey ?? field.inheritedFrom);
1748
+ return definition.fields.flatMap((field, position) => {
1749
+ const owner = inherited.get(foldApiKey(field.apiKey));
1750
+ return owner === void 0 ? [] : [issue(`/fields/${position}/apiKey`, "INHERITED_FIELD_COLLISION", `"${field.apiKey}" is already a field of "${owner}", which this type extends (keys are case-insensitive)`)];
1751
+ });
1752
+ };
1753
+ const GROUP_PATH = /^\/display\/groups\/(\d+)(\/.*)?$/;
1754
+ /**
1755
+ * The display checked against the resolved fields. An issue counts when it is about a value the child sets
1756
+ * itself (the parent's own values were checked on the parent); group paths are moved to the child's groups.
1757
+ */
1758
+ const displayIssues = (definition, index) => {
1759
+ const resolved = index.resolve(definition.id);
1760
+ if (!resolved) return [];
1761
+ const inheritedGroups = (resolved.display.groups?.length ?? 0) - (definition.display.groups?.length ?? 0);
1762
+ const own = definition.display;
1763
+ return checkDisplay(resolved).flatMap((found) => {
1764
+ const group = GROUP_PATH.exec(found.path);
1765
+ if (group) {
1766
+ const position = Number(group[1]) - inheritedGroups;
1767
+ return position < 0 ? [] : [{
1768
+ ...found,
1769
+ path: `/display/groups/${position}${group[2] ?? ""}`
1770
+ }];
1771
+ }
1772
+ const property = found.path.split("/")[2];
1773
+ return property !== void 0 && own[property] !== void 0 ? [found] : [];
1774
+ });
1775
+ };
1776
+ /** Every inheritance problem of a schema (see the module comment). */
1777
+ const checkInheritance = (definitions, index = createDefinitionIndex(definitions)) => definitions.flatMap((definition) => {
1778
+ if (!isModelDefinition(definition) || definition.extends === void 0) return [];
1779
+ const problems = parentIssues(definition, index);
1780
+ const parent = index.get(definition.extends);
1781
+ const checks = problems.length === 0 && parent && isModelDefinition(parent) ? [
1782
+ ...flagIssues(definition, parent),
1783
+ ...collisionIssues(definition, index),
1784
+ ...displayIssues(definition, index)
1785
+ ] : [];
1786
+ return [...problems, ...checks].map((found) => withDefinition$1(definition, found));
1787
+ });
1788
+ //#endregion
1506
1789
  //#region src/validators/parse.ts
1507
1790
  const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1508
1791
  const structuralIssues = (raw) => {
@@ -1622,7 +1905,8 @@ const checkGeneratedNames = (definitions) => {
1622
1905
  const byId = new Map(definitions.map((definition) => [definition.id, definition]));
1623
1906
  const issues = [];
1624
1907
  const seen = /* @__PURE__ */ new Map();
1625
- for (const definition of definitions) for (const name of generatedNames(definition)) {
1908
+ const parents = new Set(definitions.flatMap((definition) => isModelDefinition(definition) && definition.extends !== void 0 ? [definition.extends] : []));
1909
+ for (const definition of definitions) for (const name of generatedNames(definition, { hasChildren: parents.has(definition.id) })) {
1626
1910
  const folded = `${name.namespace}:${foldApiKey(name.name)}`;
1627
1911
  const path = name.fieldId ? `/fields/${definition.fields.findIndex((field) => field.id === name.fieldId)}/apiKey` : `/${name.property ?? "apiKey"}`;
1628
1912
  if (name.namespace === "type" && RESERVED_TYPES.has(foldApiKey(name.name))) {
@@ -1712,11 +1996,12 @@ const checkNesting = (definitions) => {
1712
1996
  };
1713
1997
  /**
1714
1998
  * Cross-definition checks over a whole schema (every active definition with the proposed change applied):
1715
- * unique IDs and API keys, GraphQL name collisions, relation and component targets, component cycles and
1716
- * nesting depth. Each definition must already pass `validateDefinition`.
1999
+ * unique IDs and API keys, model inheritance, GraphQL name collisions, relation and component targets,
2000
+ * component cycles and nesting depth. Each definition must already pass `validateDefinition`.
1717
2001
  */
1718
2002
  const validateSchema = (definitions) => [
1719
2003
  ...checkIdentity(definitions),
2004
+ ...checkInheritance(definitions),
1720
2005
  ...checkGeneratedNames(definitions),
1721
2006
  ...checkReferences(definitions),
1722
2007
  ...checkNesting(definitions)
@@ -1724,12 +2009,14 @@ const validateSchema = (definitions) => [
1724
2009
  /** Definitions that reference `id` (relations, components, dynamic zones). Used to block deletions. */
1725
2010
  const findReferencingDefinitions = (definitions, id) => definitions.filter((definition) => definition.id !== id && definition.fields.some((field) => field.type === "relation" && field.settings.target === id || field.type === "component" && field.settings.component === id || field.type === "dynamiczone" && field.settings.components.includes(id)));
1726
2011
  /**
1727
- * Models that embed a component directly or through other components, i.e. whose entries change shape
1728
- * when the component does (brief §5: "editing a shared component must account for all dependent models").
2012
+ * Models that embed a component directly, through other components or through an inherited field, i.e.
2013
+ * whose entries change shape when the component does (brief §5: "editing a shared component must account for
2014
+ * all dependent models").
1729
2015
  */
1730
2016
  const findDependentModels = (definitions, componentId) => {
1731
2017
  const embeddedBy = /* @__PURE__ */ new Map();
1732
2018
  for (const definition of definitions) for (const child of embeddedComponentIds(definition)) embeddedBy.set(child, [...embeddedBy.get(child) ?? [], definition]);
2019
+ const index = createDefinitionIndex(definitions);
1733
2020
  const models = /* @__PURE__ */ new Map();
1734
2021
  const visited = /* @__PURE__ */ new Set();
1735
2022
  const queue = [componentId];
@@ -1738,7 +2025,10 @@ const findDependentModels = (definitions, componentId) => {
1738
2025
  if (visited.has(current)) continue;
1739
2026
  visited.add(current);
1740
2027
  for (const parent of embeddedBy.get(current) ?? []) if (parent.kind === "component") queue.push(parent.id);
1741
- else models.set(parent.id, parent);
2028
+ else for (const id of index.family(parent.id)) {
2029
+ const model = index.get(id);
2030
+ if (model) models.set(id, model);
2031
+ }
1742
2032
  }
1743
2033
  return [...models.values()];
1744
2034
  };
@@ -1753,6 +2043,7 @@ const CHANGE_KINDS = [
1753
2043
  "model.kind",
1754
2044
  "model.localized",
1755
2045
  "model.draftAndPublish",
2046
+ "model.extends",
1756
2047
  "field.added",
1757
2048
  "field.removed",
1758
2049
  "field.order",
@@ -1922,6 +2213,12 @@ const diffDefinitions = (before, after) => {
1922
2213
  });
1923
2214
  }
1924
2215
  if (before.kind !== "component" && after.kind !== "component") {
2216
+ if ((before.extends ?? null) !== (after.extends ?? null)) changes.push({
2217
+ kind: "model.extends",
2218
+ definitionId,
2219
+ from: before.extends ?? null,
2220
+ to: after.extends ?? null
2221
+ });
1925
2222
  if (before.localized !== after.localized) changes.push({
1926
2223
  kind: "model.localized",
1927
2224
  definitionId,
@@ -2060,8 +2357,12 @@ const convertsAcrossFamilies = (from, to) => {
2060
2357
  * that flag rather than deciding again from the type names.
2061
2358
  */
2062
2359
  const typeChangeDropsValues = (from, to) => storageFamilyOf(from) !== storageFamilyOf(to) && !convertsAcrossFamilies(from, to);
2063
- /** Whether a (classified) change drops every stored value of its field; false for plans without the flag. */
2064
- const changeDropsValues = (change) => change.kind === "field.type" && change.dropsValues === true;
2360
+ /**
2361
+ * Whether a (classified) change drops every stored value of its field; false for plans without the flag. A
2362
+ * type change Shapio cannot convert, or an inherited field a model loses when it moves to another parent
2363
+ * (`field.removed` marked by the planner, plan polymorphic-types decision 6).
2364
+ */
2365
+ const changeDropsValues = (change) => (change.kind === "field.type" || change.kind === "field.removed") && change.dropsValues === true;
2065
2366
  /** A required field whose values are dropped is backfilled from its default, else checked (no silent nulls). */
2066
2367
  const requiredStep = (field) => {
2067
2368
  if (!field?.required) return [];
@@ -2243,6 +2544,9 @@ const classifyChange = (change, context) => {
2243
2544
  });
2244
2545
  break;
2245
2546
  }
2547
+ case "model.extends":
2548
+ classification = result("contract", { breaking: true });
2549
+ break;
2246
2550
  case "model.draftAndPublish":
2247
2551
  classification = change.to === true ? result("additive") : result("conversion", {
2248
2552
  destructive: true,
@@ -2692,11 +2996,11 @@ const BUILT_IN_THEME_KEYS = [
2692
2996
  "butter"
2693
2997
  ];
2694
2998
  /**
2695
- * A token value an extension may use: a hex colour or a colour function whose arguments are numbers, units
2999
+ * A token value an extension may use: a hex color or a color function whose arguments are numbers, units
2696
3000
  * and separators only (no `;`, quotes, braces or nested functions such as `url()` or `var()`), so a value
2697
3001
  * can never escape its declaration.
2698
3002
  */
2699
- const THEME_COLOUR_PATTERN = /^(?:#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgba?|hsla?|oklch|oklab)\([0-9a-z.,%\s/+-]{1,80}\))$/;
3003
+ const THEME_COLOR_PATTERN = /^(?:#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgba?|hsla?|oklch|oklab)\([0-9a-z.,%\s/+-]{1,80}\))$/;
2700
3004
  //#endregion
2701
3005
  //#region src/themes/contrast.ts
2702
3006
  const SURFACES = [
@@ -2781,7 +3085,7 @@ const luminance = (hex) => {
2781
3085
  ].map((index) => channel(parseInt(hex.slice(index, index + 2), 16)));
2782
3086
  return .2126 * (r ?? 0) + .7152 * (g ?? 0) + .0722 * (b ?? 0);
2783
3087
  };
2784
- /** The WCAG contrast ratio of two `#rrggbb` colours. */
3088
+ /** The WCAG contrast ratio of two `#rrggbb` colors. */
2785
3089
  const contrastRatio = (a, b) => {
2786
3090
  const [high = 0, low = 0] = [luminance(a), luminance(b)].sort((x, y) => y - x);
2787
3091
  return (high + .05) / (low + .05);
@@ -2885,6 +3189,6 @@ const SeoDefaultsSchema = Type.Object({
2885
3189
  twitterHandle: Type.Union([Type.String({ pattern: SEO_TWITTER_HANDLE_PATTERN.source }), Type.Null()])
2886
3190
  }, closed);
2887
3191
  //#endregion
2888
- export { API_KEY_PATTERN, BIGINTEGER_PATTERN, BUILT_IN_THEME_KEYS, CANVAS_ELIGIBLE, CHANGE_KINDS, CODE_LANGUAGES, COMPONENT_TYPE_SUFFIXES, CUSTOM_EDITOR_ID_PATTERN, ComponentDefinitionInputSchema, ComponentDisplayInputSchema, DATA_TYPES, DATETIME_PATTERN, DATE_PATTERN, DECIMAL_PATTERN, DEFAULT_EDITORS, DEFAULT_SEO_KEYS, DEFINITION_KINDS, DefinitionInputSchema, EDITOR_CATALOGUE, EMPTY_SEO_DEFAULTS, ENTRY_LAYOUTS, EditorChoiceInputSchema, EnumValueSchema, FIELD_WIDTHS, FieldInputSchema, INLINE_TITLE_TYPES, INVALID_TEXT_MESSAGE, LOCALE_CODE_PATTERN, LOCK_FILE_FORMAT_VERSION, LOCK_FILE_PATH, LocaleInputSchema, LockEntrySchema, LockFileError, LockFileSchema, MARK_RULES, MAX_API_KEY_LENGTH, MAX_COMPONENT_DEPTH, MAX_DESCRIPTION_LENGTH, MAX_FALLBACK_CHAIN, MAX_FIELDS_PER_DEFINITION, MAX_LABEL_LENGTH, MAX_RICHTEXT_DEPTH, MEDIA_KINDS, MODEL_KINDS, MODEL_TYPE_SUFFIXES, MUTATION_PREFIXES, ModelDefinitionInputSchema, ModelDisplayInputSchema, NODE_RULES, PLURAL_FALLBACK_SUFFIX, REFERENCE_DATA_TYPES, RELATION_CARDINALITIES, RESERVED_DEFINITION_API_KEYS, RESERVED_FIELD_API_KEYS, RESERVED_QUERY_NAMES, RESERVED_TYPE_NAMES, RICHTEXT_FORMAT, RICHTEXT_FORMAT_VERSION, SAFE_LINK_PATTERN, SCALAR_DATA_TYPES, SCHEMA_KIND_DIRECTORIES, SEO_COMPONENT_API_KEY, SEO_COMPONENT_ID, SEO_DESCRIPTION_SOFT_MAX, SEO_EDITOR_ID, SEO_FIELD_IDS, SEO_TITLE_PLACEHOLDER, SEO_TITLE_SOFT_MAX, SEO_TITLE_TEMPLATE_PATTERN, SEO_TWITTER_HANDLE_PATTERN, SETTINGS_SCHEMAS, SITES_DIRECTORY, SLUG_PATTERN, SORT_DIRECTIONS, STABLE_ID_PATTERN, STORAGE_FAMILY, STRIP_DEFAULT_COUNT, SchemaValidationError, SeoDefaultsSchema, SeoLocaleDefaultsSchema, TEXT_TITLE_FIELD_TYPES, THEME_BRAND_TOKENS, THEME_COLOUR_PATTERN, THEME_DARK_UI_PAIRS, THEME_KEY_PATTERN, THEME_SEMANTIC_TOKENS, THEME_TEXT_PAIRS, THEME_TOKENS, THEME_UI_PAIRS, THEME_VARIANTS, TIME_PATTERN, TITLE_FIELD_TYPES, UID_PATTERN, UNIQUE_CAPABLE_DATA_TYPES, applyTitleTemplate, assignIds, canonicalJson, changeDropsValues, checkApiKeySyntax, checkDefaultValue, checkThemeContrast, classifyChange, classifyChanges, classifyLocaleChange, collectionQueryName, contrastRatio, createStableId, decideSync, diffDefinitions, effectiveFormLayout, effectiveLayout, effectiveTitleField, embeddedComponentIds, entryLayoutOf, escapeHtml, findDependentModels, findInvalidText, findReferencingDefinitions, foldApiKey, generatedNames, hashDefinition, isApiKeySyntaxValid, isCanvasEligible, isComponentDefinition, isCoverEligible, isCustomEditorId, isDataType, isDocumentPlaceable, isEmptyRichText, isModelDefinition, isSeoField, isStableId, isValidText, isValidTitleTemplate, issue, listCompatibleEditors, lockCoversSite, lockEntriesForSite, mergePulledLock, normalizeDefinition, parseDefinition, parseLockFile, plainTextToRichText, renderRichTextHtml, renormalizeDefinition, resolveSeo, richTextBodyOf, richTextMediaIds, richTextPlainText, richTextToPlainText, routeKeyOf, schemaFilePath, scopeDirectory, scopeOfSchemaFilePath, seoComponentDefinition, seoDefaultsForLocale, serializeDefinition, serializeLockFile, sha256, storageFamilyOf, stripFieldsOf, suggestPlural, summarizeChanges, toTypeName, typeChangeDropsValues, uniqueFieldsChangingLocalization, upgradeLockFile, validateDefinition, validateRichText, validateSchema, withDerivedPlural, withSiteCovered };
3192
+ export { API_KEY_PATTERN, BIGINTEGER_PATTERN, BUILT_IN_THEME_KEYS, CANVAS_ELIGIBLE, CHANGE_KINDS, CODE_LANGUAGES, COMPONENT_TYPE_SUFFIXES, CUSTOM_EDITOR_ID_PATTERN, ComponentDefinitionInputSchema, ComponentDisplayInputSchema, DATA_TYPES, DATETIME_PATTERN, DATE_PATTERN, DECIMAL_PATTERN, DEFAULT_EDITORS, DEFAULT_SEO_KEYS, DEFINITION_KINDS, DefinitionInputSchema, EDITOR_CATALOG, EMPTY_SEO_DEFAULTS, ENTRY_LAYOUTS, EditorChoiceInputSchema, EnumValueSchema, FIELD_WIDTHS, FieldInputSchema, INLINE_TITLE_TYPES, INTERFACE_TYPE_SUFFIX, INVALID_TEXT_MESSAGE, LOCALE_CODE_PATTERN, LOCK_FILE_FORMAT_VERSION, LOCK_FILE_PATH, LocaleInputSchema, LockEntrySchema, LockFileError, LockFileSchema, MARK_RULES, MAX_API_KEY_LENGTH, MAX_COMPONENT_DEPTH, MAX_DESCRIPTION_LENGTH, MAX_FALLBACK_CHAIN, MAX_FIELDS_PER_DEFINITION, MAX_INHERITANCE_DEPTH, MAX_LABEL_LENGTH, MAX_RICHTEXT_DEPTH, MAX_ROUTE_KEY_LENGTH, MEDIA_KINDS, MODEL_KINDS, MODEL_TYPE_SUFFIXES, MUTATION_PREFIXES, ModelDefinitionInputSchema, ModelDisplayInputSchema, NODE_RULES, PLURAL_FALLBACK_SUFFIX, REFERENCE_DATA_TYPES, RELATION_CARDINALITIES, RESERVED_DEFINITION_API_KEYS, RESERVED_FIELD_API_KEYS, RESERVED_QUERY_NAMES, RESERVED_TYPE_NAMES, RICHTEXT_FORMAT, RICHTEXT_FORMAT_VERSION, ROUTE_KEY_PATTERN, SAFE_LINK_PATTERN, SCALAR_DATA_TYPES, SCHEMA_KIND_DIRECTORIES, SEO_COMPONENT_API_KEY, SEO_COMPONENT_ID, SEO_DESCRIPTION_SOFT_MAX, SEO_EDITOR_ID, SEO_FIELD_IDS, SEO_TITLE_PLACEHOLDER, SEO_TITLE_SOFT_MAX, SEO_TITLE_TEMPLATE_PATTERN, SEO_TWITTER_HANDLE_PATTERN, SETTINGS_SCHEMAS, SITES_DIRECTORY, SLUG_PATTERN, SORT_DIRECTIONS, STABLE_ID_PATTERN, STORAGE_FAMILY, STRIP_DEFAULT_COUNT, SchemaValidationError, SeoDefaultsSchema, SeoLocaleDefaultsSchema, TEXT_TITLE_FIELD_TYPES, THEME_BRAND_TOKENS, THEME_COLOR_PATTERN, THEME_DARK_UI_PAIRS, THEME_KEY_PATTERN, THEME_SEMANTIC_TOKENS, THEME_TEXT_PAIRS, THEME_TOKENS, THEME_UI_PAIRS, THEME_VARIANTS, TIME_PATTERN, TITLE_FIELD_TYPES, UID_PATTERN, UNIQUE_CAPABLE_DATA_TYPES, ancestorChain, applyTitleTemplate, assignIds, canonicalJson, changeDropsValues, checkApiKeySyntax, checkDefaultValue, checkDisplay, checkInheritance, checkThemeContrast, classifyChange, classifyChanges, classifyLocaleChange, collectionQueryName, contrastRatio, createDefinitionIndex, createStableId, decideSync, diffDefinitions, effectiveFormLayout, effectiveLayout, effectiveTitleField, embeddedComponentIds, entryLayoutOf, escapeHtml, findChildDefinitions, findDependentModels, findInvalidText, findReferencingDefinitions, foldApiKey, generatedNames, hashDefinition, isApiKeySyntaxValid, isCanvasEligible, isComponentDefinition, isCoverEligible, isCustomEditorId, isDataType, isDocumentPlaceable, isEmptyRichText, isModelDefinition, isSeoField, isStableId, isValidText, isValidTitleTemplate, issue, listCompatibleEditors, lockCoversSite, lockEntriesForSite, mergeDisplay, mergePulledLock, normalizeDefinition, ownFields, parseDefinition, parseLockFile, plainTextToRichText, renderRichTextHtml, renormalizeDefinition, resolveModelDefinition, resolveSeo, richTextBodyOf, richTextMediaIds, richTextPlainText, richTextToPlainText, routeKeyOf, routePathOf, schemaFilePath, scopeDirectory, scopeOfSchemaFilePath, seoComponentDefinition, seoDefaultsForLocale, serializeDefinition, serializeLockFile, sha256, splitRoutePath, storageFamilyOf, stripFieldsOf, suggestPlural, summarizeChanges, toTypeName, typeChangeDropsValues, uniqueFieldsChangingLocalization, upgradeLockFile, validateDefinition, validateRichText, validateSchema, withDerivedPlural, withSiteCovered };
2889
3193
 
2890
3194
  //# sourceMappingURL=index.js.map