@nonext/husk 0.2.3
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/CHANGELOG.md +148 -0
- package/LICENSE +166 -0
- package/README.md +140 -0
- package/api/public-api.md +1070 -0
- package/dist/adapter-C_0mhENy.d.ts +320 -0
- package/dist/admin/admin.css +5 -0
- package/dist/admin/index.d.ts +265 -0
- package/dist/admin/index.js +393 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/tokens.css +597 -0
- package/dist/admin/ui.d.ts +3013 -0
- package/dist/admin/ui.js +9783 -0
- package/dist/admin/ui.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.js +9 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth-B2gBtHl5.d.ts +28 -0
- package/dist/can-Chy7zmx7.d.ts +69 -0
- package/dist/chunk-3I6337WJ.js +83 -0
- package/dist/chunk-3I6337WJ.js.map +1 -0
- package/dist/chunk-4Z6P3XCE.js +62 -0
- package/dist/chunk-4Z6P3XCE.js.map +1 -0
- package/dist/chunk-6E3ZS5Z5.js +1007 -0
- package/dist/chunk-6E3ZS5Z5.js.map +1 -0
- package/dist/chunk-77JDRXXU.js +3 -0
- package/dist/chunk-77JDRXXU.js.map +1 -0
- package/dist/chunk-FHYWJCG6.js +171 -0
- package/dist/chunk-FHYWJCG6.js.map +1 -0
- package/dist/chunk-JAE7HHMM.js +506 -0
- package/dist/chunk-JAE7HHMM.js.map +1 -0
- package/dist/chunk-NPDBQBLW.js +68 -0
- package/dist/chunk-NPDBQBLW.js.map +1 -0
- package/dist/chunk-ORMYARXD.js +6 -0
- package/dist/chunk-ORMYARXD.js.map +1 -0
- package/dist/chunk-R4O7APIX.js +51 -0
- package/dist/chunk-R4O7APIX.js.map +1 -0
- package/dist/chunk-ROLKRJWI.js +187 -0
- package/dist/chunk-ROLKRJWI.js.map +1 -0
- package/dist/chunk-SOTQ3WP5.js +31 -0
- package/dist/chunk-SOTQ3WP5.js.map +1 -0
- package/dist/chunk-TCFFHTY3.js +97 -0
- package/dist/chunk-TCFFHTY3.js.map +1 -0
- package/dist/chunk-TS7Y7GEZ.js +7 -0
- package/dist/chunk-TS7Y7GEZ.js.map +1 -0
- package/dist/chunk-VLGR5PZX.js +3 -0
- package/dist/chunk-VLGR5PZX.js.map +1 -0
- package/dist/chunk-ZW2PUSVS.js +40 -0
- package/dist/chunk-ZW2PUSVS.js.map +1 -0
- package/dist/chunk-ZYNBMYJ6.js +392 -0
- package/dist/chunk-ZYNBMYJ6.js.map +1 -0
- package/dist/cli/nonext-husk.js +3360 -0
- package/dist/cli/nonext-husk.js.map +1 -0
- package/dist/cli/templates/firestore.rules +393 -0
- package/dist/cli/templates/storage.rules +165 -0
- package/dist/client/index.d.ts +27 -0
- package/dist/client/index.js +15 -0
- package/dist/client/index.js.map +1 -0
- package/dist/cms-9fSA3uQV.d.ts +158 -0
- package/dist/core/index.d.ts +1267 -0
- package/dist/core/index.js +12 -0
- package/dist/core/index.js.map +1 -0
- package/dist/entry-D3kiE8T1.d.ts +163 -0
- package/dist/field-Diyf5siF.d.ts +158 -0
- package/dist/field-type-D25Vojb0.d.ts +157 -0
- package/dist/firebase/index.d.ts +251 -0
- package/dist/firebase/index.js +991 -0
- package/dist/firebase/index.js.map +1 -0
- package/dist/index-5ylaMBUH.d.ts +155 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/permissions/index.d.ts +101 -0
- package/dist/permissions/index.js +7 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/registry-CUC25qay.d.ts +59 -0
- package/dist/richtext/index.d.ts +85 -0
- package/dist/richtext/index.js +127 -0
- package/dist/richtext/index.js.map +1 -0
- package/dist/schema/index.d.ts +50 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/server/index.d.ts +39 -0
- package/dist/server/index.js +25 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/write-limits-Djmm6L3R.d.ts +215 -0
- package/package.json +136 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/cache-tags.ts","../src/core/entry-title.ts","../src/core/delete-confirmation.ts","../src/core/reorder.ts","../src/core/schema-editor.ts","../src/core/entry-form.ts","../src/core/entry-status.ts","../src/core/entry-unique.ts","../src/core/error-taxonomy.ts","../src/core/list-view.ts","../src/core/indexes.ts","../src/core/registry-store.ts","../src/core/migrations.ts"],"names":[],"mappings":"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","file":"chunk-6E3ZS5Z5.js","sourcesContent":["/**\n * The cache tag vocabulary, as plain strings.\n *\n * A cached read and the thing that invalidates it have to agree on a name, and they are\n * almost never the same file: one is a page in the customer's app, the other is a route\n * handler, a script, or a person typing into a terminal. Two hand-written strings that must\n * match is exactly the kind of agreement that rots, so the names are computed here and both\n * sides import the same function.\n *\n * Nothing in this module imports Next. The functions are pure, which is what lets the same\n * names be used with `cacheTag` under Cache Components, with `unstable_cache({ tags })`\n * without it, with a CDN's surrogate keys, or with nothing at all. Husk does not decide the\n * caching strategy for a customer's website; it only makes the names available.\n *\n * What Husk cannot do with them is call `revalidateTag` by itself. The admin writes from the\n * browser through the client SDK, so there is no server-side write hook in this architecture\n * to hang an invalidation on. See `docs/caching.md` and\n * `.docs/decisions/2026-09-19-caching-and-revalidation.md` for the seam and what closes it.\n */\n\n/**\n * The namespace every Husk tag starts with.\n *\n * A customer's own cache tags share the tag space with these, so they carry a prefix that\n * says where they came from. It also means a customer can reason about \"everything Husk\"\n * when reading a list of tags in a log.\n */\nexport const HUSK_TAG_PREFIX = \"husk\"\n\n/**\n * The ceiling Next puts on a cache tag.\n *\n * Checked rather than trusted, because the failure is silent in the worst way: a tag that is\n * rejected or truncated still lets the cached read succeed, and the invalidation that was\n * supposed to match it simply never fires. Stale content with no error is harder to find\n * than a throw at the point the name is built.\n */\nconst MAX_TAG_LENGTH = 256\n\nfunction part(kind: string, value: string): string {\n if (value === \"\") {\n throw new Error(`A cache tag needs ${kind}. An empty one would match everything.`)\n }\n // A tag is read back by splitting on \":\", so a segment carrying one would parse as two.\n if (value.includes(\":\")) {\n throw new Error(\n `The ${kind} in a cache tag may not contain \":\", because that is the separator. Received \"${value}\".`,\n )\n }\n return value\n}\n\nfunction tag(segments: readonly string[]): string {\n const built = [HUSK_TAG_PREFIX, ...segments].join(\":\")\n if (built.length > MAX_TAG_LENGTH) {\n throw new Error(\n `The cache tag \"${built}\" is ${String(built.length)} characters, over the ${String(MAX_TAG_LENGTH)} character limit. A tag that is rejected invalidates nothing, silently.`,\n )\n }\n return built\n}\n\n/**\n * Everything of one content type: the list, the counts, every entry of it.\n *\n * This is the tag a list page uses. It is also the one an editor's publish has to clear,\n * because a new entry has no entry tag anything could have cached under yet.\n */\nexport function contentTypeTag(slug: string): string {\n return tag([\"type\", part(\"a content type slug\", slug)])\n}\n\n/**\n * One entry.\n *\n * Narrow on purpose: editing one project should not have to discard the cached page of\n * every other project. A detail page tags both this and the content type, so either level\n * of invalidation reaches it.\n */\nexport function entryTag(slug: string, entryId: string): string {\n return tag([\"entry\", part(\"a content type slug\", slug), part(\"an entry id\", entryId)])\n}\n\n/**\n * One entry, named by the value of its slug field rather than by its id.\n *\n * A URL-addressed page knows the slug and does not know the id until it has read the entry,\n * which is one read too late to tag a cache entry with. The publish side knows both, so the\n * two meet: a page caches under the slug it was asked for, and an invalidation clears the\n * slug the entry carries. Renaming a slug moves the page to a new URL and leaves the old\n * tag holding a page nothing links to any more, which expires on its own.\n */\nexport function entrySlugTag(slug: string, value: string): string {\n return tag([\"slug\", part(\"a content type slug\", slug), part(\"a slug value\", value)])\n}\n\n/** The pair a detail page caches under. Order is stable, so it is comparable. */\nexport function entryTags(slug: string, entryId: string): readonly string[] {\n return [contentTypeTag(slug), entryTag(slug, entryId)]\n}\n\n/**\n * One media item.\n *\n * Replacing a file keeps the media id and mints a new download URL, so a page that cached\n * the old URL is stale in a way no entry-level tag describes.\n */\nexport function mediaTag(mediaId: string): string {\n return tag([\"media\", part(\"a media id\", mediaId)])\n}\n\n/** One settings document, by key. */\nexport function settingsTag(key: string): string {\n return tag([\"settings\", part(\"a settings key\", key)])\n}\n\n/** Whether a string is one of the names above, for a revalidation endpoint to check. */\nexport function isHuskTag(value: string): boolean {\n return value.startsWith(`${HUSK_TAG_PREFIX}:`) && value.length <= MAX_TAG_LENGTH\n}\n","import type { ContentTypeLike } from \"../types/content-type.js\"\nimport type { Entry } from \"../types/entry.js\"\n\n/** What an entry is called when its title field is empty or was never declared. */\nexport const UNTITLED_ENTRY = \"Untitled\"\n\n/**\n * The one-line name of an entry, resolved from the content type rather than guessed.\n *\n * Every screen that lists entries needs this: the dashboard, the collection list, a\n * relation picker, a delete confirmation. Resolving it in one place is what keeps an entry\n * from being called two different things on two screens.\n *\n * `titleField` is optional and its stored value is untrusted, so a missing field, a\n * non-string value and a blank string all fall back to `UNTITLED_ENTRY`. An entry with no\n * usable title still has to be nameable, or it cannot be listed at all.\n */\nexport function entryTitle(type: ContentTypeLike, entry: Entry): string {\n const key = type.titleField\n if (key === undefined) return UNTITLED_ENTRY\n\n const value = (entry as Record<string, unknown>)[key]\n if (typeof value !== \"string\") return UNTITLED_ENTRY\n\n const trimmed = value.trim()\n return trimmed === \"\" ? UNTITLED_ENTRY : trimmed\n}\n","import type { ContentTypeLike } from \"../types/content-type.js\"\nimport type { Entry } from \"../types/entry.js\"\n\nimport { entryTitle, UNTITLED_ENTRY } from \"./entry-title.js\"\n\n/**\n * The phrase a destructive confirmation asks the user to type, and what counts as typing\n * it. Canonical source: .docs/styleguide/04-components.md section 12, \"Destructive\n * confirmation\".\n *\n * Two failure modes bound this, and they pull in opposite directions. A phrase the user\n * cannot produce is a trap: the delete becomes impossible and the only way out is the\n * cancel button. A phrase any keypress satisfies is theatre: it costs a gesture and buys\n * no attention. So the phrase is always printed in the dialog, always typeable, and always\n * specific to what is about to be destroyed.\n *\n * None of it renders, so all of it is testable without a DOM, and the dialog and any\n * future bulk flow cannot disagree about what \"the name\" is.\n */\n\n/**\n * What to type to delete one entry.\n *\n * The entry's title, when it has one. When it does not (no `titleField`, a non-string\n * value, or a blank string) `entryTitle` answers \"Untitled\", and \"Untitled\" is a\n * placeholder rather than a name: it is the same word for every untitled entry in the\n * collection, so typing it proves nothing about which entry is meant. The document id is\n * used instead. It is ugly and it is unique, and the dialog prints it, so it is typeable.\n *\n * Two entries can still legitimately share a title. That ambiguity is harmless here,\n * because the dialog is already pointed at one entry: the phrase is a gate on attention,\n * not a lookup key.\n */\nexport function deleteConfirmationName(type: ContentTypeLike, entry: Entry): string {\n const title = entryTitle(type, entry)\n return title === UNTITLED_ENTRY ? entry.id : title\n}\n\n/**\n * What to type to delete several entries at once.\n *\n * Not a list of names: ten names is a transcription exercise, and an editor would paste it\n * or give up. The count and the content type is the fact worth confirming, because the\n * mistake a bulk delete actually makes is about how many rows were selected.\n */\nexport function deleteConfirmationPhrase(type: ContentTypeLike, count: number): string {\n const noun = count === 1 ? type.label : (type.labelPlural ?? `${type.label}s`)\n return `${String(count)} ${noun.toLowerCase()}`\n}\n\n/**\n * The comparison form of a confirmation phrase.\n *\n * Forgiving of whitespace: leading and trailing space, and any run of spaces, tabs or\n * newlines inside, collapse to one space. None of that is visible in the input, so\n * refusing it would fail a user who typed the phrase correctly and cannot see why it was\n * rejected. Unicode is normalized to NFC for the same reason: an accented character typed\n * on one keyboard and stored from another can be two byte sequences that read identically.\n *\n * Deliberately not forgiving of case. Case is visible, the phrase is printed right above\n * the input, and matching it is the deliberate act the gate exists to require. This is the\n * same choice GitHub makes for deleting a repository, and it is the line between a\n * confirmation and a formality.\n */\nexport function normalizeConfirmation(value: string): string {\n return value.normalize(\"NFC\").replace(/\\s+/gu, \" \").trim()\n}\n\n/**\n * Whether what was typed confirms the phrase.\n *\n * An empty expected phrase never matches. A caller that cannot name what it is about to\n * delete has no business unlocking the button, and an empty input would otherwise satisfy\n * an empty phrase on an empty dialog.\n */\nexport function confirmationMatches(expected: string, typed: string): boolean {\n const target = normalizeConfirmation(expected)\n if (target === \"\") return false\n return normalizeConfirmation(typed) === target\n}\n","/**\n * Moving one item of a list, as a pure function.\n *\n * It lives in core rather than beside either of its callers because both of them reorder\n * for the same reason and must behave identically: the repeater field moves a row, and the\n * Schema Builder moves a field. A drag and an arrow key are two ways to call this, never\n * two implementations of it.\n */\n\n/**\n * Returns a new list with the item at `from` placed at `to`.\n *\n * An out-of-range index returns the list unchanged rather than throwing. Every caller is an\n * event handler (the last row pressing ArrowDown, a drop outside the list), and none of\n * them has anything useful to do with an exception.\n */\nexport function moveItem<TItem>(\n items: readonly TItem[],\n from: number,\n to: number,\n): TItem[] {\n const next = [...items]\n if (from === to) return next\n if (from < 0 || from >= next.length || to < 0 || to >= next.length) return next\n\n const [moved] = next.splice(from, 1)\n if (moved === undefined) return [...items]\n next.splice(to, 0, moved)\n return next\n}\n","import type { FieldTypeRegistry } from \"../schema/field-type.js\"\nimport { RESERVED_FIELD_KEYS } from \"../types/content-type.js\"\nimport type { ContentTypeLike } from \"../types/content-type.js\"\nimport type {\n FieldDefinition,\n FieldMap,\n FieldType,\n ValidationRules,\n} from \"../types/field.js\"\n\nimport { assertValidContentType } from \"./define-content-type.js\"\nimport { SchemaError } from \"./errors.js\"\nimport { moveItem } from \"./reorder.js\"\nimport { fieldKeyState } from \"./schema-fields.js\"\n\n/**\n * The field editor as data: the order fields are shown in, what a new field starts as,\n * whether a key may be used, and which options are still unanswered.\n *\n * Nothing here renders and nothing here writes. The Schema Builder screen calls it to\n * decide what to show and what to enable, and the same functions are callable from a test\n * or a script without a DOM.\n *\n * None of it switches on a field type. The per-type answers come from each field type's own\n * `editor` spec through the registry, which is why adding a thirteenth field type adds it\n * to the Schema Builder with no edit in this file and none in any screen.\n */\n\nexport interface FieldRow {\n key: string\n field: FieldDefinition\n}\n\n/**\n * The field keys of a content type, in display order.\n *\n * `fieldOrder` leads, then any field it does not mention, in map order. Both halves are\n * needed: a stored type's map order is whatever came back from storage, which is not a\n * guarantee to build on, and a `fieldOrder` written before a field was added does not\n * mention that field. An unknown key in `fieldOrder` is skipped rather than trusted, so a\n * stale entry cannot conjure a field.\n */\nexport function orderedFieldKeys(\n type: Pick<ContentTypeLike, \"fields\" | \"fieldOrder\">,\n): readonly string[] {\n const declared: readonly unknown[] = Array.isArray(type.fieldOrder)\n ? type.fieldOrder\n : []\n const ordered: string[] = []\n\n for (const key of declared) {\n if (typeof key === \"string\" && key in type.fields && !ordered.includes(key)) {\n ordered.push(key)\n }\n }\n\n for (const key of Object.keys(type.fields)) {\n if (!ordered.includes(key)) ordered.push(key)\n }\n\n return ordered\n}\n\n/** The fields of a content type as an ordered list, which is what a form renders. */\nexport function orderedFields(\n type: Pick<ContentTypeLike, \"fields\" | \"fieldOrder\">,\n): readonly FieldRow[] {\n const rows: FieldRow[] = []\n\n for (const key of orderedFieldKeys(type)) {\n const field = type.fields[key]\n if (field !== undefined) rows.push({ key, field })\n }\n\n return rows\n}\n\n/** Moves one field. An out-of-range index is a no-op; see `moveItem`. */\nexport function moveFieldRow(\n rows: readonly FieldRow[],\n from: number,\n to: number,\n): FieldRow[] {\n return moveItem(rows, from, to)\n}\n\n/**\n * Rebuilds a content type from an ordered list of fields.\n *\n * Writes both halves: the map, because that is what every consumer reads, and `fieldOrder`,\n * because a map's key order carries no promise across a round trip. Validated here, so the\n * editor fails on the form rather than on the round trip.\n */\nexport function withFieldRows(\n type: ContentTypeLike,\n rows: readonly FieldRow[],\n): ContentTypeLike {\n const seen = new Set<string>()\n const fields: { [key: string]: FieldDefinition } = {}\n\n for (const row of rows) {\n if (seen.has(row.key)) {\n throw new SchemaError(\n `Content type \"${type.slug}\" would have two fields called \"${row.key}\".`,\n )\n }\n seen.add(row.key)\n fields[row.key] = row.field\n }\n\n const next: ContentTypeLike = {\n ...type,\n fields,\n fieldOrder: rows.map((row) => row.key),\n }\n\n assertValidContentType(next)\n return next\n}\n\n/**\n * Deep equality for two stored definitions, independent of key order.\n *\n * `JSON.stringify` is not enough on its own, and the reason is the same one `fieldOrder`\n * exists for: a field map is an object, the editor rebuilds it in display order, and the\n * stored copy is in whatever order it came back in. Comparing the two literally would\n * report a difference after a load that reordered nothing, and leave the Save button\n * enabled on a draft nobody has touched.\n *\n * Arrays keep their order, which is what makes a reorder, an option list and a\n * `fieldOrder` change all register as real differences.\n */\nexport function sameDefinition(left: ContentTypeLike, right: ContentTypeLike): boolean {\n return canonical(left) === canonical(right)\n}\n\nfunction canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(\",\")}]`\n\n if (typeof value === \"object\" && value !== null) {\n return `{${Object.entries(value)\n .filter(([, member]) => member !== undefined)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, member]) => `${JSON.stringify(key)}:${canonical(member)}`)\n .join(\",\")}}`\n }\n\n return JSON.stringify(value) ?? \"null\"\n}\n\n/** A field key becomes a TypeScript property and a Firestore field name. */\nconst FIELD_KEY_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/\n\nexport type FieldKeyRejection = \"invalid\" | \"reserved\" | \"live\" | \"removed\"\n\nexport interface FieldKeyAvailability {\n available: boolean\n reason?: FieldKeyRejection\n /** Ready to print, and it names what is in the way. */\n message?: string\n}\n\nconst KEY_AVAILABLE: FieldKeyAvailability = { available: true }\n\n/**\n * May this content type take this field key?\n *\n * The removed case is the one that matters. A key whose values are still stored is not\n * free, and saying only \"already used\" would send an editor looking for a field that is not\n * on the screen; the message points at the restore instead.\n */\nexport function checkFieldKey(type: ContentTypeLike, key: string): FieldKeyAvailability {\n if (!FIELD_KEY_PATTERN.test(key)) {\n return {\n available: false,\n reason: \"invalid\",\n message: `\"${key}\" is not a valid field key. Use letters, digits and underscores, starting with a letter, for example \"jobTitle\".`,\n }\n }\n\n if ((RESERVED_FIELD_KEYS as readonly string[]).includes(key)) {\n return {\n available: false,\n reason: \"reserved\",\n message: `\"${key}\" is reserved for entry metadata. Reserved keys: ${RESERVED_FIELD_KEYS.join(\", \")}.`,\n }\n }\n\n const state = fieldKeyState(type, key)\n\n if (state === \"live\") {\n return {\n available: false,\n reason: \"live\",\n message: `\"${key}\" is already a field of this content type.`,\n }\n }\n\n if (state === \"removed\") {\n return {\n available: false,\n reason: \"removed\",\n message: `\"${key}\" was removed and its values are still stored. Restore it to get those values back, or purge them first.`,\n }\n }\n\n return KEY_AVAILABLE\n}\n\n/**\n * The same question one level down, inside a repeater's nested map.\n *\n * Reserved keys are not checked here, and deliberately so: `status` is entry metadata at\n * the top level and an ordinary key inside a row, and the validator agrees. Nothing is\n * parked at this level either, because `removedFields` describes top-level keys only.\n */\nexport function checkNestedFieldKey(fields: FieldMap, key: string): FieldKeyAvailability {\n if (!FIELD_KEY_PATTERN.test(key)) {\n return {\n available: false,\n reason: \"invalid\",\n message: `\"${key}\" is not a valid field key. Use letters, digits and underscores, starting with a letter.`,\n }\n }\n\n return key in fields\n ? { available: false, reason: \"live\", message: `\"${key}\" is already a field here.` }\n : KEY_AVAILABLE\n}\n\n/**\n * A field key suggested from a label, so an editor types \"Job title\" and gets \"jobTitle\".\n *\n * A suggestion only: the editor can overwrite it, and `checkFieldKey` answers whether what\n * ends up in the box is usable. A label with nothing usable in it yields an empty string\n * rather than a made-up key, and the form then asks for one.\n */\nexport function suggestFieldKey(label: string): string {\n const words = label\n .normalize(\"NFKD\")\n .replace(/[^a-zA-Z0-9]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter((word) => word.length > 0)\n\n const [first, ...rest] = words\n if (first === undefined) return \"\"\n\n const key =\n first.toLowerCase() +\n rest\n .map((word) => `${(word[0] ?? \"\").toUpperCase()}${word.slice(1).toLowerCase()}`)\n .join(\"\")\n\n // A key may not start with a digit, and prefixing rather than dropping keeps \"2024 note\"\n // distinguishable from \"note\".\n return /^[0-9]/.test(key) ? `f${key}` : key\n}\n\n/**\n * What a newly added field of this type starts as.\n *\n * The per-type part comes from the field type's own `editor.defaults`, so a select starts\n * with one option and a repeater with one nested field without this function knowing that\n * either of them exists. An unregistered type is refused rather than guessed at.\n */\nexport function newFieldDefinition(\n registry: FieldTypeRegistry,\n type: FieldType,\n label: string,\n): FieldDefinition {\n const spec = registry.require(type)\n return { ...spec.editor.defaults, type, label } as FieldDefinition\n}\n\nexport interface FieldOptionIssue {\n /** The option key the issue belongs to, so the control can show it. */\n option: string\n message: string\n}\n\n/** Is this option value empty, whatever shape the option happens to have? */\nfunction isBlank(value: unknown): boolean {\n if (value === undefined || value === null) return true\n if (typeof value === \"string\") return value.trim() === \"\"\n if (Array.isArray(value)) return value.length === 0\n if (typeof value === \"object\") return Object.keys(value).length === 0\n return false\n}\n\n/**\n * The options of one field that are still unanswered, named by option.\n *\n * Generic: a descriptor marked `required` whose value is empty is an issue, and that is the\n * whole rule. A relation with no target and a select with no options are both reported by\n * it, and a field type added later gets the same treatment from its own descriptors.\n */\nexport function fieldOptionIssues(\n registry: FieldTypeRegistry,\n field: FieldDefinition,\n): readonly FieldOptionIssue[] {\n const spec = registry.get(field.type)\n if (spec === undefined) return []\n\n const values = field as unknown as Record<string, unknown>\n\n return spec.editor.options\n .filter((option) => option.required === true && isBlank(values[option.key]))\n .map((option) => ({\n option: option.key,\n message: `${option.label} is required for a ${field.type} field.`,\n }))\n}\n\n/** Every unanswered option across a whole draft, each carrying the field it belongs to. */\nexport function draftIssues(\n registry: FieldTypeRegistry,\n rows: readonly FieldRow[],\n): readonly (FieldOptionIssue & { key: string })[] {\n return rows.flatMap((row) =>\n fieldOptionIssues(registry, row.field).map((issue) => ({ key: row.key, ...issue })),\n )\n}\n\nexport type ValidationRuleName = keyof ValidationRules\n\nexport interface ValidationRuleControl {\n label: string\n control: \"boolean\" | \"number\" | \"text\"\n help: string\n}\n\n/**\n * How each shared validation rule is edited, and what it means.\n *\n * Keyed by the rule rather than by the field type: `maxLength` is the same rule and the\n * same control wherever it is offered. Which of them a given field type offers is that\n * field type's own answer, on `editor.rules`.\n */\nexport const VALIDATION_RULE_CONTROLS: {\n readonly [TRule in ValidationRuleName]: ValidationRuleControl\n} = {\n required: {\n label: \"Required\",\n control: \"boolean\",\n help: \"An entry cannot be saved while this field is empty.\",\n },\n minLength: {\n label: \"Minimum length\",\n control: \"number\",\n help: \"Fewest characters allowed.\",\n },\n maxLength: {\n label: \"Maximum length\",\n control: \"number\",\n help: \"Most characters allowed.\",\n },\n min: { label: \"Minimum\", control: \"number\", help: \"Smallest value allowed.\" },\n max: { label: \"Maximum\", control: \"number\", help: \"Largest value allowed.\" },\n pattern: {\n label: \"Pattern\",\n control: \"text\",\n help: \"A regular expression the value must match.\",\n },\n unique: {\n label: \"Unique\",\n control: \"boolean\",\n help: \"No other entry of this content type may hold the same value.\",\n },\n}\n\n/** Sets or clears one rule, dropping `validation` entirely once nothing is left in it. */\nexport function withValidationRule(\n field: FieldDefinition,\n rule: ValidationRuleName,\n value: string | number | boolean | undefined,\n): FieldDefinition {\n const { validation, ...rest } = field\n const next: { [key: string]: unknown } = { ...validation }\n\n // An off switch and an empty box both mean \"no rule\". Storing `false` or `\"\"` would\n // leave a rule in the document that reads as configured and does nothing.\n if (value === undefined || value === \"\" || value === false) {\n delete next[rule]\n } else {\n next[rule] = value\n }\n\n return (Object.keys(next).length === 0\n ? rest\n : { ...rest, validation: next }) as unknown as FieldDefinition\n}\n\n/** Sets or clears one per-type option, dropping the key when the value is empty. */\nexport function withFieldOption(\n field: FieldDefinition,\n option: string,\n value: unknown,\n): FieldDefinition {\n const next: { [key: string]: unknown } = { ...field }\n\n if (value === undefined || value === \"\") {\n delete next[option]\n } else {\n next[option] = value\n }\n\n return next as unknown as FieldDefinition\n}\n","import type { FieldTypeRegistry } from \"../schema/field-type.js\"\nimport type { ContentTypeLike } from \"../types/content-type.js\"\nimport type { Entry, EntryMeta, FieldError } from \"../types/entry.js\"\nimport type { FieldDefinition } from \"../types/field.js\"\n\nimport { orderedFields } from \"./schema-editor.js\"\n\n/**\n * The entry form as data: which fields it renders, what it starts from, which error\n * belongs to which control, and whether anything has changed.\n *\n * None of it renders, so all of it is testable in the node project, and the create screen\n * and the edit screen cannot drift apart by deriving the same answers twice. The field\n * type registry arrives as an argument rather than as an import, because `src/fields`\n * pulls in React and core must not.\n */\n\nexport interface FormField {\n key: string\n field: FieldDefinition\n}\n\n/**\n * The fields the form renders, in the order the schema declares them.\n *\n * A hidden field is skipped: it still stores and still validates (see the note on\n * `hidden` in types/field.ts), it simply has no control. That is exactly why an error on\n * a hidden field is reported at form level by `unroutableIssues` instead of vanishing.\n */\nexport function formFields(type: ContentTypeLike): readonly FormField[] {\n // `orderedFields` rather than the map's own order: a stored content type comes back from\n // Firestore with its field map sorted, so the order an editor arranged in the Schema\n // Builder lives in `fieldOrder` and nowhere else.\n return orderedFields(type).filter(({ field }) => field.hidden !== true)\n}\n\n/** What a new entry starts with: every field at its own field type's default. */\nexport function initialFormValues(\n type: ContentTypeLike,\n registry: FieldTypeRegistry,\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n\n for (const [key, field] of Object.entries(type.fields)) {\n const spec = registry.get(field.type)\n values[key] = spec === undefined ? null : spec.defaultValue(field)\n }\n\n return values\n}\n\n/**\n * The values of a stored entry, narrowed to the fields the schema declares.\n *\n * Metadata is deliberately dropped: `status`, `createdBy` and the timestamps belong to the\n * adapter, and a form that carried them in its value map could write one back. A field the\n * entry predates falls back to the field type's default rather than to `undefined`, so the\n * control is not handed a value it has no state for.\n */\nexport function formValuesFromEntry(\n type: ContentTypeLike,\n entry: Entry,\n registry: FieldTypeRegistry,\n): Record<string, unknown> {\n const stored = entry as unknown as Record<string, unknown>\n const values: Record<string, unknown> = {}\n\n for (const [key, field] of Object.entries(type.fields)) {\n if (key in stored && stored[key] !== undefined) {\n values[key] = stored[key]\n continue\n }\n const spec = registry.get(field.type)\n values[key] = spec === undefined ? null : spec.defaultValue(field)\n }\n\n return values\n}\n\n/**\n * The errors belonging to one field and everything nested under it.\n *\n * Path-based, which is what lets a server error reach the control that owns it: the\n * validator and the adapter both report `blocks.1.items.0.text`, and passing that whole\n * subtree to the `blocks` input lets the repeater hand it down row by row. A prefix match\n * on the dot is required, or `title` would also claim `titleImage`.\n */\nexport function errorsUnder<TError extends FieldError>(\n errors: readonly TError[],\n path: string,\n): TError[] {\n return errors.filter(\n (error) => error.path === path || error.path.startsWith(`${path}.`),\n )\n}\n\n/** Where an error came from. The two are told apart because they expire differently. */\nexport type ErrorOrigin = \"client\" | \"server\"\n\nexport interface FormIssue extends FieldError {\n origin: ErrorOrigin\n}\n\n/**\n * The errors on screen, from the two sources that produce them.\n *\n * A client error is recomputed from the current values on every keystroke, so it is always\n * about what is in the form right now. A server error is a verdict about the values that\n * were submitted, and only another submission can retract it: uniqueness is the clearest\n * case, because nothing in the browser can know that a slug is free.\n *\n * So they cannot simply be concatenated. A client error wins its own path, because it is\n * the more current statement about that value; a server error survives on every path the\n * client has nothing to say about, which is what keeps \"that slug is taken\" on screen\n * while the editor types the next character.\n */\nexport function mergeIssues(\n client: readonly FieldError[],\n server: readonly FieldError[],\n): readonly FormIssue[] {\n const claimed = new Set(client.map((error) => error.path))\n\n return [\n ...client.map((error): FormIssue => ({ ...error, origin: \"client\" })),\n ...server\n .filter((error) => !claimed.has(error.path))\n .map((error): FormIssue => ({ ...error, origin: \"server\" })),\n ]\n}\n\n/**\n * Issues no control will render, which is the only case where a toast or a form-level\n * message is the right answer.\n *\n * Three ways an error gets here: it names a hidden field, it names a field the schema no\n * longer declares, or it names nothing at all. In each case the form has to say it\n * somewhere, and saying it next to an unrelated control would be worse than saying it at\n * the top.\n */\nexport function unroutableIssues<TError extends FieldError>(\n issues: readonly TError[],\n type: ContentTypeLike,\n): readonly TError[] {\n const rendered = new Set(formFields(type).map((entry) => entry.key))\n\n return issues.filter((issue) => {\n const root = issue.path.split(\".\")[0]\n return root === undefined || root === \"\" || !rendered.has(root)\n })\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (a === null || b === null) return false\n if (typeof a !== \"object\" || typeof b !== \"object\") return false\n\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false\n return a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))\n }\n\n const left = a as Record<string, unknown>\n const right = b as Record<string, unknown>\n const keys = new Set([...Object.keys(left), ...Object.keys(right)])\n\n for (const key of keys) {\n if (!deepEqual(left[key], right[key])) return false\n }\n return true\n}\n\n/**\n * Whether the form holds anything the baseline does not.\n *\n * Structural rather than by reference, because every field input replaces the value it\n * owns on each edit, and a repeater rebuilds its whole array to move one row. Reference\n * equality would call an entry dirty for having been touched and untouched again, and the\n * unsaved-changes guard is only worth having if it is quiet when nothing changed.\n */\nexport function valuesChanged(\n baseline: Record<string, unknown>,\n current: Record<string, unknown>,\n): boolean {\n return !deepEqual(baseline, current)\n}\n\n/**\n * Whether the stored entry moved since the editor read it.\n *\n * `updatedAt` is stamped by the adapter on every write, so a difference means somebody\n * else saved. The editor asks this immediately before its own write rather than\n * subscribing: a live listener would be a second read path, a second failure mode, and an\n * interruption in the middle of typing. It closes the wide window, not the narrow one; two\n * writes in the same second still resolve last-write-wins, which the adapter's document\n * semantics already decide.\n */\nexport function entryChangedSince(\n baseline: Pick<EntryMeta, \"updatedAt\">,\n current: Pick<EntryMeta, \"updatedAt\">,\n): boolean {\n return baseline.updatedAt !== current.updatedAt\n}\n","import { can } from \"../permissions/can.js\"\nimport type { AuthUser } from \"../types/auth.js\"\nimport type { ContentTypeLike } from \"../types/content-type.js\"\nimport type { EntryMeta, EntryStatus } from \"../types/entry.js\"\n\n/**\n * What the status control may offer, and to whom.\n *\n * Status is the one piece of entry metadata an editor writes, so it is not a display: it\n * is a set of transitions out of the status the entry is in, each one answered by `can()`\n * against the entry itself. A denied transition keeps its reason rather than disappearing,\n * because \"you cannot publish this\" is information and an empty panel is not.\n *\n * Hiding or disabling a transition here is presentation. The adapter validates the write\n * and the security rules check the same matrix again, so a user who reaches past this list\n * gains nothing.\n */\n\nexport interface StatusTransition {\n to: EntryStatus\n /** Imperative label for the control: \"Publish\", \"Unpublish\", \"Archive\". */\n label: string\n /** What it does to the public site, in one sentence. */\n effect: string\n allowed: boolean\n /** Why it is not offered. Null when it is. */\n reason: string | null\n}\n\n/** Just enough of an entry to answer a transition question. */\nexport type StatusSubject = Pick<EntryMeta, \"status\" | \"createdBy\">\n\ninterface TransitionShape {\n to: EntryStatus\n label: string\n effect: string\n}\n\n/**\n * The legal moves, keyed by where the entry is now.\n *\n * Exhaustive by construction, so a fourth status cannot be added to `ENTRY_STATUSES`\n * without deciding what leads into and out of it. The wording follows what the adapter\n * actually does with `publishedAt`: first publish stamps it, republishing keeps it, and\n * moving back to draft clears it.\n */\nconst TRANSITIONS: Readonly<Record<EntryStatus, readonly TransitionShape[]>> = {\n draft: [\n {\n to: \"published\",\n label: \"Publish\",\n effect: \"Makes this entry readable on the public site.\",\n },\n {\n to: \"archived\",\n label: \"Archive\",\n effect: \"Keeps the entry but takes it out of the public site.\",\n },\n ],\n published: [\n {\n to: \"draft\",\n label: \"Unpublish\",\n effect: \"Takes the entry off the public site and clears its publication date.\",\n },\n {\n to: \"archived\",\n label: \"Archive\",\n effect: \"Keeps the entry but takes it out of the public site.\",\n },\n ],\n archived: [\n {\n to: \"draft\",\n label: \"Restore as draft\",\n effect: \"Brings the entry back into editing. It stays off the public site.\",\n },\n {\n to: \"published\",\n label: \"Publish\",\n effect: \"Makes this entry readable on the public site again.\",\n },\n ],\n}\n\n/** How a status reads in the sidebar, under the badge. */\nexport const STATUS_EFFECT: Readonly<Record<EntryStatus, string>> = {\n draft: \"Not visible on the public site.\",\n published: \"Visible on the public site.\",\n archived: \"Kept out of the public site and out of the way.\",\n}\n\n/**\n * The transitions for one entry, in a fixed order.\n *\n * `entry` is null for an entry that has not been created yet. A new entry is a draft that\n * does not exist, so the only move it has is the one that brings it into existence\n * published, and the permission question is `create` rather than `update`.\n */\nexport function statusTransitions(\n user: AuthUser,\n type: ContentTypeLike,\n entry: StatusSubject | null,\n): readonly StatusTransition[] {\n const from = entry?.status ?? \"draft\"\n const shapes =\n entry === null\n ? TRANSITIONS.draft.filter((shape) => shape.to === \"published\")\n : TRANSITIONS[from]\n\n const allowed =\n entry === null\n ? can(user, \"create\", { type })\n : can(user, \"update\", { type, entry: { createdBy: entry.createdBy } })\n\n const reason = allowed ? null : denial(user, entry)\n\n return shapes.map((shape) => ({ ...shape, allowed, reason }))\n}\n\nfunction denial(user: AuthUser, entry: StatusSubject | null): string {\n if (user.role === \"author\" && entry !== null && entry.createdBy !== user.uid) {\n return \"An author can only change the status of their own entries.\"\n }\n return \"Your role cannot change the status of this entry.\"\n}\n\n/**\n * Whether this user may save the values of this entry at all.\n *\n * Separate from the transitions because saving without changing the status is its own\n * action, and because the create screen asks the question before an entry exists.\n */\nexport function canSaveEntry(\n user: AuthUser,\n type: ContentTypeLike,\n entry: StatusSubject | null,\n): boolean {\n return entry === null\n ? can(user, \"create\", { type })\n : can(user, \"update\", { type, entry: { createdBy: entry.createdBy } })\n}\n","import type { CmsAdapter } from \"./adapter.js\"\nimport { slugFieldKey } from \"./slug-field.js\"\nimport type { ContentTypeLike } from \"../types/content-type.js\"\n\n/**\n * The live uniqueness answer the form hands to the field layer.\n *\n * `unique` is the one validation rule that cannot be answered without reading other\n * entries, so the shared validator leaves it to the adapter, which checks it again on\n * every write. This is only the hint in front of that check, and it is deliberately narrow.\n *\n * It answers for the content type's slug field and for nothing else. That is not a\n * shortcut: `getBySlug` is a single equality query that Firestore serves from an automatic\n * single-field index, while asking the same question about an arbitrary field would need a\n * composite index per field, generated per content type, for a hint. Any other path is\n * rejected rather than answered \"free\", because telling an editor a value is available\n * when nobody looked is worse than telling them nothing. The rejection is what the slug\n * input reads as \"no answer\", and it then shows no availability hint at all.\n */\nexport interface EntryUniqueOptions {\n adapter: CmsAdapter\n type: ContentTypeLike\n /** The entry being edited, so its own value does not count as taken. Null on create. */\n entryId: string | null\n}\n\nexport function entryUniqueCheck(\n options: EntryUniqueOptions,\n): (path: string, value: string) => Promise<boolean> {\n const { adapter, type, entryId } = options\n const key = slugFieldKey(type)\n\n return async (path, value) => {\n if (key === null || path !== key) {\n throw new Error(\n `Uniqueness of \"${path}\" cannot be checked from the form. The adapter checks it before the write.`,\n )\n }\n\n const found = await adapter.getBySlug(type.slug, value)\n return found === null || found.id === entryId\n }\n}\n","import type { Role } from \"../types/auth.js\"\nimport type { FieldError } from \"../types/entry.js\"\n\nimport {\n PermissionError,\n SchemaError,\n ValidationError,\n WriteLimitError,\n} from \"./errors.js\"\n\n/**\n * The one error taxonomy the admin renders from.\n *\n * Before this existed the treatments were real but scattered: the entry editor routed a\n * `ValidationError` to the offending field and fell back to a form-level message, the\n * Schema Builder put every failure in one banner, the media screens put every failure in a\n * toast, and a permission refusal read as \"something went wrong\" in all three. Each of\n * those was defensible on its own and none of them agreed with the others.\n *\n * This module is not a second system layered over them. It is the first one, factored out:\n * the M6 rule that a validation error lands on the field and only an unroutable path\n * reaches the form is preserved exactly, as the `field` treatment, and every other screen\n * now asks the same question instead of answering it privately.\n *\n * Five categories, one treatment each:\n *\n * | category | treatment | what the user sees |\n * |--------------|-----------|-----------------------------------------------------------|\n * | `validation` | `field` | the message on the control that owns the path; never a bare toast |\n * | `permission` | `banner` | a form-level notice naming the action, the subject and the role |\n * | `not-found` | `state` | an empty state with a way back; no alert, no red |\n * | `conflict` | `resolve` | a banner that names both resolutions and offers them |\n * | `transport` | `retry` | a banner or state carrying the one action that can help |\n *\n * `unknown` is the sixth, for a failure that fits none of them. It takes the `banner`\n * treatment and says plainly that the cause is unknown, rather than guessing at a category\n * and offering a retry that cannot work.\n *\n * The toast is deliberately not a treatment. It is the announcement channel for a failure\n * whose surface has already closed (a dialog that dismissed itself, a background upload),\n * and when it is used it prints this same `title` and `message`, so a failure can never be\n * worded twice.\n *\n * No React and no Firebase here, so the classification is testable in the node project and\n * callable from the adapter as well as from a render.\n */\n\nexport const ERROR_CATEGORIES = [\n \"validation\",\n \"permission\",\n \"not-found\",\n \"conflict\",\n \"transport\",\n \"unknown\",\n] as const\n\nexport type ErrorCategory = (typeof ERROR_CATEGORIES)[number]\n\n/** The chrome a category is rendered in. Exactly one per category; see the table above. */\nexport type ErrorTreatment = \"field\" | \"banner\" | \"state\" | \"resolve\" | \"retry\"\n\nconst TREATMENTS: Readonly<Record<ErrorCategory, ErrorTreatment>> = {\n validation: \"field\",\n permission: \"banner\",\n \"not-found\": \"state\",\n conflict: \"resolve\",\n transport: \"retry\",\n unknown: \"banner\",\n}\n\n/** The treatment a category must be rendered in. The single mapping, used by every screen. */\nexport function treatmentFor(category: ErrorCategory): ErrorTreatment {\n return TREATMENTS[category]\n}\n\nexport interface ClassifiedError {\n category: ErrorCategory\n treatment: ErrorTreatment\n /** A short heading. Used as the banner's first line and as a toast title. */\n title: string\n /** One or two sentences: what happened, and what the user can do about it. */\n message: string\n /**\n * The field errors, for `validation` only. Every other category leaves this empty, which\n * is what stops a non-validation failure from being routed onto a control.\n */\n fieldErrors: readonly FieldError[]\n /** True only for `transport`. A retry of anything else repeats the same refusal. */\n retryable: boolean\n /** What was thrown, kept for a boundary that wants to log it. Never rendered. */\n cause: unknown\n}\n\nexport interface ErrorContext {\n /** The signed-in role, so a permission refusal can name who was refused. */\n role?: Role | undefined\n /**\n * What the user was doing, as a verb phrase that completes \"could not ...\": \"save this\n * entry\", \"delete these entries\". Used for transport and unknown failures, where the\n * error itself says nothing a reader can act on.\n */\n action?: string | undefined\n /** What was acted on, named the way the UI names it: a content type label, a file name. */\n subject?: string | undefined\n}\n\n/**\n * Firestore error codes that mean the request never got a verdict.\n *\n * Duck-typed off `error.code` rather than caught by class, because nothing under\n * `src/core` or `src/admin` may import Firebase and a `FirebaseError` is therefore not\n * nameable here. Only these five are transport: everything else Firestore reports is a\n * verdict about the request, and telling a user to try again after a verdict is a lie.\n */\nconst TRANSPORT_CODES: readonly string[] = [\n \"unavailable\",\n \"deadline-exceeded\",\n \"cancelled\",\n \"internal\",\n \"resource-exhausted\",\n]\n\nconst PERMISSION_CODES: readonly string[] = [\"permission-denied\", \"unauthenticated\"]\n\nconst NOT_FOUND_CODES: readonly string[] = [\"not-found\"]\n\nconst CONFLICT_CODES: readonly string[] = [\n \"aborted\",\n \"already-exists\",\n \"failed-precondition\",\n]\n\n/** `firestore/unavailable` and `unavailable` are the same code from two Firebase versions. */\nfunction codeOf(thrown: unknown): string | undefined {\n if (typeof thrown !== \"object\" || thrown === null) return undefined\n const code: unknown = (thrown as { code?: unknown }).code\n if (typeof code !== \"string\") return undefined\n const slash = code.lastIndexOf(\"/\")\n return slash === -1 ? code : code.slice(slash + 1)\n}\n\n/**\n * A network failure with no code at all.\n *\n * `fetch` rejects with a bare `TypeError` whose message is the only evidence there is.\n * Matching on it is unpleasant and it is still better than filing a dropped connection\n * under \"unknown\", which offers no retry.\n */\nfunction looksLikeNetworkFailure(thrown: unknown): boolean {\n if (!(thrown instanceof Error)) return false\n return /network|fetch failed|failed to fetch|offline|connection/i.test(thrown.message)\n}\n\nfunction withRole(sentence: string, role: Role | undefined): string {\n return role === undefined ? sentence : `${sentence} You are signed in as ${role}.`\n}\n\n/**\n * Classifies anything that was thrown into exactly one category with its treatment.\n *\n * Order matters and is by certainty. A class the package defines is conclusive, a\n * Firestore code is strong evidence, and a message match is the last resort.\n */\nexport function classifyError(\n thrown: unknown,\n context: ErrorContext = {},\n): ClassifiedError {\n const target = context.subject === undefined ? \"\" : ` \"${context.subject}\"`\n\n if (thrown instanceof ValidationError) {\n return {\n category: \"validation\",\n treatment: TREATMENTS.validation,\n title:\n thrown.errors.length === 1\n ? \"One field needs attention\"\n : `${String(thrown.errors.length)} fields need attention`,\n message:\n \"The values were refused before anything was saved. Each problem is shown on the field it belongs to.\",\n fieldErrors: thrown.errors,\n retryable: false,\n cause: thrown,\n }\n }\n\n if (thrown instanceof PermissionError) {\n return {\n category: \"permission\",\n treatment: TREATMENTS.permission,\n title: \"You are not allowed to do that\",\n // The brief for this category: say what is denied and by whom. PermissionError\n // already carries the action and the content type; the role comes from the caller.\n message: withRole(thrown.message, context.role),\n fieldErrors: [],\n retryable: false,\n cause: thrown,\n }\n }\n\n if (thrown instanceof WriteLimitError) {\n // A size or shape rejection is about the content, so it is validation and it carries a\n // path. A rate rejection is about timing alone and clears by itself, so it is the one\n // write limit a retry actually fixes.\n if (thrown.code === \"too-fast\") {\n return {\n category: \"transport\",\n treatment: TREATMENTS.transport,\n title: \"Too many writes at once\",\n message: `${thrown.message} Wait a moment and try again.`,\n fieldErrors: [],\n retryable: true,\n cause: thrown,\n }\n }\n\n return {\n category: \"validation\",\n treatment: TREATMENTS.validation,\n title: \"This is too large to save\",\n message: thrown.message,\n fieldErrors: [{ path: thrown.path, rule: thrown.code, message: thrown.message }],\n retryable: false,\n cause: thrown,\n }\n }\n\n if (thrown instanceof SchemaError) {\n return {\n category: \"validation\",\n treatment: TREATMENTS.validation,\n title: \"This definition was refused\",\n message: thrown.message,\n fieldErrors: [],\n retryable: false,\n cause: thrown,\n }\n }\n\n const code = codeOf(thrown)\n\n if (code !== undefined && PERMISSION_CODES.includes(code)) {\n return {\n category: \"permission\",\n treatment: TREATMENTS.permission,\n title: \"You are not allowed to do that\",\n message: withRole(\n code === \"unauthenticated\"\n ? \"Your session is no longer valid, so the database refused the request. Sign in again.\"\n : `The database refused this request${target}. Your account does not have permission for it.`,\n context.role,\n ),\n fieldErrors: [],\n retryable: false,\n cause: thrown,\n }\n }\n\n if (code !== undefined && NOT_FOUND_CODES.includes(code)) {\n return {\n category: \"not-found\",\n treatment: TREATMENTS[\"not-found\"],\n title: \"Not found\",\n message: `What this page refers to${target} does not exist. It may have been deleted, or the address may be wrong.`,\n fieldErrors: [],\n retryable: false,\n cause: thrown,\n }\n }\n\n if (code !== undefined && CONFLICT_CODES.includes(code)) {\n return {\n category: \"conflict\",\n treatment: TREATMENTS.conflict,\n title: \"This changed somewhere else\",\n message:\n \"Someone else wrote to this while you were working on it, so nothing was saved. Reload to see their version, or save again to overwrite it.\",\n fieldErrors: [],\n retryable: false,\n cause: thrown,\n }\n }\n\n if (\n (code !== undefined && TRANSPORT_CODES.includes(code)) ||\n looksLikeNetworkFailure(thrown)\n ) {\n return {\n category: \"transport\",\n treatment: TREATMENTS.transport,\n title: \"Could not reach the database\",\n message: `The request did not get through${\n context.action === undefined\n ? \"\"\n : `, so nothing was changed while trying to ${context.action}`\n }. Check your connection and try again.`,\n fieldErrors: [],\n retryable: true,\n cause: thrown,\n }\n }\n\n return {\n category: \"unknown\",\n treatment: TREATMENTS.unknown,\n title:\n context.action === undefined\n ? \"Something went wrong\"\n : `Could not ${context.action}`,\n message:\n thrown instanceof Error && thrown.message !== \"\"\n ? thrown.message\n : \"No further detail was reported. Nothing is known to have changed.\",\n fieldErrors: [],\n retryable: false,\n cause: thrown,\n }\n}\n\n/**\n * The conflict the entry editor detects for itself.\n *\n * A stale write is noticed by comparing timestamps before the write, not by catching\n * something, so there is nothing to classify. It still has to arrive as a `ClassifiedError`\n * or the editor would be writing its own sentence for a category this module owns, which is\n * exactly the split this module exists to close.\n */\nexport function staleWriteError(subject: string): ClassifiedError {\n return {\n category: \"conflict\",\n treatment: TREATMENTS.conflict,\n title: \"Someone else saved this first\",\n message: `${subject} was changed after you opened it. Reload to see their version, or save again to overwrite it with yours.`,\n fieldErrors: [],\n retryable: false,\n cause: null,\n }\n}\n\n/** The other locally detected case: the thing being edited is gone. A state, not an error. */\nexport function missingError(subject: string): ClassifiedError {\n return {\n category: \"not-found\",\n treatment: TREATMENTS[\"not-found\"],\n title: `This ${subject} no longer exists`,\n message:\n \"It was deleted, or the address points at something this project does not hold. Nothing was changed.\",\n fieldErrors: [],\n retryable: false,\n cause: null,\n }\n}\n\n/** The locally detected permission refusal, for a screen that asked `can()` and got false. */\nexport function deniedError(what: string, role: Role | undefined): ClassifiedError {\n return {\n category: \"permission\",\n treatment: TREATMENTS.permission,\n title: \"You are not allowed to do that\",\n message: withRole(`Your account may not ${what}.`, role),\n fieldErrors: [],\n retryable: false,\n cause: null,\n }\n}\n","import type { EntryFilter, ListQuery, SortSpec } from \"./adapter.js\"\nimport { normalizeLimit } from \"./adapter.js\"\nimport type { ContentTypeLike } from \"../types/content-type.js\"\nimport type { EntryStatus } from \"../types/entry.js\"\nimport type { FieldDefinition, FieldType } from \"../types/field.js\"\n\nimport { orderedFields } from \"./schema-editor.js\"\n\n/**\n * What a collection list is, derived from a content type.\n *\n * The screen renders this; it never decides it. Columns, the sortable set, the filter\n * controls and the query that reads a page are all computed here from the schema, which is\n * what makes a new content type a data change and not a screen change.\n *\n * Nothing in this file imports React, Firebase or the field registry, so it is domain\n * logic: it runs in the node test project, in the SDK, and in a server component.\n */\n\n/** How a column's cells are typeset. The cell itself never inspects the field type. */\nexport type ColumnAlign = \"text\" | \"numeric\" | \"date\"\n\n/**\n * What a field type contributes to a table.\n *\n * `Record<FieldType, ...>` on purpose: adding a thirteenth field type fails to typecheck\n * until somebody decides whether it fits in a table cell. This is the one place that\n * decision lives, the same way `fieldTypes` is the one place a field type is registered.\n * `\"none\"` means the value cannot be reduced to one readable line, so it gets no column:\n * a rich text document, an uploaded file, a repeater of rows.\n */\nconst COLUMN_ALIGN: Record<FieldType, ColumnAlign | \"none\"> = {\n text: \"text\",\n textarea: \"none\",\n number: \"numeric\",\n boolean: \"text\",\n select: \"text\",\n date: \"date\",\n slug: \"text\",\n richtext: \"none\",\n image: \"none\",\n file: \"none\",\n relation: \"none\",\n repeater: \"none\",\n}\n\n/**\n * What a field type can be filtered by.\n *\n * \"Simple\" means a closed set of values the schema already names, matched by equality.\n * `select` carries its options, and `boolean` has exactly two; both produce a filter\n * control with no free text and a query Firestore answers from one index. Everything else\n * is excluded and the reasons differ: a `text`, `slug` or `richtext` value is open-ended,\n * so equality on it is useless and substring matching is not on offer (see `search`);\n * `number` and `date` want ranges, which need an interval control and a second index axis;\n * `image`, `file` and `relation` are ids, which a reader cannot pick from a dropdown; a\n * `repeater` has no single value at all.\n */\nconst FILTERABLE: readonly FieldType[] = [\"select\", \"boolean\"]\n\nexport type ColumnKind = \"title\" | \"field\" | \"status\" | \"updated\"\n\nexport interface ListColumn {\n /** Field key, or `status` / `updatedAt` for the two metadata columns. */\n key: string\n label: string\n kind: ColumnKind\n align: ColumnAlign\n /**\n * Whether the header offers sorting. Bounded on purpose: every sortable column costs a\n * pair of composite indexes per filter combination, so the list sorts by title, by the\n * type's declared default sort field, and by last update, and not by every column it\n * shows. See `sortableFieldKeys` and core/indexes.ts.\n */\n sortable: boolean\n}\n\nexport interface ListFilterOption {\n value: string\n label: string\n}\n\nexport interface ListFilterDescriptor {\n /** Field key the filter applies to. */\n key: string\n label: string\n options: readonly ListFilterOption[]\n /**\n * `array-contains` for a multi-select, whose stored value is an array; `==` otherwise.\n * The screen passes this straight through, so it never decides an operator.\n */\n op: EntryFilter[\"op\"]\n /** Filter values are strings in the UI; a boolean field converts back on the way out. */\n valueType: \"string\" | \"boolean\"\n}\n\n/** Everything the reader has narrowed the list to, as one serializable value. */\nexport interface ListViewState {\n /** Prefix term for the title field. Empty means no search. */\n search: string\n status: EntryStatus | \"any\"\n /** Field key to selected option value. A key absent means that filter is off. */\n filters: Readonly<Record<string, string>>\n sort: SortSpec\n /** Opaque, from a previous page. The UI stores it and never builds one. */\n cursor: string | null\n}\n\n/** Last touched first: what an editor who just saved something expects to see at the top. */\nexport const FALLBACK_SORT: SortSpec = { field: \"updatedAt\", direction: \"desc\" }\n\nexport function defaultListSort(type: ContentTypeLike): SortSpec {\n return type.defaultSort === undefined\n ? FALLBACK_SORT\n : { field: type.defaultSort.field, direction: type.defaultSort.direction }\n}\n\nexport function initialListState(type: ContentTypeLike): ListViewState {\n return {\n search: \"\",\n status: \"any\",\n filters: {},\n sort: defaultListSort(type),\n cursor: null,\n }\n}\n\n/** A content type can only be searched when it names the field to match against. */\nexport function canSearch(type: ContentTypeLike): boolean {\n return type.titleField !== undefined\n}\n\n/** True while a term is narrowing the list, which is also when sorting is not on offer. */\nexport function isSearching(state: ListViewState): boolean {\n return state.search.trim() !== \"\"\n}\n\n/**\n * The keys a list may sort by: the title, the type's own default sort field, and the last\n * update. Kept small deliberately; see `ListColumn.sortable`.\n */\nexport function sortableFieldKeys(type: ContentTypeLike): readonly string[] {\n const keys = new Set<string>()\n if (type.titleField !== undefined) keys.add(type.titleField)\n if (type.defaultSort !== undefined) keys.add(type.defaultSort.field)\n keys.add(\"updatedAt\")\n return [...keys]\n}\n\n/** How many schema fields get their own column before the row would stop being readable. */\nexport const MAX_FIELD_COLUMNS = 2\n\nexport interface ListColumnOptions {\n maxFieldColumns?: number\n}\n\n/**\n * The columns, in reading order: title, up to two field columns, status, updated.\n *\n * Hidden fields are skipped because the form does not show them either, and the title\n * field is not repeated as a field column. A type with no `titleField` still gets a title\n * column: `entryTitle` names the entry \"Untitled\" and the row remains clickable, which is\n * better than a list whose rows have no handle at all.\n */\nexport function listColumns(\n type: ContentTypeLike,\n { maxFieldColumns = MAX_FIELD_COLUMNS }: ListColumnOptions = {},\n): readonly ListColumn[] {\n const sortable = new Set(sortableFieldKeys(type))\n const titleKey = type.titleField\n const titleField = titleKey === undefined ? undefined : type.fields[titleKey]\n\n const columns: ListColumn[] = [\n {\n key: titleKey ?? \"title\",\n label: titleField?.label ?? \"Title\",\n kind: \"title\",\n align: \"text\",\n sortable: titleKey !== undefined && sortable.has(titleKey),\n },\n ]\n\n for (const { key, field } of orderedFields(type)) {\n if (columns.length > maxFieldColumns) break\n if (key === titleKey || field.hidden === true) continue\n\n const align = COLUMN_ALIGN[field.type]\n if (align === \"none\") continue\n\n columns.push({\n key,\n label: field.label,\n kind: \"field\",\n align,\n sortable: sortable.has(key),\n })\n }\n\n columns.push(\n { key: \"status\", label: \"Status\", kind: \"status\", align: \"text\", sortable: false },\n {\n key: \"updatedAt\",\n label: \"Updated\",\n kind: \"updated\",\n align: \"date\",\n sortable: true,\n },\n )\n\n return columns\n}\n\n/** Whether a field definition can back a filter control. */\nexport function isFilterableField(field: FieldDefinition): boolean {\n return FILTERABLE.includes(field.type)\n}\n\n/**\n * The field filters a content type offers, in schema order.\n *\n * Status is not among them: it is entry metadata rather than a field, every content type\n * has it, and the adapter takes it as its own argument.\n */\nexport function listFilters(type: ContentTypeLike): readonly ListFilterDescriptor[] {\n const descriptors: ListFilterDescriptor[] = []\n\n for (const { key, field } of orderedFields(type)) {\n if (field.hidden === true || !isFilterableField(field)) continue\n\n if (field.type === \"select\") {\n descriptors.push({\n key,\n label: field.label,\n options: field.options.map((option) => ({\n value: option.value,\n label: option.label,\n })),\n // A multi-select stores an array, so equality would only match an entry whose\n // whole array is that one value.\n op: field.multiple === true ? \"array-contains\" : \"==\",\n valueType: \"string\",\n })\n continue\n }\n\n descriptors.push({\n key,\n label: field.label,\n options: [\n { value: \"true\", label: \"Yes\" },\n { value: \"false\", label: \"No\" },\n ],\n op: \"==\",\n valueType: \"boolean\",\n })\n }\n\n return descriptors\n}\n\n/**\n * The active filters as adapter filters.\n *\n * A stored value the schema no longer offers is dropped rather than sent: a filter for a\n * select option that was removed from the content type would return nothing and read as\n * \"there are no entries\".\n */\nexport function listFilterQuery(\n type: ContentTypeLike,\n filters: Readonly<Record<string, string>>,\n): readonly EntryFilter[] {\n const active: EntryFilter[] = []\n\n for (const descriptor of listFilters(type)) {\n const chosen = filters[descriptor.key]\n if (chosen === undefined || chosen === \"\") continue\n if (!descriptor.options.some((option) => option.value === chosen)) continue\n\n active.push({\n field: descriptor.key,\n op: descriptor.op,\n value: descriptor.valueType === \"boolean\" ? chosen === \"true\" : chosen,\n })\n }\n\n return active\n}\n\nexport interface BuildListQueryOptions {\n pageSize?: number\n}\n\n/**\n * The read for one page.\n *\n * The sort is omitted while a search is active rather than sent and ignored: the adapter\n * forces title order for a prefix range, and a query object that still claimed another\n * order would be a lie the caller could act on.\n */\nexport function buildListQuery(\n type: ContentTypeLike,\n state: ListViewState,\n { pageSize }: BuildListQueryOptions = {},\n): ListQuery {\n const term = state.search.trim()\n const searching = term !== \"\" && canSearch(type)\n\n return {\n filters: listFilterQuery(type, state.filters),\n ...(searching ? { search: term } : { sort: state.sort }),\n limit: normalizeLimit(pageSize),\n cursor: state.cursor,\n status: state.status,\n }\n}\n\n/** The same narrowing as a count: no order, no cursor, no page size. */\nexport function buildCountQuery(\n type: ContentTypeLike,\n state: ListViewState,\n): Pick<ListQuery, \"filters\" | \"status\" | \"search\"> {\n const term = state.search.trim()\n\n return {\n filters: listFilterQuery(type, state.filters),\n status: state.status,\n ...(term !== \"\" && canSearch(type) ? { search: term } : {}),\n }\n}\n\n/** Whether anything is narrowing the list, which is what tells an empty result apart. */\nexport function isNarrowed(state: ListViewState): boolean {\n return (\n isSearching(state) ||\n state.status !== \"any\" ||\n Object.values(state.filters).some((value) => value !== \"\")\n )\n}\n\n/**\n * The next sort after a header is clicked.\n *\n * Clicking the sorted column flips it. Clicking another column starts in the direction\n * that column is usually read in: newest first for a date, A to Z for everything else.\n */\nexport function nextSort(\n current: SortSpec,\n column: ListColumn,\n key = column.key,\n): SortSpec {\n if (current.field === key) {\n return { field: key, direction: current.direction === \"asc\" ? \"desc\" : \"asc\" }\n }\n return { field: key, direction: column.align === \"date\" ? \"desc\" : \"asc\" }\n}\n","import { SchemaError } from \"./errors.js\"\nimport { isFilterableField, sortableFieldKeys } from \"./list-view.js\"\nimport type { ContentTypeLike } from \"../types/content-type.js\"\n\n/**\n * The composite indexes a collection list needs, derived from the content types.\n *\n * `.docs/rules/firestore-indexes.md` requires every query to ship with its index, and the\n * list view's queries are generated, so the index entries have to be generated too. A\n * filter on a field plus an ordering on another field is a composite index, and the field\n * paths come from the customer's schema, which no hand-written file can know in advance.\n *\n * Entries of every content type live in a subcollection called `entries`, so one\n * `COLLECTION`-scoped index serves every type that uses the same field path. That is why\n * the result is deduplicated across types rather than emitted per type.\n *\n * What is deliberately not emitted: a query with no filter at all. Ordering by one field\n * with `__name__` as the tiebreaker in the same direction is served by Firestore's\n * automatic single-field indexes, and emitting it would waste an index slot.\n */\n\nexport type IndexOrder = \"ASCENDING\" | \"DESCENDING\"\n\nexport interface FirestoreIndexField {\n fieldPath: string\n order: IndexOrder\n}\n\nexport interface FirestoreIndex {\n collectionGroup: string\n queryScope: \"COLLECTION\"\n fields: readonly FirestoreIndexField[]\n}\n\nexport const ENTRY_COLLECTION_GROUP = \"entries\"\n\n/**\n * Firestore allows 200 composite indexes per database. Passing it is not a warning to be\n * ignored later: the deploy fails. Better to fail here, where the schema that caused it is\n * in hand, than in a customer's deploy log.\n */\nexport const MAX_ENTRY_INDEXES = 200\n\nfunction order(direction: \"asc\" | \"desc\"): IndexOrder {\n return direction === \"asc\" ? \"ASCENDING\" : \"DESCENDING\"\n}\n\n/** Every subset of the filterable fields, since the toolbar lets them combine freely. */\nfunction subsets(keys: readonly string[]): readonly (readonly string[])[] {\n return keys.reduce<readonly (readonly string[])[]>(\n (found, key) => [...found, ...found.map((subset) => [...subset, key])],\n [[]],\n )\n}\n\nfunction filterableKeys(type: ContentTypeLike): readonly string[] {\n return Object.entries(type.fields)\n .filter(([, field]) => field.hidden !== true && isFilterableField(field))\n .map(([key]) => key)\n}\n\nfunction keyOf(index: FirestoreIndex): string {\n return index.fields.map((field) => `${field.fieldPath}:${field.order}`).join(\"|\")\n}\n\n/**\n * Every index the generated list view can ask for, across the given content types.\n *\n * The shape of each query it covers is: an optional `status` equality, zero or more field\n * equalities, then the ordering field and `__name__` in the same direction. A prefix\n * search needs no extra entry: it ranges over the title field and orders by it, which is\n * the ordering already emitted for the title column.\n */\nexport function requiredEntryIndexes(\n types: readonly ContentTypeLike[],\n): readonly FirestoreIndex[] {\n const found = new Map<string, FirestoreIndex>()\n\n for (const type of types) {\n if (type.kind !== \"collection\") continue\n\n for (const filters of subsets(filterableKeys(type))) {\n for (const sortField of sortableFieldKeys(type)) {\n for (const direction of [\"asc\", \"desc\"] as const) {\n for (const withStatus of [true, false]) {\n // No equality at all is an automatic single-field index.\n if (!withStatus && filters.length === 0) continue\n // An equality on the field being ordered by is answered by the same index\n // without the ordering, and Firestore rejects the duplicate field path.\n if (filters.includes(sortField)) continue\n\n const index: FirestoreIndex = {\n collectionGroup: ENTRY_COLLECTION_GROUP,\n queryScope: \"COLLECTION\",\n fields: [\n ...(withStatus\n ? [{ fieldPath: \"status\", order: \"ASCENDING\" as const }]\n : []),\n ...filters.map((key) => ({\n fieldPath: key,\n order: \"ASCENDING\" as const,\n })),\n { fieldPath: sortField, order: order(direction) },\n { fieldPath: \"__name__\", order: order(direction) },\n ],\n }\n\n found.set(keyOf(index), index)\n }\n }\n }\n }\n }\n\n const indexes = [...found.values()].sort((left, right) =>\n keyOf(left).localeCompare(keyOf(right)),\n )\n\n if (indexes.length > MAX_ENTRY_INDEXES) {\n throw new SchemaError(\n `These content types need ${indexes.length} composite indexes on \"entries\", and Firestore allows ${MAX_ENTRY_INDEXES} per database. Reduce the number of filterable fields or sort fields.`,\n )\n }\n\n return indexes\n}\n\n/** Whether a checked-in index file already covers one required index. */\nexport function indexCovered(\n file: { indexes?: readonly FirestoreIndex[] },\n required: FirestoreIndex,\n): boolean {\n return (file.indexes ?? []).some(\n (candidate) =>\n candidate.collectionGroup === required.collectionGroup &&\n keyOf(candidate) === keyOf(required),\n )\n}\n\n/** The required indexes missing from a checked-in index file, for a drift test. */\nexport function missingEntryIndexes(\n file: { indexes?: readonly FirestoreIndex[] },\n types: readonly ContentTypeLike[],\n): readonly FirestoreIndex[] {\n return requiredEntryIndexes(types).filter((required) => !indexCovered(file, required))\n}\n","import type { ContentTypeLike } from \"../types/content-type.js\"\n\nimport { buildRegistry } from \"./registry.js\"\nimport type { Registry } from \"./registry.js\"\n\n/**\n * A registry that can change while the admin is running.\n *\n * Creating a content type in the Schema Builder has to put it in the sidebar, give it\n * routes and give it working CRUD without a rebuild and without a reload. The code half of\n * the registry is fixed for the lifetime of the process; only the database half moves, so\n * this holds the code half and swaps the database half.\n *\n * **How the change arrives, and what it costs.** `SchemaAdapter.watch` opens one Firestore\n * listener on the whole `schemas` collection and hands every snapshot to `setDatabaseTypes`.\n * One listener per open admin tab. It bills the documents of the initial snapshot once, then\n * one document read per changed schema document, and nothing at all while nobody is editing\n * structure. `schemas` holds one small document per database-defined content type, so a\n * customer with twenty of them pays twenty reads when an admin tab opens.\n *\n * The alternative considered was refetching the collection on every navigation inside the\n * admin. It needs no listener, but it bills the whole collection again on every screen\n * change, which for one editor clicking through twenty screens in an hour is already an\n * order of magnitude more reads than the listener, and it is still stale in the tab where\n * nobody navigated. The listener is both cheaper in the steady state and the only one of\n * the two that actually refreshes without an interaction. See\n * .docs/decisions/2026-09-18-schema-field-removal.md.\n *\n * A snapshot that produces an identical registry is dropped rather than published, because\n * Firestore replays a local write as a second snapshot once the server acknowledges it and\n * a re-render of every admin screen for a change nobody made is a visible cost.\n */\nexport interface RegistryStore {\n /** The current registry. Replaced, never mutated, so a React snapshot stays stable. */\n readonly registry: Registry\n /**\n * Swaps the database half. Returns true when the registry actually changed, which is what\n * the caller subscribes for.\n */\n setDatabaseTypes(types: readonly ContentTypeLike[]): boolean\n /** Notified after every real change. Returns the unsubscribe. */\n subscribe(listener: (registry: Registry) => void): () => void\n /**\n * Changes counter, monotonic. `useSyncExternalStore` compares it instead of deep\n * comparing the registry, and a server render can send it to the client to detect that\n * the two disagree.\n */\n readonly revision: number\n}\n\n/**\n * Stable string for a list of stored content types.\n *\n * `JSON.stringify` over the slug-sorted list. The stored definitions are plain JSON by\n * construction (that is why `RemovedField.removedAt` is an ISO string rather than a\n * Timestamp), so this is total, and key order inside one document is stable because\n * Firestore returns object keys in a fixed order.\n */\nfunction fingerprint(types: readonly ContentTypeLike[]): string {\n return JSON.stringify(\n [...types].sort((left, right) => left.slug.localeCompare(right.slug)),\n )\n}\n\nexport function createRegistryStore(\n code: readonly ContentTypeLike[],\n initialDatabase: readonly ContentTypeLike[] = [],\n): RegistryStore {\n let database = [...initialDatabase]\n let signature = fingerprint(database)\n let registry = buildRegistry(code, database)\n let revision = 0\n const listeners = new Set<(next: Registry) => void>()\n\n return {\n get registry() {\n return registry\n },\n get revision() {\n return revision\n },\n setDatabaseTypes(types) {\n const next = fingerprint(types)\n if (next === signature) return false\n\n database = [...types]\n signature = next\n // A malformed stored document is collected into `registry.invalid` rather than\n // thrown, so one corrupt schema cannot take the running admin down mid-session.\n registry = buildRegistry(code, database)\n revision += 1\n\n for (const listener of listeners) listener(registry)\n return true\n },\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n }\n}\n","import type { MigrationAdapter, StoredEntryRecord } from \"./adapter.js\"\nimport { SchemaError } from \"./errors.js\"\nimport { schemaVersionOf } from \"./schema-version.js\"\nimport type { ContentTypeLike } from \"../types/content-type.js\"\n\n/**\n * The migration runner: enough to move stored entries from one schema version to the next,\n * and deliberately not a migration framework.\n *\n * `content/{slug}.schemaVersion` says what shape the type declares today and every entry\n * carries the version it was last written at (see core/schema-version.ts). A migration is\n * the transform between two consecutive versions of one content type. This module owns the\n * registry of those transforms, the plan that chains them, and the loop that applies them.\n *\n * Four properties, because a migration that has any of them wrong is worse than none:\n *\n * 1. **Idempotent.** An entry is selected only while its stored version is behind, and the\n * write is conditional on that version still being what was read. Running twice migrates\n * nothing the first run finished, and two runners racing cannot both apply a step.\n * 2. **Resumable.** There is no run state to keep. A run that is interrupted leaves every\n * entry either fully at the old version or fully at the new one, so the next run is\n * simply the same run again over what is left.\n * 3. **Bounded.** It works in batches and returns what remains rather than walking a\n * collection of unknown size inside one call.\n * 4. **Honest when it fails.** A failing entry is named and the run stops rather than\n * grinding through thousands of entries repeating the same error. Nothing is rolled\n * back, because rolling back a partially migrated collection would need a second\n * migration to be correct; the entries that succeeded are at the new version and the\n * rest are untouched, which is a state the next run understands.\n *\n * What it is not: it does not create a migration for you, does not infer one from a schema\n * diff, and does not run automatically. A schema change that needs one says so (the Schema\n * Builder warns on a storage shape change) and somebody writes the transform.\n */\n\n/** The context a transform gets besides the values. */\nexport interface MigrationContext {\n entryId: string\n slug: string\n /** The version the stored entry is at, which is this migration's `from`. */\n fromVersion: number\n}\n\n/**\n * One step, from one version to the next.\n *\n * `from` and `to` are consecutive by construction: a transform that jumped two versions\n * could not be composed with one that filled the gap, and the registry would have no way\n * to migrate an entry that is stuck in between.\n */\nexport interface EntryMigration {\n slug: string\n from: number\n to: number\n /** One line, shown before a run so somebody can decide whether to start it. */\n describe: string\n /**\n * Returns the new values. Pure: no IO, no clock, no randomness, because it may be\n * replayed on a later run over a different slice of the same collection and has to give\n * the same answer.\n */\n migrate(\n values: Record<string, unknown>,\n context: MigrationContext,\n ): Record<string, unknown>\n}\n\nexport interface MigrationRegistry {\n /** Every registered migration for one content type, in version order. */\n forType(slug: string): readonly EntryMigration[]\n /**\n * The steps that take an entry from `fromVersion` to the type's current version.\n *\n * A version with no registered step is an identity step, not an error. The stamp moves\n * on any change to the stored shape, and most of those changes (adding a field, removing\n * one) need no transform at all, so requiring a migration per version would mean writing\n * an empty one after every schema edit. What a gap costs is stated rather than hidden: a\n * transform that was needed and never written is not detected here, and the place that\n * flags one is the Schema Builder's breaking-change warning at the moment of the edit.\n */\n plan(type: ContentTypeLike, fromVersion: number): readonly EntryMigration[]\n}\n\nexport function createMigrationRegistry(\n migrations: readonly EntryMigration[],\n): MigrationRegistry {\n const bySlug = new Map<string, EntryMigration[]>()\n\n for (const migration of migrations) {\n if (migration.to !== migration.from + 1) {\n throw new SchemaError(\n `Migration \"${migration.slug}\" ${String(migration.from)} to ${String(migration.to)} is not a single step. A migration moves an entry one version forward.`,\n )\n }\n if (migration.from < 1) {\n throw new SchemaError(\n `Migration \"${migration.slug}\" starts at version ${String(migration.from)}. The first stored version is 1.`,\n )\n }\n\n const found = bySlug.get(migration.slug) ?? []\n if (found.some((candidate) => candidate.from === migration.from)) {\n throw new SchemaError(\n `Two migrations of \"${migration.slug}\" both start at version ${String(migration.from)}.`,\n )\n }\n found.push(migration)\n bySlug.set(migration.slug, found)\n }\n\n for (const found of bySlug.values()) found.sort((left, right) => left.from - right.from)\n\n return {\n forType(slug) {\n return bySlug.get(slug) ?? []\n },\n\n plan(type, fromVersion) {\n const target = schemaVersionOf(type)\n if (fromVersion >= target) return []\n\n const steps: EntryMigration[] = []\n const available = bySlug.get(type.slug) ?? []\n\n for (let version = fromVersion; version < target; version += 1) {\n const step = available.find((candidate) => candidate.from === version)\n if (step !== undefined) steps.push(step)\n }\n\n return steps\n },\n }\n}\n\n/** Applies a plan to one entry's values. Exported so a dry run can show the result. */\nexport function applyPlan(\n plan: readonly EntryMigration[],\n values: Record<string, unknown>,\n context: Omit<MigrationContext, \"fromVersion\">,\n): Record<string, unknown> {\n return plan.reduce(\n (current, step) => step.migrate(current, { ...context, fromVersion: step.from }),\n values,\n )\n}\n\nexport interface MigrationFailure {\n entryId: string\n /** The version the entry was at when the attempt was made. */\n fromVersion: number\n reason: string\n}\n\nexport interface MigrationRunResult {\n slug: string\n /** The version every entry is being moved to: the type's current schema version. */\n targetVersion: number\n /** Entries looked at. */\n scanned: number\n /** Entries whose values were rewritten and whose version now equals the target. */\n migrated: number\n /**\n * Entries another runner had already moved between the read and the write. Not an error:\n * it is the conditional write doing its job.\n */\n skipped: number\n failed: readonly MigrationFailure[]\n /** True when entries are still behind, either because the cap was hit or a step failed. */\n remaining: boolean\n}\n\nexport interface MigrationRunOptions {\n /** Entries per batch. Bounded so one call cannot become an unbounded read. */\n batchSize?: number\n /** Batches per call. The run returns `remaining: true` when this is reached. */\n maxBatches?: number\n /** Called after each batch, so a long run can show progress. */\n onProgress?: (progress: { scanned: number; migrated: number }) => void\n}\n\nexport const MIGRATION_BATCH_SIZE = 50\nexport const MIGRATION_MAX_BATCHES = 20\n\nexport interface MigrationRunDeps {\n migrations: MigrationAdapter\n registry: MigrationRegistry\n type: ContentTypeLike\n}\n\n/**\n * Moves every entry of one content type up to its current schema version.\n *\n * The plan is computed per entry, from the version that entry is actually at, because a\n * collection can hold entries at several versions at once: one written before a change,\n * one written after, one left behind by an interrupted run.\n *\n * Failure behaviour, stated exactly. A transform that throws and a write that is refused\n * are the same thing here: the entry is named in `failed`, its stored document is\n * untouched, and the batch finishes so one bad entry does not hide the rest. The run then\n * stops, because the selection is \"everything still behind\" and a failing entry would be\n * picked up again on the next batch forever. `remaining` is true, the caller fixes the\n * transform or the data, and runs again; everything that already moved stays moved.\n */\nexport async function runMigration(\n deps: MigrationRunDeps,\n options: MigrationRunOptions = {},\n): Promise<MigrationRunResult> {\n const { migrations, registry, type } = deps\n const target = schemaVersionOf(type)\n const batchSize = Math.min(\n Math.max(Math.trunc(options.batchSize ?? MIGRATION_BATCH_SIZE), 1),\n MIGRATION_BATCH_SIZE,\n )\n const maxBatches = Math.max(Math.trunc(options.maxBatches ?? MIGRATION_MAX_BATCHES), 1)\n\n const failed: MigrationFailure[] = []\n let scanned = 0\n let migrated = 0\n let skipped = 0\n let remaining = false\n\n for (let batch = 0; batch < maxBatches; batch += 1) {\n const page = await migrations.listBehind(type.slug, target, { limit: batchSize })\n if (page.items.length === 0) break\n\n let progressed = false\n\n for (const record of page.items) {\n scanned += 1\n const outcome = await migrateOne(migrations, registry, type, target, record)\n\n if (outcome === \"migrated\") {\n migrated += 1\n progressed = true\n } else if (outcome === \"skipped\") {\n skipped += 1\n progressed = true\n } else {\n failed.push(outcome)\n }\n }\n\n options.onProgress?.({ scanned, migrated })\n\n // Nothing moved, so the same entries would be read again on the next pass. Stop and\n // say so rather than spin.\n if (!progressed) {\n remaining = true\n break\n }\n\n // A failure ends the run at the end of its batch. The selection is \"everything still\n // behind\", so a failing entry is read again by the next batch, and a systematic\n // failure would otherwise be reported once per batch for as long as the run lasted.\n if (failed.length > 0) break\n\n if (page.hasMore && batch === maxBatches - 1) remaining = true\n }\n\n if (failed.length > 0) remaining = true\n\n return {\n slug: type.slug,\n targetVersion: target,\n scanned,\n migrated,\n skipped,\n failed,\n remaining,\n }\n}\n\nasync function migrateOne(\n migrations: MigrationAdapter,\n registry: MigrationRegistry,\n type: ContentTypeLike,\n target: number,\n record: StoredEntryRecord,\n): Promise<\"migrated\" | \"skipped\" | MigrationFailure> {\n try {\n const plan = registry.plan(type, record.schemaVersion)\n if (plan.length === 0) {\n // Nothing to transform, but the stamp is behind: the shape did not change in a way\n // anybody wrote a migration for, so the entry only needs restamping.\n const stamped = await migrations.applyMigration(\n type.slug,\n record.id,\n record.values,\n {\n expectedVersion: record.schemaVersion,\n nextVersion: target,\n },\n )\n return stamped ? \"migrated\" : \"skipped\"\n }\n\n const values = applyPlan(plan, record.values, { entryId: record.id, slug: type.slug })\n const written = await migrations.applyMigration(type.slug, record.id, values, {\n expectedVersion: record.schemaVersion,\n nextVersion: target,\n })\n return written ? \"migrated\" : \"skipped\"\n } catch (thrown) {\n return {\n entryId: record.id,\n fromVersion: record.schemaVersion,\n reason: thrown instanceof Error ? thrown.message : String(thrown),\n }\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-77JDRXXU.js"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// src/schema/validate.ts
|
|
2
|
+
function isEmpty(value) {
|
|
3
|
+
if (value === null || value === void 0) return true;
|
|
4
|
+
if (typeof value === "string") return value.trim() === "";
|
|
5
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
function error(path, rule, message) {
|
|
9
|
+
return { path, rule, message };
|
|
10
|
+
}
|
|
11
|
+
function checkType(value, field, path, label) {
|
|
12
|
+
switch (field.type) {
|
|
13
|
+
case "text":
|
|
14
|
+
case "textarea":
|
|
15
|
+
case "slug":
|
|
16
|
+
case "date":
|
|
17
|
+
return typeof value === "string" ? null : error(path, "type", `${label} must be text.`);
|
|
18
|
+
case "number":
|
|
19
|
+
return typeof value === "number" && Number.isFinite(value) ? null : error(path, "type", `${label} must be a number.`);
|
|
20
|
+
case "boolean":
|
|
21
|
+
return typeof value === "boolean" ? null : error(path, "type", `${label} must be true or false.`);
|
|
22
|
+
case "select": {
|
|
23
|
+
const allowed = field.options.map((option) => option.value);
|
|
24
|
+
if (field.multiple === true) {
|
|
25
|
+
if (!Array.isArray(value)) {
|
|
26
|
+
return error(path, "type", `${label} must be a list of options.`);
|
|
27
|
+
}
|
|
28
|
+
const bad = value.find(
|
|
29
|
+
(entry) => typeof entry !== "string" || !allowed.includes(entry)
|
|
30
|
+
);
|
|
31
|
+
return bad === void 0 ? null : error(path, "option", `${label} does not allow the option ${String(bad)}.`);
|
|
32
|
+
}
|
|
33
|
+
return typeof value === "string" && allowed.includes(value) ? null : error(path, "option", `${label} does not allow the option ${String(value)}.`);
|
|
34
|
+
}
|
|
35
|
+
case "richtext":
|
|
36
|
+
return typeof value === "object" && value !== null && value.format === "husk-richtext-v1" ? null : error(path, "type", `${label} must be a rich text document.`);
|
|
37
|
+
case "image":
|
|
38
|
+
case "file":
|
|
39
|
+
return typeof value === "object" && value !== null && typeof value.mediaId === "string" ? null : error(path, "type", `${label} must reference an item in the media library.`);
|
|
40
|
+
case "relation":
|
|
41
|
+
if (field.many === true) {
|
|
42
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? null : error(path, "type", `${label} must be a list of entry references.`);
|
|
43
|
+
}
|
|
44
|
+
return typeof value === "string" ? null : error(path, "type", `${label} must be an entry reference.`);
|
|
45
|
+
case "repeater":
|
|
46
|
+
return Array.isArray(value) ? null : error(path, "type", `${label} must be a list.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function checkRules(value, rules, path, label) {
|
|
50
|
+
const errors = [];
|
|
51
|
+
const { minLength, maxLength, min, max, pattern } = rules;
|
|
52
|
+
const measurable = typeof value === "string" ? value.length : Array.isArray(value) ? value.length : null;
|
|
53
|
+
if (measurable !== null) {
|
|
54
|
+
if (minLength !== void 0 && measurable < minLength) {
|
|
55
|
+
errors.push(
|
|
56
|
+
error(
|
|
57
|
+
path,
|
|
58
|
+
"minLength",
|
|
59
|
+
typeof value === "string" ? `${label} must be at least ${minLength} characters.` : `${label} must have at least ${minLength} entries.`
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (maxLength !== void 0 && measurable > maxLength) {
|
|
64
|
+
errors.push(
|
|
65
|
+
error(
|
|
66
|
+
path,
|
|
67
|
+
"maxLength",
|
|
68
|
+
typeof value === "string" ? `${label} must be at most ${maxLength} characters.` : `${label} must have at most ${maxLength} entries.`
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (typeof value === "number") {
|
|
74
|
+
if (min !== void 0 && value < min) {
|
|
75
|
+
errors.push(error(path, "min", `${label} must be ${min} or more.`));
|
|
76
|
+
}
|
|
77
|
+
if (max !== void 0 && value > max) {
|
|
78
|
+
errors.push(error(path, "max", `${label} must be ${max} or less.`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (pattern !== void 0 && typeof value === "string") {
|
|
82
|
+
let regex;
|
|
83
|
+
try {
|
|
84
|
+
regex = new RegExp(pattern);
|
|
85
|
+
} catch {
|
|
86
|
+
errors.push(
|
|
87
|
+
error(
|
|
88
|
+
path,
|
|
89
|
+
"pattern",
|
|
90
|
+
`${label} has an invalid pattern in its schema: ${pattern}`
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
if (!regex.test(value)) {
|
|
96
|
+
errors.push(error(path, "pattern", `${label} has the wrong format.`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return errors;
|
|
100
|
+
}
|
|
101
|
+
function validateFieldMap(fields, values, prefix, options, root) {
|
|
102
|
+
const errors = [];
|
|
103
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
104
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
105
|
+
const label = field.label;
|
|
106
|
+
const rules = field.validation ?? {};
|
|
107
|
+
const value = values[key];
|
|
108
|
+
if (isEmpty(value)) {
|
|
109
|
+
if (rules.required === true && options.skipRequired !== true) {
|
|
110
|
+
errors.push(error(path, "required", `${label} is required.`));
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const typeError = checkType(value, field, path, label);
|
|
115
|
+
if (typeError) {
|
|
116
|
+
errors.push(typeError);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
errors.push(...checkRules(value, rules, path, label));
|
|
120
|
+
const spec = options.fieldTypes?.get(field.type);
|
|
121
|
+
if (spec) {
|
|
122
|
+
errors.push(
|
|
123
|
+
...spec.validate(
|
|
124
|
+
value,
|
|
125
|
+
rules,
|
|
126
|
+
{ path, contentType: root.type, entry: root.values },
|
|
127
|
+
field
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (field.type === "repeater" && Array.isArray(value)) {
|
|
132
|
+
value.forEach((row, index) => {
|
|
133
|
+
if (typeof row !== "object" || row === null || Array.isArray(row)) {
|
|
134
|
+
errors.push(
|
|
135
|
+
error(
|
|
136
|
+
`${path}.${index}`,
|
|
137
|
+
"type",
|
|
138
|
+
`${label} entry ${index + 1} is not valid.`
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
errors.push(
|
|
144
|
+
...validateFieldMap(
|
|
145
|
+
field.fields,
|
|
146
|
+
row,
|
|
147
|
+
`${path}.${index}`,
|
|
148
|
+
options,
|
|
149
|
+
root
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return errors;
|
|
156
|
+
}
|
|
157
|
+
function validateEntry(type, values, options = {}) {
|
|
158
|
+
return validateFieldMap(type.fields, values, "", options, { type, values });
|
|
159
|
+
}
|
|
160
|
+
function uniqueFieldPaths(type) {
|
|
161
|
+
return Object.entries(type.fields).filter(([, field]) => field.validation?.unique === true).map(([key]) => key);
|
|
162
|
+
}
|
|
163
|
+
var PATTERN_PRESETS = {
|
|
164
|
+
email: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$",
|
|
165
|
+
slug: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
166
|
+
url: "^https?://[^\\s]+$"
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export { PATTERN_PRESETS, uniqueFieldPaths, validateEntry };
|
|
170
|
+
//# sourceMappingURL=chunk-FHYWJCG6.js.map
|
|
171
|
+
//# sourceMappingURL=chunk-FHYWJCG6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema/validate.ts"],"names":[],"mappings":";AAiCA,SAAS,QAAQ,KAAA,EAAyB;AACxC,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW,OAAO,IAAA;AAClD,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,KAAA,CAAM,MAAK,KAAM,EAAA;AACvD,EAAA,IAAI,MAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,MAAM,MAAA,KAAW,CAAA;AAClD,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,KAAA,CAAM,IAAA,EAAc,IAAA,EAAc,OAAA,EAA6B;AACtE,EAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,OAAA,EAAQ;AAC/B;AAGA,SAAS,SAAA,CACP,KAAA,EACA,KAAA,EACA,IAAA,EACA,KAAA,EACmB;AACnB,EAAA,QAAQ,MAAM,IAAA;AAAM,IAClB,KAAK,MAAA;AAAA,IACL,KAAK,UAAA;AAAA,IACL,KAAK,MAAA;AAAA,IACL,KAAK,MAAA;AACH,MAAA,OAAO,OAAO,UAAU,QAAA,GACpB,IAAA,GACA,MAAM,IAAA,EAAM,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,cAAA,CAAgB,CAAA;AAAA,IAElD,KAAK,QAAA;AACH,MAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,GACrD,IAAA,GACA,KAAA,CAAM,IAAA,EAAM,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,kBAAA,CAAoB,CAAA;AAAA,IAEtD,KAAK,SAAA;AACH,MAAA,OAAO,OAAO,UAAU,SAAA,GACpB,IAAA,GACA,MAAM,IAAA,EAAM,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,uBAAA,CAAyB,CAAA;AAAA,IAE3D,KAAK,QAAA,EAAU;AACb,MAAA,MAAM,UAAoB,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAC,MAAA,KAAW,OAAO,KAAK,CAAA;AACpE,MAAA,IAAI,KAAA,CAAM,aAAa,IAAA,EAAM;AAC3B,QAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACzB,UAAA,OAAO,KAAA,CAAM,IAAA,EAAM,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,2BAAA,CAA6B,CAAA;AAAA,QAClE;AACA,QAAA,MAAM,MAAM,KAAA,CAAM,IAAA;AAAA,UAChB,CAAC,UAAU,OAAO,KAAA,KAAU,YAAY,CAAC,OAAA,CAAQ,SAAS,KAAK;AAAA,SACjE;AACA,QAAA,OAAO,GAAA,KAAQ,MAAA,GACX,IAAA,GACA,KAAA,CAAM,IAAA,EAAM,QAAA,EAAU,CAAA,EAAG,KAAK,CAAA,2BAAA,EAA8B,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,MAChF;AACA,MAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,OAAA,CAAQ,QAAA,CAAS,KAAK,CAAA,GACtD,IAAA,GACA,KAAA,CAAM,IAAA,EAAM,UAAU,CAAA,EAAG,KAAK,8BAA8B,MAAA,CAAO,KAAK,CAAC,CAAA,CAAA,CAAG,CAAA;AAAA,IAClF;AAAA,IAEA,KAAK,UAAA;AACH,MAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IACtB,KAAA,KAAU,QACT,KAAA,CAA+B,MAAA,KAAW,kBAAA,GACzC,IAAA,GACA,KAAA,CAAM,IAAA,EAAM,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,8BAAA,CAAgC,CAAA;AAAA,IAElE,KAAK,OAAA;AAAA,IACL,KAAK,MAAA;AACH,MAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IACtB,KAAA,KAAU,QACV,OAAQ,KAAA,CAAgC,OAAA,KAAY,QAAA,GAClD,OACA,KAAA,CAAM,IAAA,EAAM,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,6CAAA,CAA+C,CAAA;AAAA,IAEjF,KAAK,UAAA;AACH,MAAA,IAAI,KAAA,CAAM,SAAS,IAAA,EAAM;AACvB,QAAA,OAAO,MAAM,OAAA,CAAQ,KAAK,KAAK,KAAA,CAAM,KAAA,CAAM,CAAC,KAAA,KAAU,OAAO,KAAA,KAAU,QAAQ,IAC3E,IAAA,GACA,KAAA,CAAM,MAAM,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,oCAAA,CAAsC,CAAA;AAAA,MACxE;AACA,MAAA,OAAO,OAAO,UAAU,QAAA,GACpB,IAAA,GACA,MAAM,IAAA,EAAM,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,4BAAA,CAA8B,CAAA;AAAA,IAEhE,KAAK,UAAA;AACH,MAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,GAAI,IAAA,GAAO,MAAM,IAAA,EAAM,MAAA,EAAQ,CAAA,EAAG,KAAK,CAAA,gBAAA,CAAkB,CAAA;AAAA;AAEzF;AAEA,SAAS,UAAA,CACP,KAAA,EACA,KAAA,EACA,IAAA,EACA,KAAA,EACc;AACd,EAAA,MAAM,SAAuB,EAAC;AAC9B,EAAA,MAAM,EAAE,SAAA,EAAW,SAAA,EAAW,GAAA,EAAK,GAAA,EAAK,SAAQ,GAAI,KAAA;AAEpD,EAAA,MAAM,UAAA,GACJ,OAAO,KAAA,KAAU,QAAA,GAAW,KAAA,CAAM,MAAA,GAAS,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,GAAI,KAAA,CAAM,MAAA,GAAS,IAAA;AAEnF,EAAA,IAAI,eAAe,IAAA,EAAM;AACvB,IAAA,IAAI,SAAA,KAAc,MAAA,IAAa,UAAA,GAAa,SAAA,EAAW;AACrD,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,KAAA;AAAA,UACE,IAAA;AAAA,UACA,WAAA;AAAA,UACA,OAAO,KAAA,KAAU,QAAA,GACb,CAAA,EAAG,KAAK,CAAA,kBAAA,EAAqB,SAAS,CAAA,YAAA,CAAA,GACtC,CAAA,EAAG,KAAK,CAAA,oBAAA,EAAuB,SAAS,CAAA,SAAA;AAAA;AAC9C,OACF;AAAA,IACF;AACA,IAAA,IAAI,SAAA,KAAc,MAAA,IAAa,UAAA,GAAa,SAAA,EAAW;AACrD,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,KAAA;AAAA,UACE,IAAA;AAAA,UACA,WAAA;AAAA,UACA,OAAO,KAAA,KAAU,QAAA,GACb,CAAA,EAAG,KAAK,CAAA,iBAAA,EAAoB,SAAS,CAAA,YAAA,CAAA,GACrC,CAAA,EAAG,KAAK,CAAA,mBAAA,EAAsB,SAAS,CAAA,SAAA;AAAA;AAC7C,OACF;AAAA,IACF;AAAA,EACF;AAEA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,IAAI,GAAA,KAAQ,MAAA,IAAa,KAAA,GAAQ,GAAA,EAAK;AACpC,MAAA,MAAA,CAAO,IAAA,CAAK,MAAM,IAAA,EAAM,KAAA,EAAO,GAAG,KAAK,CAAA,SAAA,EAAY,GAAG,CAAA,SAAA,CAAW,CAAC,CAAA;AAAA,IACpE;AACA,IAAA,IAAI,GAAA,KAAQ,MAAA,IAAa,KAAA,GAAQ,GAAA,EAAK;AACpC,MAAA,MAAA,CAAO,IAAA,CAAK,MAAM,IAAA,EAAM,KAAA,EAAO,GAAG,KAAK,CAAA,SAAA,EAAY,GAAG,CAAA,SAAA,CAAW,CAAC,CAAA;AAAA,IACpE;AAAA,EACF;AAEA,EAAA,IAAI,OAAA,KAAY,MAAA,IAAa,OAAO,KAAA,KAAU,QAAA,EAAU;AACtD,IAAA,IAAI,KAAA;AACJ,IAAA,IAAI;AACF,MAAA,KAAA,GAAQ,IAAI,OAAO,OAAO,CAAA;AAAA,IAC5B,CAAA,CAAA,MAAQ;AACN,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,KAAA;AAAA,UACE,IAAA;AAAA,UACA,SAAA;AAAA,UACA,CAAA,EAAG,KAAK,CAAA,uCAAA,EAA0C,OAAO,CAAA;AAAA;AAC3D,OACF;AACA,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,IAAI,CAAC,KAAA,CAAM,IAAA,CAAK,KAAK,CAAA,EAAG;AACtB,MAAA,MAAA,CAAO,KAAK,KAAA,CAAM,IAAA,EAAM,WAAW,CAAA,EAAG,KAAK,wBAAwB,CAAC,CAAA;AAAA,IACtE;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;AAQA,SAAS,gBAAA,CACP,MAAA,EACA,MAAA,EACA,MAAA,EACA,SACA,IAAA,EACc;AACd,EAAA,MAAM,SAAuB,EAAC;AAE9B,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AACjD,IAAA,MAAM,OAAO,MAAA,GAAS,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,GAAK,GAAA;AAC3C,IAAA,MAAM,QAAQ,KAAA,CAAM,KAAA;AACpB,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,UAAA,IAAc,EAAC;AACnC,IAAA,MAAM,KAAA,GAAQ,OAAO,GAAG,CAAA;AAExB,IAAA,IAAI,OAAA,CAAQ,KAAK,CAAA,EAAG;AAClB,MAAA,IAAI,KAAA,CAAM,QAAA,KAAa,IAAA,IAAQ,OAAA,CAAQ,iBAAiB,IAAA,EAAM;AAC5D,QAAA,MAAA,CAAO,KAAK,KAAA,CAAM,IAAA,EAAM,YAAY,CAAA,EAAG,KAAK,eAAe,CAAC,CAAA;AAAA,MAC9D;AAGA,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,SAAA,GAAY,SAAA,CAAU,KAAA,EAAO,KAAA,EAAO,MAAM,KAAK,CAAA;AACrD,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,MAAA,CAAO,KAAK,SAAS,CAAA;AACrB,MAAA;AAAA,IACF;AAEA,IAAA,MAAA,CAAO,KAAK,GAAG,UAAA,CAAW,OAAO,KAAA,EAAO,IAAA,EAAM,KAAK,CAAC,CAAA;AAEpD,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,UAAA,EAAY,GAAA,CAAI,MAAM,IAAI,CAAA;AAC/C,IAAA,IAAI,IAAA,EAAM;AACR,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,GAAG,IAAA,CAAK,QAAA;AAAA,UACN,KAAA;AAAA,UACA,KAAA;AAAA,UACA,EAAE,IAAA,EAAM,WAAA,EAAa,KAAK,IAAA,EAAM,KAAA,EAAO,KAAK,MAAA,EAAO;AAAA,UACnD;AAAA;AACF,OACF;AAAA,IACF;AAEA,IAAA,IAAI,MAAM,IAAA,KAAS,UAAA,IAAc,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACrD,MAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,GAAA,EAAc,KAAA,KAAkB;AAC7C,QAAA,IAAI,OAAO,QAAQ,QAAA,IAAY,GAAA,KAAQ,QAAQ,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA,EAAG;AACjE,UAAA,MAAA,CAAO,IAAA;AAAA,YACL,KAAA;AAAA,cACE,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA;AAAA,cAChB,MAAA;AAAA,cACA,CAAA,EAAG,KAAK,CAAA,OAAA,EAAU,KAAA,GAAQ,CAAC,CAAA,cAAA;AAAA;AAC7B,WACF;AACA,UAAA;AAAA,QACF;AACA,QAAA,MAAA,CAAO,IAAA;AAAA,UACL,GAAG,gBAAA;AAAA,YACD,KAAA,CAAM,MAAA;AAAA,YACN,GAAA;AAAA,YACA,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA;AAAA,YAChB,OAAA;AAAA,YACA;AAAA;AACF,SACF;AAAA,MACF,CAAC,CAAA;AAAA,IACH;AAAA,EACF;AAMA,EAAA,OAAO,MAAA;AACT;AAEO,SAAS,aAAA,CACd,IAAA,EACA,MAAA,EACA,OAAA,GAA2B,EAAC,EACd;AACd,EAAA,OAAO,gBAAA,CAAiB,KAAK,MAAA,EAAQ,MAAA,EAAQ,IAAI,OAAA,EAAS,EAAE,IAAA,EAAM,MAAA,EAAQ,CAAA;AAC5E;AAOO,SAAS,iBAAiB,IAAA,EAAqC;AACpE,EAAA,OAAO,MAAA,CAAO,QAAQ,IAAA,CAAK,MAAM,EAC9B,MAAA,CAAO,CAAC,GAAG,KAAK,MAAM,KAAA,CAAM,UAAA,EAAY,WAAW,IAAI,CAAA,CACvD,IAAI,CAAC,CAAC,GAAG,CAAA,KAAM,GAAG,CAAA;AACvB;AAGO,IAAM,eAAA,GAAkB;AAAA,EAC7B,KAAA,EAAO,gCAAA;AAAA,EACP,IAAA,EAAM,4BAAA;AAAA,EACN,GAAA,EAAK;AACP","file":"chunk-FHYWJCG6.js","sourcesContent":["import type { FieldTypeRegistry } from \"./field-type.js\"\nimport type { AnyContentTypeShape, FieldError } from \"../types/entry.js\"\nimport type { FieldDefinition, FieldMap, ValidationRules } from \"../types/field.js\"\n\n/**\n * One validation pass over an entry.\n *\n * This is the only validator in the product. The admin form runs it on change and on\n * submit; the adapter runs it again before every write. A rule enforced in one place only\n * is a rule that is not enforced.\n *\n * `unique` cannot be answered without reading other entries, so it is not checked here.\n * `uniqueFieldPaths()` lists the fields the adapter must check, and the adapter reports the\n * failure with the same `FieldError` shape, so the form renders it identically.\n */\nexport interface ValidateOptions {\n /**\n * Skip `required`. Used when saving a draft, where an incomplete entry is the point.\n * Every other rule still applies: a value that is present must still be valid.\n */\n skipRequired?: boolean\n /**\n * The registered field types, so each field's own `validate` runs alongside the shared\n * rules. Optional, because the rules below stand on their own and a database-defined type\n * using an unregistered field type must still validate rather than throw.\n *\n * The two halves do not overlap by construction: everything expressible as a\n * `ValidationRules` key is checked here, once, and a spec adds only what its own field\n * definition knows (a number's `step`, a date's precision, a slug's shape).\n */\n fieldTypes?: FieldTypeRegistry\n}\n\nfunction isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === \"string\") return value.trim() === \"\"\n if (Array.isArray(value)) return value.length === 0\n return false\n}\n\nfunction error(path: string, rule: string, message: string): FieldError {\n return { path, rule, message }\n}\n\n/** Generic shape check, independent of the field type specs that arrive in M5. */\nfunction checkType(\n value: unknown,\n field: FieldDefinition,\n path: string,\n label: string,\n): FieldError | null {\n switch (field.type) {\n case \"text\":\n case \"textarea\":\n case \"slug\":\n case \"date\":\n return typeof value === \"string\"\n ? null\n : error(path, \"type\", `${label} must be text.`)\n\n case \"number\":\n return typeof value === \"number\" && Number.isFinite(value)\n ? null\n : error(path, \"type\", `${label} must be a number.`)\n\n case \"boolean\":\n return typeof value === \"boolean\"\n ? null\n : error(path, \"type\", `${label} must be true or false.`)\n\n case \"select\": {\n const allowed: string[] = field.options.map((option) => option.value)\n if (field.multiple === true) {\n if (!Array.isArray(value)) {\n return error(path, \"type\", `${label} must be a list of options.`)\n }\n const bad = value.find(\n (entry) => typeof entry !== \"string\" || !allowed.includes(entry),\n )\n return bad === undefined\n ? null\n : error(path, \"option\", `${label} does not allow the option ${String(bad)}.`)\n }\n return typeof value === \"string\" && allowed.includes(value)\n ? null\n : error(path, \"option\", `${label} does not allow the option ${String(value)}.`)\n }\n\n case \"richtext\":\n return typeof value === \"object\" &&\n value !== null &&\n (value as { format?: unknown }).format === \"husk-richtext-v1\"\n ? null\n : error(path, \"type\", `${label} must be a rich text document.`)\n\n case \"image\":\n case \"file\":\n return typeof value === \"object\" &&\n value !== null &&\n typeof (value as { mediaId?: unknown }).mediaId === \"string\"\n ? null\n : error(path, \"type\", `${label} must reference an item in the media library.`)\n\n case \"relation\":\n if (field.many === true) {\n return Array.isArray(value) && value.every((entry) => typeof entry === \"string\")\n ? null\n : error(path, \"type\", `${label} must be a list of entry references.`)\n }\n return typeof value === \"string\"\n ? null\n : error(path, \"type\", `${label} must be an entry reference.`)\n\n case \"repeater\":\n return Array.isArray(value) ? null : error(path, \"type\", `${label} must be a list.`)\n }\n}\n\nfunction checkRules(\n value: unknown,\n rules: ValidationRules,\n path: string,\n label: string,\n): FieldError[] {\n const errors: FieldError[] = []\n const { minLength, maxLength, min, max, pattern } = rules\n\n const measurable =\n typeof value === \"string\" ? value.length : Array.isArray(value) ? value.length : null\n\n if (measurable !== null) {\n if (minLength !== undefined && measurable < minLength) {\n errors.push(\n error(\n path,\n \"minLength\",\n typeof value === \"string\"\n ? `${label} must be at least ${minLength} characters.`\n : `${label} must have at least ${minLength} entries.`,\n ),\n )\n }\n if (maxLength !== undefined && measurable > maxLength) {\n errors.push(\n error(\n path,\n \"maxLength\",\n typeof value === \"string\"\n ? `${label} must be at most ${maxLength} characters.`\n : `${label} must have at most ${maxLength} entries.`,\n ),\n )\n }\n }\n\n if (typeof value === \"number\") {\n if (min !== undefined && value < min) {\n errors.push(error(path, \"min\", `${label} must be ${min} or more.`))\n }\n if (max !== undefined && value > max) {\n errors.push(error(path, \"max\", `${label} must be ${max} or less.`))\n }\n }\n\n if (pattern !== undefined && typeof value === \"string\") {\n let regex: RegExp\n try {\n regex = new RegExp(pattern)\n } catch {\n errors.push(\n error(\n path,\n \"pattern\",\n `${label} has an invalid pattern in its schema: ${pattern}`,\n ),\n )\n return errors\n }\n if (!regex.test(value)) {\n errors.push(error(path, \"pattern\", `${label} has the wrong format.`))\n }\n }\n\n return errors\n}\n\n/** The whole entry, kept for the cross-field context a spec's `validate` receives. */\ninterface ValidateRoot {\n type: AnyContentTypeShape\n values: Record<string, unknown>\n}\n\nfunction validateFieldMap(\n fields: FieldMap,\n values: Record<string, unknown>,\n prefix: string,\n options: ValidateOptions,\n root: ValidateRoot,\n): FieldError[] {\n const errors: FieldError[] = []\n\n for (const [key, field] of Object.entries(fields)) {\n const path = prefix ? `${prefix}.${key}` : key\n const label = field.label\n const rules = field.validation ?? {}\n const value = values[key]\n\n if (isEmpty(value)) {\n if (rules.required === true && options.skipRequired !== true) {\n errors.push(error(path, \"required\", `${label} is required.`))\n }\n // Nothing else is meaningful about an empty value. An optional empty field is valid\n // by definition, and a required empty one has already been reported.\n continue\n }\n\n const typeError = checkType(value, field, path, label)\n if (typeError) {\n errors.push(typeError)\n continue\n }\n\n errors.push(...checkRules(value, rules, path, label))\n\n const spec = options.fieldTypes?.get(field.type)\n if (spec) {\n errors.push(\n ...spec.validate(\n value,\n rules,\n { path, contentType: root.type, entry: root.values },\n field,\n ),\n )\n }\n\n if (field.type === \"repeater\" && Array.isArray(value)) {\n value.forEach((row: unknown, index: number) => {\n if (typeof row !== \"object\" || row === null || Array.isArray(row)) {\n errors.push(\n error(\n `${path}.${index}`,\n \"type\",\n `${label} entry ${index + 1} is not valid.`,\n ),\n )\n return\n }\n errors.push(\n ...validateFieldMap(\n field.fields,\n row as Record<string, unknown>,\n `${path}.${index}`,\n options,\n root,\n ),\n )\n })\n }\n }\n\n // Values for fields the schema no longer declares are left alone on purpose. M8 removes a\n // field by marking it removed and keeping its data until an explicit purge, so an unknown\n // key is expected state, not an error.\n\n return errors\n}\n\nexport function validateEntry(\n type: AnyContentTypeShape,\n values: Record<string, unknown>,\n options: ValidateOptions = {},\n): FieldError[] {\n return validateFieldMap(type.fields, values, \"\", options, { type, values })\n}\n\n/**\n * Field keys carrying `unique`, for the adapter to check against stored entries. Fields\n * nested in a repeater are excluded: uniqueness across repeater rows is not a constraint\n * this CMS offers, and pretending otherwise would need a query no index supports.\n */\nexport function uniqueFieldPaths(type: AnyContentTypeShape): string[] {\n return Object.entries(type.fields)\n .filter(([, field]) => field.validation?.unique === true)\n .map(([key]) => key)\n}\n\n/** Patterns the Schema Builder offers as presets, so editors do not hand-write regexes. */\nexport const PATTERN_PRESETS = {\n email: \"^[^\\\\s@]+@[^\\\\s@]+\\\\.[^\\\\s@]+$\",\n slug: \"^[a-z0-9]+(?:-[a-z0-9]+)*$\",\n url: \"^https?://[^\\\\s]+$\",\n} as const\n"]}
|