@murumets-ee/content 0.78.0 → 0.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { r as ChromePlacement } from "./site-chrome-CpnNlaw5.mjs";
1
+ import { r as ChromePlacement } from "./site-chrome-CUE16iIz.mjs";
2
2
  import { ToolkitApp } from "@murumets-ee/core";
3
3
 
4
4
  //#region src/chrome-provider.d.ts
@@ -1,2 +1,2 @@
1
- import{t as e}from"./chrome-entity-names-zYJzP-_U.mjs";import{t}from"./site-chrome-BhlkbRuq.mjs";import{getBlockTreeFieldName as n}from"@murumets-ee/entity";import{composeChromeRegion as r,stripScopeTags as i}from"@murumets-ee/blocks/render";const a=t;async function o(e,t){let n=e.document&&(!t.claim||t.claim())?await t.readOverrides(e.document.entityType,e.document.id,e.locale):null,o={header:null,footer:null};for(let s of a){if(t.claim&&!t.claim())continue;let a=await t.readChromeDocument(s,e.locale);if(!a)continue;let c={id:a.id,blocks:a.blocks};try{let e=r(c,n);if(!e)continue;o[s]={blocks:i(e.blocks),overridden:e.overridden}}catch(n){t.logger?.warn({region:s,documentId:c.id,locale:e.locale,err:n},`content: chrome region failed to compose (malformed stored blocks, or strip depth exceeded) — serving no chrome for this region`)}}return a.some(e=>o[e]!==null)?o:null}function s(t){return{entity:e,getChrome:async r=>{let i=await t(),{createQueryClient:a}=await import(`@murumets-ee/core/clients`),{schemaRegistry:s}=await import(`@murumets-ee/db`),{eq:c,getTableColumns:l}=await import(`drizzle-orm`),u=e=>{let t=s.get(e);if(t)return l(t)},d=async(e,t,n,r)=>{let o=i.entities.get(e);return o?(await a(o).findMany({limit:1,locale:t,...n!==void 0&&{where:n},...r!==void 0&&{exposeInternalFields:r}}))[0]??null:null},f=i.entities.get(e),p=f?n(f):void 0,m=u(e)?.placement;return!p||!m?null:o(r,{claim:()=>r.budget.claim(`block`,1)>0,readChromeDocument:async(t,n)=>{let r=await d(e,n,c(m,t));if(!r)return null;let i=r[p];return{id:typeof r.id==`string`?r.id:e,blocks:Array.isArray(i)?i:[]}},readOverrides:async(e,t,n)=>{let r=u(e)?.id;if(!r)return null;let i=await d(e,n,c(r,t),[`_overrides`]);if(!i)return null;let a=i._overrides;return a&&typeof a==`object`&&!Array.isArray(a)?a:null},logger:i.logger})}}}export{o as composeChromeRegions,s as createChromeProvider};
1
+ import{t as e}from"./chrome-entity-names-zYJzP-_U.mjs";import{t}from"./site-chrome-BUEOhmyf.mjs";import{getBlockTreeFieldName as n}from"@murumets-ee/entity";import{composeChromeRegion as r,stripScopeTags as i}from"@murumets-ee/blocks/render";const a=t;async function o(e,t){let n=e.document&&(!t.claim||t.claim())?await t.readOverrides(e.document.entityType,e.document.id,e.locale):null,o={header:null,footer:null};for(let s of a){if(t.claim&&!t.claim())continue;let a=await t.readChromeDocument(s,e.locale);if(!a)continue;let c={id:a.id,blocks:a.blocks};try{let e=r(c,n);if(!e)continue;o[s]={blocks:i(e.blocks),overridden:e.overridden}}catch(n){t.logger?.warn({region:s,documentId:c.id,locale:e.locale,err:n},`content: chrome region failed to compose (malformed stored blocks, or strip depth exceeded) — serving no chrome for this region`)}}return a.some(e=>o[e]!==null)?o:null}function s(t){return{entity:e,getChrome:async r=>{let i=await t(),{createQueryClient:a}=await import(`@murumets-ee/core/clients`),{schemaRegistry:s}=await import(`@murumets-ee/db`),{eq:c,getTableColumns:l}=await import(`drizzle-orm`),u=e=>{let t=s.get(e);if(t)return l(t)},d=async(e,t,n,r)=>{let o=i.entities.get(e);return o?(await a(o).findMany({limit:1,locale:t,...n!==void 0&&{where:n},...r!==void 0&&{exposeInternalFields:r}}))[0]??null:null},f=i.entities.get(e),p=f?n(f):void 0,m=u(e)?.placement;return!p||!m?null:o(r,{claim:()=>r.budget.claim(`block`,1)>0,readChromeDocument:async(t,n)=>{let r=await d(e,n,c(m,t));if(!r)return null;let i=r[p];return{id:typeof r.id==`string`?r.id:e,blocks:Array.isArray(i)?i:[]}},readOverrides:async(e,t,n)=>{let r=u(e)?.id;if(!r)return null;let i=await d(e,n,c(r,t),[`_overrides`]);if(!i)return null;let a=i._overrides;return a&&typeof a==`object`&&!Array.isArray(a)?a:null},logger:i.logger})}}}export{o as composeChromeRegions,s as createChromeProvider};
2
2
  //# sourceMappingURL=chrome-provider.mjs.map
@@ -1,2 +1,2 @@
1
- import { a as siteChromeEntity, i as SiteChromeEntityOptions, n as CHROME_PLACEMENT_LABELS, r as ChromePlacement, t as CHROME_PLACEMENTS } from "../site-chrome-CpnNlaw5.mjs";
1
+ import { a as siteChromeEntity, i as SiteChromeEntityOptions, n as CHROME_PLACEMENT_LABELS, r as ChromePlacement, t as CHROME_PLACEMENTS } from "../site-chrome-CUE16iIz.mjs";
2
2
  export { CHROME_PLACEMENTS, CHROME_PLACEMENT_LABELS, type ChromePlacement, type SiteChromeEntityOptions, siteChromeEntity };
@@ -1 +1 @@
1
- import{n as e,r as t,t as n}from"../site-chrome-BhlkbRuq.mjs";export{n as CHROME_PLACEMENTS,e as CHROME_PLACEMENT_LABELS,t as siteChromeEntity};
1
+ import{n as e,r as t,t as n}from"../site-chrome-BUEOhmyf.mjs";export{n as CHROME_PLACEMENTS,e as CHROME_PLACEMENT_LABELS,t as siteChromeEntity};
package/dist/seed.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{t as e}from"./chrome-entity-names-zYJzP-_U.mjs";import{i as t,n}from"./schema-D8sBQozw.mjs";import{n as r,t as i}from"./site-chrome-BhlkbRuq.mjs";import{getApp as a}from"@murumets-ee/core";import{eq as o}from"drizzle-orm";import{createAdminClient as s}from"@murumets-ee/core/clients";import{randomUUID as c}from"node:crypto";const l={header:{handle:`main`},footer:{handle:`footer`}};function u(e){return[{id:c(),type:`menu`,props:{handle:l[e].handle,orientation:`horizontal`},traits:{}}]}async function d(){let n=a().entities.get(e);if(!n)return{created:0};let c=s(n),l=0,d=c.getTable().placement;if(!d)throw Error(`${e} has no "placement" column — the chrome entity registered by this app is not the one @murumets-ee/content ships.`);for(let e of i)if(!((await c.findMany({where:o(d,e),limit:1})).length>0))try{await c.create({placement:e,label:r[e],layout:u(e)}),l++}catch(e){if(!t(e))throw e}return{created:l}}async function f(e){let t=n.makeClient(a().db.readWrite),r={};for(let n of e)r[n]=(await t.deleteMany({entityType:n})).length;return r}export{f as forgetEntityPaths,d as seedSiteChrome};
1
+ import{t as e}from"./chrome-entity-names-zYJzP-_U.mjs";import{i as t,n}from"./schema-D8sBQozw.mjs";import{n as r,t as i}from"./site-chrome-BUEOhmyf.mjs";import{getApp as a}from"@murumets-ee/core";import{eq as o}from"drizzle-orm";import{createAdminClient as s}from"@murumets-ee/core/clients";import{randomUUID as c}from"node:crypto";const l={header:{handle:`main`},footer:{handle:`footer`}};function u(e){return[{id:c(),type:`menu`,props:{handle:l[e].handle,orientation:`horizontal`},traits:{}}]}async function d(){let n=a().entities.get(e);if(!n)return{created:0};let c=s(n),l=0,d=c.getTable().placement;if(!d)throw Error(`${e} has no "placement" column — the chrome entity registered by this app is not the one @murumets-ee/content ships.`);for(let e of i)if(!((await c.findMany({where:o(d,e),limit:1})).length>0))try{await c.create({placement:e,label:r[e],layout:u(e)}),l++}catch(e){if(!t(e))throw e}return{created:l}}async function f(e){let t=n.makeClient(a().db.readWrite),r={};for(let n of e)r[n]=(await t.deleteMany({entityType:n})).length;return r}export{f as forgetEntityPaths,d as seedSiteChrome};
2
2
  //# sourceMappingURL=seed.mjs.map
@@ -0,0 +1,2 @@
1
+ import{n as e}from"./versionable-Br_G5VGR.mjs";import{auditable as t,defineEntity as n,field as r,publishable as i}from"@murumets-ee/entity";const a=[`header`,`footer`],o={header:`Site header`,footer:`Site footer`},s={label:{label:`Name`,description:`Shown in the admin list and as the editor title. Visitors never see it.`}};function c(e){let t={...s};for(let[n,r]of Object.entries(e??{}))t[n]={...t[n],...r};return t}function l(o){let s=o?.viewRole??`editor`,l=o?.updateRole??`editor`,u=o?.adminRole??`admin`,d=o?.maxVersions??30;return n({name:`site_chrome`,kind:`collection`,fields:{placement:r.select({options:a,required:!0,unique:!0}),label:r.text({required:!0}),layout:r.blockTree({translatable:!0,...o?.environment!==void 0&&{environment:o.environment}})},behaviors:[t(),i(),e({maxPerDoc:d}),...o?.behaviors??[]],scope:`global`,access:{view:s,create:u,update:l,delete:u},admin:{icon:`panel-top`,description:`Blocks rendered around every page that does not override them.`,rootFields:[`label`],fieldOverrides:c(o?.admin?.fieldOverrides),titleField:`label`,group:`Site`,disableCreate:!0}})}l().name;export{o as n,l as r,a as t};
2
+ //# sourceMappingURL=site-chrome-BUEOhmyf.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-chrome-BUEOhmyf.mjs","names":[],"sources":["../src/entities/site-chrome.ts"],"sourcesContent":["/**\n * `site_chrome` — the block documents that hold the site chrome, one row per\n * PLACEMENT, so a header is edited on the same canvas as the page it sits\n * above instead of being typed into a JSON textarea.\n *\n * ## Placement, not type\n *\n * There is ONE entity. A chrome document says where it appears by carrying a\n * `placement` value, and `'header'` / `'footer'` are exactly the region keys\n * `GET /v1/_chrome` emits — so the provider maps placement to region by\n * identity rather than through a lookup table. That is the whole of the model.\n *\n * It replaces three entities (`site_header`, `site_footer`, and a single-row\n * site layout) and six hardcoded name lists, the most consequential of which\n * was not a list of names at all: `apps/admin/lib/chrome-documents.ts` carried\n * a module constant pairing each chrome entity with a\n * `position: 'before' | 'after'`. That was the placement flag, written in code,\n * in the app, where no operator could reach it and no plugin could extend it.\n *\n * ## What survives, and why it had to\n *\n * **A separate ROW per placement.** `publishable()` and `versionable()` are\n * per-row, so a header and a footer sharing one row could not be published or\n * rolled back independently. That constraint is real and this model honours\n * it — it is only the entity TYPE that collapses, never the row.\n *\n * **Peer-document editing.** The editor composes header, page and footer as\n * peers with per-document undo (`plan/site-chrome` D001 / D003). It never\n * keyed on the entity NAME to do that: `DocumentSlice.entity` is the only\n * discriminator and `pageDocumentId` is explicit state, precisely so that no\n * entity-name allowlist is needed. One name in place of two costs it nothing.\n *\n * ## Cardinality — why `kind: 'collection'` and a unique column\n *\n * `kind: 'singleton'` is enforced now (`plan/site-chrome` D011), and\n * `assertSingletonSlotFree` counts every live row of an entity — so an entity\n * holding a header row AND a footer row trips the gate on the second insert.\n * The kind and the collapse are mutually exclusive, which is why phase 09 lists\n * \"the singleton kind\" among the things that go.\n *\n * `placement`'s `unique: true` is what takes over the half that mattered: the\n * rule becomes one row per placement, enforced at the DATABASE for every\n * writer rather than in one client. On a `trashable()` entity — which this is,\n * by auto-enable, because it is publishable — the schema builder re-emits that\n * as a PARTIAL unique index (`WHERE \"deletedAt\" IS NULL`), so trashing a\n * header frees its placement for a replacement instead of wedging it.\n *\n * What is NOT carried over is the singleton DELETE rule (\"never leave zero\").\n * Deleting the only header is now possible, and the site falls back to the\n * delivery template's own markup — the same thing it does before the first\n * header is ever created. `trashable()` makes it recoverable.\n *\n * ## `admin.disableCreate` is correctness, not a workaround\n *\n * The block-editor create branch auto-POSTs `{ title: 'Untitled' }` and\n * nothing else, so any entity on that branch with a second required field\n * would reject on `/new`. `placement` is exactly such a field.\n *\n * Suppressing the affordance is also the honest answer: a third chrome row\n * would render NOWHERE, because the delivery template has exactly two chrome\n * slots. A new placement is a code change on both sides — a `placement` option\n * here and a slot in `BaseLayout.astro` — and that coupling is what keeps the\n * enum from accumulating values nothing renders.\n *\n * The two rows a site needs are created by the `content:site-chrome` seeder,\n * which `lumi migrate` runs automatically as a `bootstrap` seeder. Nobody has\n * to press anything.\n *\n * ## Wiring\n *\n * @example\n * ```ts\n * // apps/<app>/entities/site-chrome.ts\n * import { withOpLog } from '@murumets-ee/blocks/server'\n * import { siteChromeEntity } from '@murumets-ee/content/entities'\n *\n * export const SiteChrome = siteChromeEntity({ environment: 'page' })\n * .with(withOpLog()) // see below — the app layers this on\n * ```\n *\n * `withOpLog()` is deliberately NOT in the factory: it ships from\n * `@murumets-ee/blocks/server`, and `@murumets-ee/content` must not depend on\n * `@murumets-ee/blocks` (blocks depends back on content). The app layers it at\n * config time. **Without it the block editor 404s** — `editor-state` refuses a\n * document whose entity has no op-log behavior — so an app that registers this\n * and skips it gets a chrome document it cannot open.\n *\n * Behaviors the factory does apply:\n *\n * - `auditable()` — created/updated stamps\n * - `publishable()` — a header is drafted and published like any document,\n * which is the point of it being one\n * - `versionable({ maxPerDoc })` — per-locale history\n */\n\nimport type { Behavior, EntityAdminConfig } from '@murumets-ee/entity'\nimport { auditable, defineEntity, field, publishable } from '@murumets-ee/entity'\nimport { versionable } from '../behaviors/versionable.js'\n\n/**\n * Where a chrome document appears on every page that does not override it.\n *\n * These strings ARE the `/v1/_chrome` region keys and the delivery template's\n * slot names. Adding one means adding a slot to `BaseLayout.astro` in the same\n * change, or the value names a region nothing renders.\n */\nexport const CHROME_PLACEMENTS = ['header', 'footer'] as const\n\n/** One placement. Also a region name on the wire — same string, by design. */\nexport type ChromePlacement = (typeof CHROME_PLACEMENTS)[number]\n\n/** The default label a bootstrap row gets, per placement. */\nexport const CHROME_PLACEMENT_LABELS: Readonly<Record<ChromePlacement, string>> = {\n header: 'Site header',\n footer: 'Site footer',\n}\n\n/** Optional overrides apps can apply when calling the factory. */\nexport interface SiteChromeEntityOptions {\n /**\n * Roles allowed to read the document. Default `'editor'`.\n *\n * ADVISORY entity-level metadata that does NOT govern the public content API\n * — that path resolves a role and asks `buildPermissionChecker`.\n * `site_chrome:view` is seeded onto `public` by\n * `buildInitialRoleDefinitions()` so a fresh install serves its chrome;\n * changing this option does not change that.\n */\n viewRole?: string\n /** Roles allowed to edit a document's blocks. Default `'editor'`. */\n updateRole?: string\n /** Roles allowed to create/delete a chrome row. Default `'admin'`. */\n adminRole?: string\n /**\n * The block environment the editor offers in this document's canvas. Pass\n * the same value the site's page entity uses so a header can be built from\n * the same vocabulary. Omitted means \"no environment restriction\".\n */\n environment?: string\n /** Override the version-retention cap. Default 30 versions per locale. */\n maxVersions?: number\n /**\n * Extra behaviors layered after the factory's own.\n *\n * This is how an app supplies `withOpLog()`, which is REQUIRED for the block\n * editor to open the document and which cannot live in this package —\n * `withOpLog` ships from `@murumets-ee/blocks/server`, and\n * `@murumets-ee/content` must not depend on `@murumets-ee/blocks` because\n * blocks depends back on content.\n *\n * Typed as field-contributing-nothing on purpose: the entity's inferred field\n * map is part of its public type, and a behavior that added fields here would\n * widen it silently. `withOpLog()` adds none, which its own docblock states.\n */\n behaviors?: readonly Behavior<Record<never, never>, string>[]\n /**\n * How the Page-settings tab and forms NAME this entity's fields. Merged per\n * field over the factory's English defaults, so a project adds only what it\n * translates — typically `labelKey` / `descriptionKey` into its OWN catalogue\n * (e.g. `{ label: { labelKey: 'SiteChrome.fields.label', descriptionKey:\n * 'SiteChrome.fields.labelHelp' } }`). Resolution is key → English label →\n * humanized name, never the raw field name (editor-curation D003).\n */\n admin?: {\n fieldOverrides?: EntityAdminConfig['fieldOverrides']\n }\n}\n\n/**\n * The English defaults every project gets. `label` is the only root field the\n * Page-settings tab offers (`rootFields` below), so it is the one an operator\n * reads; without this it humanized to \"Label\", untranslatable.\n */\nconst DEFAULT_FIELD_OVERRIDES: NonNullable<EntityAdminConfig['fieldOverrides']> = {\n label: {\n label: 'Name',\n description: 'Shown in the admin list and as the editor title. Visitors never see it.',\n },\n}\n\n/** Per-field shallow merge — a project's key never drops the default English fallback. */\nfunction mergeFieldOverrides(\n project: EntityAdminConfig['fieldOverrides'],\n): NonNullable<EntityAdminConfig['fieldOverrides']> {\n const merged: NonNullable<EntityAdminConfig['fieldOverrides']> = { ...DEFAULT_FIELD_OVERRIDES }\n for (const [name, override] of Object.entries(project ?? {})) {\n merged[name] = { ...merged[name], ...override }\n }\n return merged\n}\n\n/**\n * Build the `site_chrome` entity — the block documents rendered around every\n * page that does not override them.\n *\n * The return type is intentionally INFERRED (no annotation) so `'site_chrome'`\n * stays a string literal in the caller's `Entity<…, 'site_chrome', …>` view;\n * annotating it as bare `Entity` widens the name to `string`, which propagates\n * into the resolved-permissions union as a `${string}:view` template. The\n * compile-time guard at the foot of this file is what keeps that true.\n */\nexport function siteChromeEntity(options?: SiteChromeEntityOptions) {\n const viewRole = options?.viewRole ?? 'editor'\n const updateRole = options?.updateRole ?? 'editor'\n const adminRole = options?.adminRole ?? 'admin'\n const maxVersions = options?.maxVersions ?? 30\n\n return defineEntity({\n name: 'site_chrome',\n // NOT `'singleton'` — see the cardinality note in this file's header. One\n // row per placement, enforced by `placement`'s own unique constraint.\n kind: 'collection',\n fields: {\n // THE placement flag. Deliberately not translatable: where a document\n // appears is a property of the document, not of a translation of it.\n //\n // `unique: true` is load-bearing rather than decorative — it is the\n // whole cardinality rule. `indexed` is NOT set alongside it: the schema\n // builder skips indexing unique fields because the constraint already\n // emits its own btree.\n placement: field.select({\n options: CHROME_PLACEMENTS,\n required: true,\n unique: true,\n }),\n // The human name of the row, shown in the list and as the editor's\n // title. `titleField` below points at it explicitly so nothing has to\n // fall back to a heuristic — or to a `title` column this entity has\n // never had.\n label: field.text({ required: true }),\n // The ONE blockTree field, which is what makes this a canvas.\n //\n // `translatable: true`, load-bearing: without it the column lives on the\n // base row, so writing the English header overwrites the Estonian one\n // and every locale then serves English. The write succeeds; only a\n // per-locale READ reveals it.\n //\n // `translation: 'shared'` is not an option — the schema builder throws on\n // it for blockTree — so each locale's chrome is an independent tree. That\n // matches the editor, whose session id is `${id}:${locale}`.\n layout: field.blockTree({\n translatable: true,\n ...(options?.environment !== undefined && { environment: options.environment }),\n }),\n },\n behaviors: [\n auditable(),\n publishable(),\n versionable({ maxPerDoc: maxVersions }),\n ...(options?.behaviors ?? []),\n ],\n scope: 'global',\n access: {\n view: viewRole,\n // Restricts WHO may create; `placement`'s unique constraint restricts\n // HOW MANY. Both apply, and neither substitutes for the other.\n create: adminRole,\n update: updateRole,\n delete: adminRole,\n },\n admin: {\n icon: 'panel-top',\n description: 'Blocks rendered around every page that does not override them.',\n // `layout` is a blockTree and is excluded from forms by type, so this is\n // only about what the block editor's settings sheet offers. `placement`\n // is deliberately absent: moving a header to the footer slot by editing\n // a dropdown is not an operation anyone wants, and it would collide with\n // the row already holding that placement.\n rootFields: ['label'],\n fieldOverrides: mergeFieldOverrides(options?.admin?.fieldOverrides),\n titleField: 'label',\n group: 'Site',\n // See this file's header — the rows are bootstrapped, not hand-created.\n disableCreate: true,\n },\n })\n}\n\n/**\n * COMPILE-TIME guard for the inferred-return-type note above.\n *\n * It lives in `src/`, NOT in `tests/`, and that placement is the point: this\n * package's tsconfig includes `src` only, so a type-level assertion under\n * `tests/` is compiled by no gate at all — vitest transpiles without checking\n * types. A guard nobody compiles is worse than no guard, because it reads as\n * one.\n *\n * Negative-tested: annotating `siteChromeEntity`'s return as bare `Entity`\n * makes this line fail `tsc --noEmit` with TS2322.\n */\nconst _chromeNameStaysLiteral: 'site_chrome' = siteChromeEntity().name\nvoid _chromeNameStaysLiteral\n"],"mappings":"6IA0GA,MAAa,EAAoB,CAAC,SAAU,QAAQ,EAMvC,EAAqE,CAChF,OAAQ,cACR,OAAQ,aACV,EA0DM,EAA4E,CAChF,MAAO,CACL,MAAO,OACP,YAAa,yEACf,CACF,EAGA,SAAS,EACP,EACkD,CAClD,IAAM,EAA2D,CAAE,GAAG,CAAwB,EAC9F,IAAK,GAAM,CAAC,EAAM,KAAa,OAAO,QAAQ,GAAW,CAAC,CAAC,EACzD,EAAO,GAAQ,CAAE,GAAG,EAAO,GAAO,GAAG,CAAS,EAEhD,OAAO,CACT,CAYA,SAAgB,EAAiB,EAAmC,CAClE,IAAM,EAAW,GAAS,UAAY,SAChC,EAAa,GAAS,YAAc,SACpC,EAAY,GAAS,WAAa,QAClC,EAAc,GAAS,aAAe,GAE5C,OAAO,EAAa,CAClB,KAAM,cAGN,KAAM,aACN,OAAQ,CAQN,UAAW,EAAM,OAAO,CACtB,QAAS,EACT,SAAU,GACV,OAAQ,EACV,CAAC,EAKD,MAAO,EAAM,KAAK,CAAE,SAAU,EAAK,CAAC,EAWpC,OAAQ,EAAM,UAAU,CACtB,aAAc,GACd,GAAI,GAAS,cAAgB,IAAA,IAAa,CAAE,YAAa,EAAQ,WAAY,CAC/E,CAAC,CACH,EACA,UAAW,CACT,EAAU,EACV,EAAY,EACZ,EAAY,CAAE,UAAW,CAAY,CAAC,EACtC,GAAI,GAAS,WAAa,CAAC,CAC7B,EACA,MAAO,SACP,OAAQ,CACN,KAAM,EAGN,OAAQ,EACR,OAAQ,EACR,OAAQ,CACV,EACA,MAAO,CACL,KAAM,YACN,YAAa,iEAMb,WAAY,CAAC,OAAO,EACpB,eAAgB,EAAoB,GAAS,OAAO,cAAc,EAClE,WAAY,QACZ,MAAO,OAEP,cAAe,EACjB,CACF,CAAC,CACH,CAc+C,EAAiB,CAAC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { t as VersionableFields } from "./versionable-ByZgfpp0.mjs";
2
- import { Behavior } from "@murumets-ee/entity";
2
+ import { Behavior, EntityAdminConfig } from "@murumets-ee/entity";
3
3
 
4
4
  //#region src/entities/site-chrome.d.ts
5
5
  /**
@@ -52,6 +52,17 @@ interface SiteChromeEntityOptions {
52
52
  * widen it silently. `withOpLog()` adds none, which its own docblock states.
53
53
  */
54
54
  behaviors?: readonly Behavior<Record<never, never>, string>[];
55
+ /**
56
+ * How the Page-settings tab and forms NAME this entity's fields. Merged per
57
+ * field over the factory's English defaults, so a project adds only what it
58
+ * translates — typically `labelKey` / `descriptionKey` into its OWN catalogue
59
+ * (e.g. `{ label: { labelKey: 'SiteChrome.fields.label', descriptionKey:
60
+ * 'SiteChrome.fields.labelHelp' } }`). Resolution is key → English label →
61
+ * humanized name, never the raw field name (editor-curation D003).
62
+ */
63
+ admin?: {
64
+ fieldOverrides?: EntityAdminConfig['fieldOverrides'];
65
+ };
55
66
  }
56
67
  /**
57
68
  * Build the `site_chrome` entity — the block documents rendered around every
@@ -83,4 +94,4 @@ declare function siteChromeEntity(options?: SiteChromeEntityOptions): import("@m
83
94
  }, "site_chrome", [Behavior<import("@murumets-ee/entity").AuditableFields, string>, Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, Behavior<VersionableFields, string>, ...Behavior<Record<never, never>, string>[]]>;
84
95
  //#endregion
85
96
  export { siteChromeEntity as a, SiteChromeEntityOptions as i, CHROME_PLACEMENT_LABELS as n, ChromePlacement as r, CHROME_PLACEMENTS as t };
86
- //# sourceMappingURL=site-chrome-CpnNlaw5.d.mts.map
97
+ //# sourceMappingURL=site-chrome-CUE16iIz.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"site-chrome-CpnNlaw5.d.mts","names":[],"sources":["../src/entities/site-chrome.ts"],"mappings":";;;;;;;;;;;cA0Ga,iBAAA;;KAGD,eAAA,WAA0B,iBAAiB;;cAG1C,uBAAA,EAAyB,QAAA,CAAS,MAAA,CAAO,eAAA;;UAMrC,uBAAA;;;;;;;;;;EAUf,QAAA;;EAEA,UAAA;;EAEA,SAAA;;;;;;EAMA,WAAA;;EAEA,WAAA;;;;;;;;;;;;;;EAcA,SAAA,YAAqB,QAAQ,CAAC,MAAA;AAAA;;;;;;;;;;;iBAahB,gBAAA,CAAiB,OAAA,GAAU,uBAAA,iCAAuB,MAAA;;sGAAA,iBAAA"}
1
+ {"version":3,"file":"site-chrome-CUE16iIz.d.mts","names":[],"sources":["../src/entities/site-chrome.ts"],"mappings":";;;;;;;;;;;cA0Ga,iBAAA;;KAGD,eAAA,WAA0B,iBAAiB;;cAG1C,uBAAA,EAAyB,QAAA,CAAS,MAAA,CAAO,eAAA;;UAMrC,uBAAA;;;;;;;;;;EAUf,QAAA;;EAEA,UAAA;;EAEA,SAAA;;;;;;EAMA,WAAA;;EAEA,WAAA;;;;;;;;;;;;;;EAcA,SAAA,YAAqB,QAAA,CAAS,MAAA;;;;;;;;;EAS9B,KAAA;IACE,cAAA,GAAiB,iBAAA;EAAA;AAAA;;;;;;;;;;;iBAqCL,gBAAA,CAAiB,OAAA,GAAU,uBAAA,iCAAuB,MAAA;;sGAAA,iBAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@murumets-ee/content",
3
- "version": "0.78.0",
3
+ "version": "0.79.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,14 +56,14 @@
56
56
  "drizzle-orm": "^0.45.2",
57
57
  "server-only": "^0.0.1",
58
58
  "zod": "^3.24.1",
59
- "@murumets-ee/blocks": "0.78.0",
60
- "@murumets-ee/core": "0.78.0",
61
- "@murumets-ee/db": "0.78.0",
62
- "@murumets-ee/entity": "0.78.0",
63
- "@murumets-ee/i18n": "0.78.0",
64
- "@murumets-ee/logging": "0.78.0",
65
- "@murumets-ee/queue": "0.78.0",
66
- "@murumets-ee/settings": "0.78.0"
59
+ "@murumets-ee/blocks": "0.79.0",
60
+ "@murumets-ee/core": "0.79.0",
61
+ "@murumets-ee/db": "0.79.0",
62
+ "@murumets-ee/entity": "0.79.0",
63
+ "@murumets-ee/i18n": "0.79.0",
64
+ "@murumets-ee/logging": "0.79.0",
65
+ "@murumets-ee/queue": "0.79.0",
66
+ "@murumets-ee/settings": "0.79.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "^20.19.42",
@@ -1,2 +0,0 @@
1
- import{n as e}from"./versionable-Br_G5VGR.mjs";import{auditable as t,defineEntity as n,field as r,publishable as i}from"@murumets-ee/entity";const a=[`header`,`footer`],o={header:`Site header`,footer:`Site footer`};function s(o){let s=o?.viewRole??`editor`,c=o?.updateRole??`editor`,l=o?.adminRole??`admin`,u=o?.maxVersions??30;return n({name:`site_chrome`,kind:`collection`,fields:{placement:r.select({options:a,required:!0,unique:!0}),label:r.text({required:!0}),layout:r.blockTree({translatable:!0,...o?.environment!==void 0&&{environment:o.environment}})},behaviors:[t(),i(),e({maxPerDoc:u}),...o?.behaviors??[]],scope:`global`,access:{view:s,create:l,update:c,delete:l},admin:{icon:`panel-top`,description:`Blocks rendered around every page that does not override them.`,rootFields:[`label`],titleField:`label`,group:`Site`,disableCreate:!0}})}s().name;export{o as n,s as r,a as t};
2
- //# sourceMappingURL=site-chrome-BhlkbRuq.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"site-chrome-BhlkbRuq.mjs","names":[],"sources":["../src/entities/site-chrome.ts"],"sourcesContent":["/**\n * `site_chrome` — the block documents that hold the site chrome, one row per\n * PLACEMENT, so a header is edited on the same canvas as the page it sits\n * above instead of being typed into a JSON textarea.\n *\n * ## Placement, not type\n *\n * There is ONE entity. A chrome document says where it appears by carrying a\n * `placement` value, and `'header'` / `'footer'` are exactly the region keys\n * `GET /v1/_chrome` emits — so the provider maps placement to region by\n * identity rather than through a lookup table. That is the whole of the model.\n *\n * It replaces three entities (`site_header`, `site_footer`, and a single-row\n * site layout) and six hardcoded name lists, the most consequential of which\n * was not a list of names at all: `apps/admin/lib/chrome-documents.ts` carried\n * a module constant pairing each chrome entity with a\n * `position: 'before' | 'after'`. That was the placement flag, written in code,\n * in the app, where no operator could reach it and no plugin could extend it.\n *\n * ## What survives, and why it had to\n *\n * **A separate ROW per placement.** `publishable()` and `versionable()` are\n * per-row, so a header and a footer sharing one row could not be published or\n * rolled back independently. That constraint is real and this model honours\n * it — it is only the entity TYPE that collapses, never the row.\n *\n * **Peer-document editing.** The editor composes header, page and footer as\n * peers with per-document undo (`plan/site-chrome` D001 / D003). It never\n * keyed on the entity NAME to do that: `DocumentSlice.entity` is the only\n * discriminator and `pageDocumentId` is explicit state, precisely so that no\n * entity-name allowlist is needed. One name in place of two costs it nothing.\n *\n * ## Cardinality — why `kind: 'collection'` and a unique column\n *\n * `kind: 'singleton'` is enforced now (`plan/site-chrome` D011), and\n * `assertSingletonSlotFree` counts every live row of an entity — so an entity\n * holding a header row AND a footer row trips the gate on the second insert.\n * The kind and the collapse are mutually exclusive, which is why phase 09 lists\n * \"the singleton kind\" among the things that go.\n *\n * `placement`'s `unique: true` is what takes over the half that mattered: the\n * rule becomes one row per placement, enforced at the DATABASE for every\n * writer rather than in one client. On a `trashable()` entity — which this is,\n * by auto-enable, because it is publishable — the schema builder re-emits that\n * as a PARTIAL unique index (`WHERE \"deletedAt\" IS NULL`), so trashing a\n * header frees its placement for a replacement instead of wedging it.\n *\n * What is NOT carried over is the singleton DELETE rule (\"never leave zero\").\n * Deleting the only header is now possible, and the site falls back to the\n * delivery template's own markup — the same thing it does before the first\n * header is ever created. `trashable()` makes it recoverable.\n *\n * ## `admin.disableCreate` is correctness, not a workaround\n *\n * The block-editor create branch auto-POSTs `{ title: 'Untitled' }` and\n * nothing else, so any entity on that branch with a second required field\n * would reject on `/new`. `placement` is exactly such a field.\n *\n * Suppressing the affordance is also the honest answer: a third chrome row\n * would render NOWHERE, because the delivery template has exactly two chrome\n * slots. A new placement is a code change on both sides — a `placement` option\n * here and a slot in `BaseLayout.astro` — and that coupling is what keeps the\n * enum from accumulating values nothing renders.\n *\n * The two rows a site needs are created by the `content:site-chrome` seeder,\n * which `lumi migrate` runs automatically as a `bootstrap` seeder. Nobody has\n * to press anything.\n *\n * ## Wiring\n *\n * @example\n * ```ts\n * // apps/<app>/entities/site-chrome.ts\n * import { withOpLog } from '@murumets-ee/blocks/server'\n * import { siteChromeEntity } from '@murumets-ee/content/entities'\n *\n * export const SiteChrome = siteChromeEntity({ environment: 'page' })\n * .with(withOpLog()) // see below — the app layers this on\n * ```\n *\n * `withOpLog()` is deliberately NOT in the factory: it ships from\n * `@murumets-ee/blocks/server`, and `@murumets-ee/content` must not depend on\n * `@murumets-ee/blocks` (blocks depends back on content). The app layers it at\n * config time. **Without it the block editor 404s** — `editor-state` refuses a\n * document whose entity has no op-log behavior — so an app that registers this\n * and skips it gets a chrome document it cannot open.\n *\n * Behaviors the factory does apply:\n *\n * - `auditable()` — created/updated stamps\n * - `publishable()` — a header is drafted and published like any document,\n * which is the point of it being one\n * - `versionable({ maxPerDoc })` — per-locale history\n */\n\nimport type { Behavior } from '@murumets-ee/entity'\nimport { auditable, defineEntity, field, publishable } from '@murumets-ee/entity'\nimport { versionable } from '../behaviors/versionable.js'\n\n/**\n * Where a chrome document appears on every page that does not override it.\n *\n * These strings ARE the `/v1/_chrome` region keys and the delivery template's\n * slot names. Adding one means adding a slot to `BaseLayout.astro` in the same\n * change, or the value names a region nothing renders.\n */\nexport const CHROME_PLACEMENTS = ['header', 'footer'] as const\n\n/** One placement. Also a region name on the wire — same string, by design. */\nexport type ChromePlacement = (typeof CHROME_PLACEMENTS)[number]\n\n/** The default label a bootstrap row gets, per placement. */\nexport const CHROME_PLACEMENT_LABELS: Readonly<Record<ChromePlacement, string>> = {\n header: 'Site header',\n footer: 'Site footer',\n}\n\n/** Optional overrides apps can apply when calling the factory. */\nexport interface SiteChromeEntityOptions {\n /**\n * Roles allowed to read the document. Default `'editor'`.\n *\n * ADVISORY entity-level metadata that does NOT govern the public content API\n * — that path resolves a role and asks `buildPermissionChecker`.\n * `site_chrome:view` is seeded onto `public` by\n * `buildInitialRoleDefinitions()` so a fresh install serves its chrome;\n * changing this option does not change that.\n */\n viewRole?: string\n /** Roles allowed to edit a document's blocks. Default `'editor'`. */\n updateRole?: string\n /** Roles allowed to create/delete a chrome row. Default `'admin'`. */\n adminRole?: string\n /**\n * The block environment the editor offers in this document's canvas. Pass\n * the same value the site's page entity uses so a header can be built from\n * the same vocabulary. Omitted means \"no environment restriction\".\n */\n environment?: string\n /** Override the version-retention cap. Default 30 versions per locale. */\n maxVersions?: number\n /**\n * Extra behaviors layered after the factory's own.\n *\n * This is how an app supplies `withOpLog()`, which is REQUIRED for the block\n * editor to open the document and which cannot live in this package —\n * `withOpLog` ships from `@murumets-ee/blocks/server`, and\n * `@murumets-ee/content` must not depend on `@murumets-ee/blocks` because\n * blocks depends back on content.\n *\n * Typed as field-contributing-nothing on purpose: the entity's inferred field\n * map is part of its public type, and a behavior that added fields here would\n * widen it silently. `withOpLog()` adds none, which its own docblock states.\n */\n behaviors?: readonly Behavior<Record<never, never>, string>[]\n}\n\n/**\n * Build the `site_chrome` entity — the block documents rendered around every\n * page that does not override them.\n *\n * The return type is intentionally INFERRED (no annotation) so `'site_chrome'`\n * stays a string literal in the caller's `Entity<…, 'site_chrome', …>` view;\n * annotating it as bare `Entity` widens the name to `string`, which propagates\n * into the resolved-permissions union as a `${string}:view` template. The\n * compile-time guard at the foot of this file is what keeps that true.\n */\nexport function siteChromeEntity(options?: SiteChromeEntityOptions) {\n const viewRole = options?.viewRole ?? 'editor'\n const updateRole = options?.updateRole ?? 'editor'\n const adminRole = options?.adminRole ?? 'admin'\n const maxVersions = options?.maxVersions ?? 30\n\n return defineEntity({\n name: 'site_chrome',\n // NOT `'singleton'` — see the cardinality note in this file's header. One\n // row per placement, enforced by `placement`'s own unique constraint.\n kind: 'collection',\n fields: {\n // THE placement flag. Deliberately not translatable: where a document\n // appears is a property of the document, not of a translation of it.\n //\n // `unique: true` is load-bearing rather than decorative — it is the\n // whole cardinality rule. `indexed` is NOT set alongside it: the schema\n // builder skips indexing unique fields because the constraint already\n // emits its own btree.\n placement: field.select({\n options: CHROME_PLACEMENTS,\n required: true,\n unique: true,\n }),\n // The human name of the row, shown in the list and as the editor's\n // title. `titleField` below points at it explicitly so nothing has to\n // fall back to a heuristic — or to a `title` column this entity has\n // never had.\n label: field.text({ required: true }),\n // The ONE blockTree field, which is what makes this a canvas.\n //\n // `translatable: true`, load-bearing: without it the column lives on the\n // base row, so writing the English header overwrites the Estonian one\n // and every locale then serves English. The write succeeds; only a\n // per-locale READ reveals it.\n //\n // `translation: 'shared'` is not an option — the schema builder throws on\n // it for blockTree — so each locale's chrome is an independent tree. That\n // matches the editor, whose session id is `${id}:${locale}`.\n layout: field.blockTree({\n translatable: true,\n ...(options?.environment !== undefined && { environment: options.environment }),\n }),\n },\n behaviors: [\n auditable(),\n publishable(),\n versionable({ maxPerDoc: maxVersions }),\n ...(options?.behaviors ?? []),\n ],\n scope: 'global',\n access: {\n view: viewRole,\n // Restricts WHO may create; `placement`'s unique constraint restricts\n // HOW MANY. Both apply, and neither substitutes for the other.\n create: adminRole,\n update: updateRole,\n delete: adminRole,\n },\n admin: {\n icon: 'panel-top',\n description: 'Blocks rendered around every page that does not override them.',\n // `layout` is a blockTree and is excluded from forms by type, so this is\n // only about what the block editor's settings sheet offers. `placement`\n // is deliberately absent: moving a header to the footer slot by editing\n // a dropdown is not an operation anyone wants, and it would collide with\n // the row already holding that placement.\n rootFields: ['label'],\n titleField: 'label',\n group: 'Site',\n // See this file's header — the rows are bootstrapped, not hand-created.\n disableCreate: true,\n },\n })\n}\n\n/**\n * COMPILE-TIME guard for the inferred-return-type note above.\n *\n * It lives in `src/`, NOT in `tests/`, and that placement is the point: this\n * package's tsconfig includes `src` only, so a type-level assertion under\n * `tests/` is compiled by no gate at all — vitest transpiles without checking\n * types. A guard nobody compiles is worse than no guard, because it reads as\n * one.\n *\n * Negative-tested: annotating `siteChromeEntity`'s return as bare `Entity`\n * makes this line fail `tsc --noEmit` with TS2322.\n */\nconst _chromeNameStaysLiteral: 'site_chrome' = siteChromeEntity().name\nvoid _chromeNameStaysLiteral\n"],"mappings":"6IA0GA,MAAa,EAAoB,CAAC,SAAU,QAAQ,EAMvC,EAAqE,CAChF,OAAQ,cACR,OAAQ,aACV,EAoDA,SAAgB,EAAiB,EAAmC,CAClE,IAAM,EAAW,GAAS,UAAY,SAChC,EAAa,GAAS,YAAc,SACpC,EAAY,GAAS,WAAa,QAClC,EAAc,GAAS,aAAe,GAE5C,OAAO,EAAa,CAClB,KAAM,cAGN,KAAM,aACN,OAAQ,CAQN,UAAW,EAAM,OAAO,CACtB,QAAS,EACT,SAAU,GACV,OAAQ,EACV,CAAC,EAKD,MAAO,EAAM,KAAK,CAAE,SAAU,EAAK,CAAC,EAWpC,OAAQ,EAAM,UAAU,CACtB,aAAc,GACd,GAAI,GAAS,cAAgB,IAAA,IAAa,CAAE,YAAa,EAAQ,WAAY,CAC/E,CAAC,CACH,EACA,UAAW,CACT,EAAU,EACV,EAAY,EACZ,EAAY,CAAE,UAAW,CAAY,CAAC,EACtC,GAAI,GAAS,WAAa,CAAC,CAC7B,EACA,MAAO,SACP,OAAQ,CACN,KAAM,EAGN,OAAQ,EACR,OAAQ,EACR,OAAQ,CACV,EACA,MAAO,CACL,KAAM,YACN,YAAa,iEAMb,WAAY,CAAC,OAAO,EACpB,WAAY,QACZ,MAAO,OAEP,cAAe,EACjB,CACF,CAAC,CACH,CAc+C,EAAiB,CAAC,CAAC"}