@murumets-ee/content 0.90.0 → 0.92.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.
@@ -0,0 +1,28 @@
1
+ import { ToolkitApp } from "@murumets-ee/core";
2
+
3
+ //#region src/detail-template-provider.d.ts
4
+ /** A published template, as stored — its blocks are resolved by the caller. */
5
+ interface DetailTemplateDocument {
6
+ /** The template row's id, for the response's cache tags. */
7
+ readonly id: string;
8
+ readonly blocks: unknown[];
9
+ }
10
+ interface DetailTemplateProvider {
11
+ /** The template entity's name, for the response's cache tags. */
12
+ readonly entity: string;
13
+ readTemplate(opts: {
14
+ entity: string;
15
+ locale: string;
16
+ }): Promise<DetailTemplateDocument | null>;
17
+ publicRow(opts: {
18
+ entity: string;
19
+ row: Readonly<Record<string, unknown>>;
20
+ locale: string;
21
+ }): Promise<Record<string, unknown>>;
22
+ }
23
+ declare function createDetailTemplateProvider(getApp: () => Promise<ToolkitApp> | ToolkitApp, options: {
24
+ defaultLocale: string;
25
+ }): DetailTemplateProvider;
26
+ //#endregion
27
+ export { DetailTemplateDocument, DetailTemplateProvider, createDetailTemplateProvider };
28
+ //# sourceMappingURL=detail-template-provider.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detail-template-provider.d.mts","names":[],"sources":["../src/detail-template-provider.ts"],"mappings":";;;AAmFA;AAAA,UAjBiB,sBAAA;;WAEN,EAAA;EAAA,SACA,MAAM;AAAA;AAAA,UAGA,sBAAA;EAcQ;EAAA,SAZd,MAAA;EACT,YAAA,CAAa,IAAA;IAAQ,MAAA;IAAgB,MAAA;EAAA,IAAmB,OAAA,CAAQ,sBAAA;EAChE,SAAA,CAAU,IAAA;IACR,MAAA;IACA,GAAA,EAAK,QAAA,CAAS,MAAA;IACd,MAAA;EAAA,IACE,OAAA,CAAQ,MAAA;AAAA;AAAA,iBAGE,4BAAA,CACd,MAAA,QAAc,OAAA,CAAQ,UAAA,IAAc,UAAA,EACpC,OAAA;EAAW,aAAA;AAAA,IACV,sBAAA"}
@@ -0,0 +1,2 @@
1
+ import{n as e,t}from"./detail-template-names-CD1Y9n1U.mjs";import{getBlockTreeFieldName as n,isTemplateDrawn as r}from"@murumets-ee/entity";import{createSystemReachResolver as i}from"@murumets-ee/core";import{QueryClient as a}from"@murumets-ee/entity/query";import{schemaRegistry as o}from"@murumets-ee/db";import{eq as s,getTableColumns as c}from"drizzle-orm";import{effectiveDetailSpecs as l,resolveDocumentDetail as u}from"@murumets-ee/blocks/server";const d=i(`detail-template-resolver`),f=new Set([`title`,`text`]);function p(i,p){return{entity:t,readTemplate:async({entity:l,locale:u})=>{let f=await i(),m=f.entities.get(l);if(!m||!r(m))return null;let h=f.entities.get(t);if(!h)return null;let g=n(h),_=o.get(t),v=(_?c(_):void 0)?.[e];if(!g||!v)return null;let y=(await new a({entity:h,db:f.db.readOnly,logger:f.logger.child({resolve:`detail-template`}),contextResolver:d}).findMany({limit:1,locale:u,defaultLocale:p.defaultLocale,where:s(v,l)}))[0];if(!y||typeof y.id!=`string`)return null;let b=y[g];return!Array.isArray(b)||b.length===0?null:{id:y.id,blocks:b}},publicRow:async({entity:e,row:t,locale:n})=>{let r=l(await i()).specs.get(e);if(r===void 0)return{};let a=u(r,t,n,f);return{...a?.title!==void 0&&{title:a.title},...a?.text!==void 0&&{description:a.text}}}}}export{p as createDetailTemplateProvider};
2
+ //# sourceMappingURL=detail-template-provider.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detail-template-provider.mjs","names":[],"sources":["../src/detail-template-provider.ts"],"sourcesContent":["/**\n * The detail-template provider `content-api` is handed (block-library D009).\n *\n * `content-api` is a leaf: it knows an entity is drawn by a template\n * (`frontend.drawnBy`), and nothing about where a template is stored or which\n * of a row's columns a site lets a visitor see. Both answers live here, and are\n * injected as {@link createDetailTemplateProvider}.\n *\n * ## 🔴 What a visitor can reach through this\n *\n * - **`readTemplate`** answers for a row's ENTITY NAME, which the handler takes\n * from a whitelisted entity the caller holds `view` on — never from a request\n * parameter and never from a stored value. It reads the PUBLISHED template\n * for that entity and locale: the read runs under a reach context that grants\n * `view` and withholds `view_unpublished`, so `QueryClient`'s publish filter\n * and trashed-row exclusion still apply. A draft template is `null`, and the\n * page falls back to a plain one.\n *\n * It runs under reach rather than under the caller's own grants on purpose. A\n * template is not a document a visitor asks for; it is how a row they may\n * already read is drawn, the same derivation a menu or a path resolution is.\n * Requiring `detail_template:view` on `public` would be a second grant an\n * operator has to remember, whose absence looks exactly like \"no template\n * published\" — the two-grant trap `chrome-provider.ts` records.\n *\n * - **`publicRow`** is the ONLY thing of the row that crosses, besides its id.\n * It projects `title` and `description` through the site's detail declaration\n * (`blocksPlugin({ details })`), whose columns were validated at boot as\n * declared, non-`internal`, non-`adminOnly`. An entity with no declaration\n * yields `{}`. No stored value names a column.\n *\n * ## Fan-out\n *\n * One query per `readTemplate` call, and the handler calls it once per by-id\n * response. `publicRow` does no I/O (it awaits only the app handle).\n */\n\nimport {\n type DocumentDetailPart,\n effectiveDetailSpecs,\n resolveDocumentDetail,\n} from '@murumets-ee/blocks/server'\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { createSystemReachResolver } from '@murumets-ee/core'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport type { SecurityContext } from '@murumets-ee/entity'\nimport { getBlockTreeFieldName, isTemplateDrawn } from '@murumets-ee/entity'\nimport { QueryClient } from '@murumets-ee/entity/query'\nimport { eq, getTableColumns } from 'drizzle-orm'\nimport type { PgColumn } from 'drizzle-orm/pg-core'\nimport {\n DETAIL_TEMPLATE_ENTITY_NAME,\n DETAIL_TEMPLATE_TARGET_FIELD,\n} from './detail-template-names.js'\n\n/**\n * Reach, never visibility of unpublished rows — see the file header. Named so\n * its reads are distinguishable from the path and menu resolvers' in audit\n * output.\n */\nconst SYSTEM_CONTEXT: () => SecurityContext = createSystemReachResolver('detail-template-resolver')\n\n/** The parts of a detail that are the row's public name and summary. */\nconst PUBLIC_ROW_PARTS: ReadonlySet<DocumentDetailPart> = new Set(['title', 'text'])\n\n/** A published template, as stored — its blocks are resolved by the caller. */\nexport interface DetailTemplateDocument {\n /** The template row's id, for the response's cache tags. */\n readonly id: string\n readonly blocks: unknown[]\n}\n\nexport interface DetailTemplateProvider {\n /** The template entity's name, for the response's cache tags. */\n readonly entity: string\n readTemplate(opts: { entity: string; locale: string }): Promise<DetailTemplateDocument | null>\n publicRow(opts: {\n entity: string\n row: Readonly<Record<string, unknown>>\n locale: string\n }): Promise<Record<string, unknown>>\n}\n\nexport function createDetailTemplateProvider(\n getApp: () => Promise<ToolkitApp> | ToolkitApp,\n options: { defaultLocale: string },\n): DetailTemplateProvider {\n return {\n entity: DETAIL_TEMPLATE_ENTITY_NAME,\n\n readTemplate: async ({ entity: targetName, locale }) => {\n const app = await getApp()\n // Asked again here rather than trusted from the caller: this is the gate\n // that keeps the provider from being a way to read a template for an\n // entity code never marked as drawn by one.\n const target = app.entities.get(targetName)\n if (!target || !isTemplateDrawn(target)) return null\n\n const templateEntity = app.entities.get(DETAIL_TEMPLATE_ENTITY_NAME)\n if (!templateEntity) return null\n const blockTreeField = getBlockTreeFieldName(templateEntity)\n const table = schemaRegistry.get(DETAIL_TEMPLATE_ENTITY_NAME)\n // `?.` — see `chrome-provider.ts`: a registry entry that is not a real\n // table must degrade to \"no template\", not throw on every detail page.\n const columns = table\n ? (getTableColumns(table) as Record<string, PgColumn> | undefined)\n : undefined\n const targetColumn = columns?.[DETAIL_TEMPLATE_TARGET_FIELD]\n if (!blockTreeField || !targetColumn) return null\n\n const client = new QueryClient({\n entity: templateEntity,\n db: app.db.readOnly,\n logger: app.logger.child({ resolve: 'detail-template' }),\n contextResolver: SYSTEM_CONTEXT,\n })\n // `eq(target)` on the unique column: at most one live row can match.\n const rows = await client.findMany({\n limit: 1,\n locale,\n defaultLocale: options.defaultLocale,\n where: eq(targetColumn, targetName),\n })\n const row = rows[0] as Record<string, unknown> | undefined\n if (!row || typeof row.id !== 'string') return null\n const blocks = row[blockTreeField]\n // A published template with nothing in it is \"no template\": the plain\n // fallback is a better page than an empty one.\n if (!Array.isArray(blocks) || blocks.length === 0) return null\n return { id: row.id, blocks }\n },\n\n publicRow: async ({ entity, row, locale }) => {\n const app = await getApp()\n const spec = effectiveDetailSpecs(app).specs.get(entity)\n if (spec === undefined) return {}\n const detail = resolveDocumentDetail(spec, row, locale, PUBLIC_ROW_PARTS)\n return {\n ...(detail?.title !== undefined && { title: detail.title }),\n ...(detail?.text !== undefined && { description: detail.text }),\n }\n },\n }\n}\n"],"mappings":"scA4DA,MAAM,EAAwC,EAA0B,0BAA0B,EAG5F,EAAoD,IAAI,IAAI,CAAC,QAAS,MAAM,CAAC,EAoBnF,SAAgB,EACd,EACA,EACwB,CACxB,MAAO,CACL,OAAQ,EAER,aAAc,MAAO,CAAE,OAAQ,EAAY,YAAa,CACtD,IAAM,EAAM,MAAM,EAAO,EAInB,EAAS,EAAI,SAAS,IAAI,CAAU,EAC1C,GAAI,CAAC,GAAU,CAAC,EAAgB,CAAM,EAAG,OAAO,KAEhD,IAAM,EAAiB,EAAI,SAAS,IAAI,CAA2B,EACnE,GAAI,CAAC,EAAgB,OAAO,KAC5B,IAAM,EAAiB,EAAsB,CAAc,EACrD,EAAQ,EAAe,IAAI,CAA2B,EAMtD,GAHU,EACX,EAAgB,CAAK,EACtB,IAAA,GAAA,GAC2B,GAC/B,GAAI,CAAC,GAAkB,CAAC,EAAc,OAAO,KAe7C,IAAM,GAAM,MANO,IAPA,EAAY,CAC7B,OAAQ,EACR,GAAI,EAAI,GAAG,SACX,OAAQ,EAAI,OAAO,MAAM,CAAE,QAAS,iBAAkB,CAAC,EACvD,gBAAiB,CACnB,CAEwB,CAAC,CAAC,SAAS,CACjC,MAAO,EACP,SACA,cAAe,EAAQ,cACvB,MAAO,EAAG,EAAc,CAAU,CACpC,CAAC,EAAA,CACgB,GACjB,GAAI,CAAC,GAAO,OAAO,EAAI,IAAO,SAAU,OAAO,KAC/C,IAAM,EAAS,EAAI,GAInB,MADI,CAAC,MAAM,QAAQ,CAAM,GAAK,EAAO,SAAW,EAAU,KACnD,CAAE,GAAI,EAAI,GAAI,QAAO,CAC9B,EAEA,UAAW,MAAO,CAAE,SAAQ,MAAK,YAAa,CAE5C,IAAM,EAAO,EAAqB,MADhB,EAAO,CACY,CAAC,CAAC,MAAM,IAAI,CAAM,EACvD,GAAI,IAAS,IAAA,GAAW,MAAO,CAAC,EAChC,IAAM,EAAS,EAAsB,EAAM,EAAK,EAAQ,CAAgB,EACxE,MAAO,CACL,GAAI,GAAQ,QAAU,IAAA,IAAa,CAAE,MAAO,EAAO,KAAM,EACzD,GAAI,GAAQ,OAAS,IAAA,IAAa,CAAE,YAAa,EAAO,IAAK,CAC/D,CACF,CACF,CACF"}
@@ -1,2 +1,64 @@
1
+ import { t as VersionableFields } from "../versionable-ByZgfpp0.mjs";
1
2
  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
- export { CHROME_PLACEMENTS, CHROME_PLACEMENT_LABELS, type ChromePlacement, type SiteChromeEntityOptions, siteChromeEntity };
3
+ import { Behavior, Entity, EntityAdminConfig, ForbiddenError } from "@murumets-ee/entity";
4
+
5
+ //#region src/entities/detail-template.d.ts
6
+ interface DetailTemplateEntityOptions {
7
+ /**
8
+ * The entities drawn by a stored template. Each must declare
9
+ * `frontend: { path, drawnBy: 'template' }`; one that does not throws here,
10
+ * at boot, rather than producing a template nothing ever draws.
11
+ *
12
+ * May be empty — a project with no such entity yet keeps the registration and
13
+ * adds a name when it gets one. The entity is then hidden from the sidebar.
14
+ */
15
+ for: readonly Pick<Entity, 'name' | 'frontend'>[];
16
+ /** Role that may read templates in the admin. Default `'editor'`. */
17
+ viewRole?: string;
18
+ /** Role that may edit and publish templates. Default `'editor'`. */
19
+ updateRole?: string;
20
+ /** Role named for create/delete (both are otherwise closed). Default `'admin'`. */
21
+ adminRole?: string;
22
+ /** Snapshots kept per template. Default 30. */
23
+ maxVersions?: number;
24
+ /** The block environment the canvas opens (e.g. `'page'`). */
25
+ environment?: string;
26
+ /** App-supplied behaviors — `withOpLog()` at least. Must contribute no fields. */
27
+ behaviors?: readonly Behavior<Record<never, never>, string>[];
28
+ /** Field naming overrides, merged per field over the English defaults. */
29
+ admin?: {
30
+ fieldOverrides?: EntityAdminConfig['fieldOverrides'];
31
+ };
32
+ }
33
+ /** Thrown when someone tries to delete or trash a detail template. */
34
+ declare class DetailTemplateDeleteRefusedError extends ForbiddenError {
35
+ constructor();
36
+ }
37
+ /** The names a template may target — validated, de-duplicated, in the given order. */
38
+ declare function detailTemplateTargets(entities: readonly Pick<Entity, 'name' | 'frontend'>[]): string[];
39
+ /**
40
+ * Build the `detail_template` entity. The return type is INFERRED, for the
41
+ * reason `siteChromeEntity` gives: annotating it widens the name to `string`.
42
+ */
43
+ declare function detailTemplateEntity(options: DetailTemplateEntityOptions): Entity<{
44
+ id: import("@murumets-ee/entity").IdField;
45
+ } & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & VersionableFields & Record<never, never> & {
46
+ target: import("@murumets-ee/entity").SelectField & {
47
+ options: string[];
48
+ } & {
49
+ readonly options: string[];
50
+ readonly required: true;
51
+ readonly unique: true;
52
+ };
53
+ label: import("@murumets-ee/entity").TextField & {
54
+ readonly required: true;
55
+ };
56
+ layout: import("@murumets-ee/entity").BlockTreeField & {
57
+ readonly environment?: string;
58
+ readonly translatable: true;
59
+ readonly rowsFrom: "target";
60
+ };
61
+ }, "detail_template", [Behavior<import("@murumets-ee/entity").AuditableFields, string>, Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, Behavior<VersionableFields, string>, Behavior<Record<never, never>, "content:detail-template-undeletable">, ...Behavior<Record<never, never>, string>[]]>;
62
+ //#endregion
63
+ export { CHROME_PLACEMENTS, CHROME_PLACEMENT_LABELS, type ChromePlacement, DetailTemplateDeleteRefusedError, type DetailTemplateEntityOptions, type SiteChromeEntityOptions, detailTemplateEntity, detailTemplateTargets, siteChromeEntity };
64
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/entities/detail-template.ts"],"mappings":";;;;;UAqEiB,2BAAA;EA0FwD;;;;;;;;EAjFvE,GAAA,WAAc,IAAA,CAAK,MAAA;;EAEnB,QAAA;;EAEA,UAAA;;EAEA,SAAA;;EAEA,WAAA;;EAEA,WAAA;;EAEA,SAAA,YAAqB,QAAA,CAAS,MAAA;;EAE9B,KAAA;IACE,cAAA,GAAiB,iBAAA;EAAA;AAAA;;cAsBR,gCAAA,SAAyC,cAAc;EAAd,WAAA,CAAA;AAAA;;iBAwBtC,qBAAA,CACd,QAAA,WAAmB,IAAI,CAAC,MAAA;;;;;iBAmBV,oBAAA,CAAqB,OAAA,EAAS,2BAAA,GAA2B,MAAA;;sGAAA,iBAAA"}
@@ -1 +1,2 @@
1
- import{n as e,r as t,t as n}from"../site-chrome-DvP9SXGu.mjs";export{n as CHROME_PLACEMENTS,e as CHROME_PLACEMENT_LABELS,t as siteChromeEntity};
1
+ import{n as e}from"../versionable-Br_G5VGR.mjs";import{t}from"../detail-template-names-CD1Y9n1U.mjs";import{n,r,t as i}from"../site-chrome-DvP9SXGu.mjs";import{ForbiddenError as a,auditable as o,defineEntity as s,field as c,isTemplateDrawn as l,publishable as u}from"@murumets-ee/entity";const d={label:{label:`Name`,description:`Shown in the admin list and as the editor title. Visitors never see it.`}};function f(e){let t={...d};for(let[n,r]of Object.entries(e??{}))t[n]={...t[n],...r};return t}var p=class extends a{constructor(){super(`A detail template is not deleted: the rows it draws are still on the site. Unpublish it instead — those pages then show a plain fallback, and the draft stays editable.`),this.name=`DetailTemplateDeleteRefusedError`}};const m={name:`content:detail-template-undeletable`,hooks:{beforeDelete:async()=>{throw new p}}};function h(e){let t=[];for(let n of e){if(!l(n))throw Error(`detailTemplateEntity: '${n.name}' is listed in \`for\` but does not declare \`frontend: { path, drawnBy: 'template' }\`. A template for it would never be drawn.`);t.includes(n.name)||t.push(n.name)}return t}function g(n){let r=n.viewRole??`editor`,i=n.updateRole??`editor`,a=n.adminRole??`admin`,l=h(n.for);return s({name:t,site:`bound`,kind:`collection`,fields:{target:c.select({options:l,required:!0,unique:!0}),label:c.text({required:!0}),layout:c.blockTree({translatable:!0,rowsFrom:`target`,...n.environment!==void 0&&{environment:n.environment}})},behaviors:[o(),u(),e({maxPerDoc:n.maxVersions??30}),m,...n.behaviors??[]],scope:`global`,access:{view:r,create:a,update:i,delete:a},admin:{icon:`layout-template`,description:`One page layout per kind of synced record, drawn for every record of that kind.`,rootFields:[`label`],fieldOverrides:f(n.admin?.fieldOverrides),titleField:`label`,group:`Site`,disableCreate:!0,...l.length===0&&{hidden:!0}}})}export{i as CHROME_PLACEMENTS,n as CHROME_PLACEMENT_LABELS,p as DetailTemplateDeleteRefusedError,g as detailTemplateEntity,h as detailTemplateTargets,r as siteChromeEntity};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/entities/detail-template.ts"],"sourcesContent":["/**\n * `detail_template` — the ONE stored block document per entity type that draws\n * every row of an entity the site does not author (block-library D009).\n *\n * A synced room type or a synced legal document has no block-tree field and\n * cannot have one: its rows are written by a machine, so there is nothing an\n * editor could save a layout into. Its detail page is therefore a document of\n * THIS entity, keyed by the entity it draws. An editor opens \"Room detail\" on\n * the canvas and edits it like a page — adds, moves and removes blocks,\n * translates its fixed copy per locale, publishes it — and the site draws that\n * one published tree for every room, with that room's values in the bound\n * blocks.\n *\n * ## The same shape as `site_chrome`, for the same reasons\n *\n * One entity, one row per key, the key's `unique: true` as the whole\n * cardinality rule (a partial index, `WHERE \"deletedAt\" IS NULL`, because a\n * publishable entity is trashable). `layout` is `translatable`, so each locale\n * is an independent tree. `withOpLog()` comes from the app, because it ships\n * from `@murumets-ee/blocks/server` and this package must not depend on blocks:\n *\n * ```ts\n * import { withOpLog } from '@murumets-ee/blocks/server'\n * import { detailTemplateEntity } from '@murumets-ee/content/entities'\n *\n * export const DetailTemplate = detailTemplateEntity({\n * for: [RoomType, LegalDocument],\n * environment: 'page',\n * behaviors: [withOpLog({ retentionDays: 7 })],\n * })\n * ```\n *\n * Rows are created by the `content:detail-templates` bootstrap seeder (one\n * DRAFT per target, on every `lumi migrate`), so `admin.disableCreate` is on.\n *\n * ## 🔴 `target` is a key, never a choice\n *\n * `target` is a `select` whose options are the names of the entities passed in\n * `for` — each of which must itself declare `frontend: { drawnBy: 'template' }`\n * or this throws at boot. So a stored value can only ever be an entity that\n * CODE offered, and the public read never follows it: the site resolves a URL\n * to a row, and asks for the template WHERE `target` = that row's entity. No\n * request reads a template and then goes looking for the entity it names.\n *\n * `layout`'s `rowsFrom: 'target'` is what tells the canvas to draw the bound\n * blocks with a sample row of that entity, read-only.\n *\n * ## Deleting is refused; unpublishing is the off switch\n *\n * While an entity is drawn by a template its rows are routable, and a deleted\n * template would leave them with nothing an editor can open again (the row is\n * bootstrapped, and `disableCreate` hides the only other way to make one). So\n * delete and trash are refused outright. To take a detail page back to the\n * plain fallback, UNPUBLISH the template: the site then draws the row's title\n * on a plain page, and the draft is still there to publish again.\n */\n\nimport type { Behavior, Entity, EntityAdminConfig } from '@murumets-ee/entity'\nimport {\n auditable,\n defineEntity,\n ForbiddenError,\n field,\n isTemplateDrawn,\n publishable,\n} from '@murumets-ee/entity'\nimport { versionable } from '../behaviors/versionable.js'\nimport { DETAIL_TEMPLATE_ENTITY_NAME } from '../detail-template-names.js'\n\nexport interface DetailTemplateEntityOptions {\n /**\n * The entities drawn by a stored template. Each must declare\n * `frontend: { path, drawnBy: 'template' }`; one that does not throws here,\n * at boot, rather than producing a template nothing ever draws.\n *\n * May be empty — a project with no such entity yet keeps the registration and\n * adds a name when it gets one. The entity is then hidden from the sidebar.\n */\n for: readonly Pick<Entity, 'name' | 'frontend'>[]\n /** Role that may read templates in the admin. Default `'editor'`. */\n viewRole?: string\n /** Role that may edit and publish templates. Default `'editor'`. */\n updateRole?: string\n /** Role named for create/delete (both are otherwise closed). Default `'admin'`. */\n adminRole?: string\n /** Snapshots kept per template. Default 30. */\n maxVersions?: number\n /** The block environment the canvas opens (e.g. `'page'`). */\n environment?: string\n /** App-supplied behaviors — `withOpLog()` at least. Must contribute no fields. */\n behaviors?: readonly Behavior<Record<never, never>, string>[]\n /** Field naming overrides, merged per field over the English defaults. */\n admin?: {\n fieldOverrides?: EntityAdminConfig['fieldOverrides']\n }\n}\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\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/** Thrown when someone tries to delete or trash a detail template. */\nexport class DetailTemplateDeleteRefusedError extends ForbiddenError {\n constructor() {\n super(\n 'A detail template is not deleted: the rows it draws are still on the site. ' +\n 'Unpublish it instead — those pages then show a plain fallback, and the draft stays editable.',\n )\n this.name = 'DetailTemplateDeleteRefusedError'\n }\n}\n\n/**\n * Refuse every delete path. `beforeDelete` runs on a single delete, a purge, a\n * trash, `deleteMany` and a cascade, so this one hook covers all of them.\n */\nconst undeletable: Behavior<Record<never, never>, 'content:detail-template-undeletable'> = {\n name: 'content:detail-template-undeletable',\n hooks: {\n beforeDelete: async () => {\n throw new DetailTemplateDeleteRefusedError()\n },\n },\n}\n\n/** The names a template may target — validated, de-duplicated, in the given order. */\nexport function detailTemplateTargets(\n entities: readonly Pick<Entity, 'name' | 'frontend'>[],\n): string[] {\n const names: string[] = []\n for (const entity of entities) {\n if (!isTemplateDrawn(entity)) {\n throw new Error(\n `detailTemplateEntity: '${entity.name}' is listed in \\`for\\` but does not declare ` +\n \"`frontend: { path, drawnBy: 'template' }`. A template for it would never be drawn.\",\n )\n }\n if (!names.includes(entity.name)) names.push(entity.name)\n }\n return names\n}\n\n/**\n * Build the `detail_template` entity. The return type is INFERRED, for the\n * reason `siteChromeEntity` gives: annotating it widens the name to `string`.\n */\nexport function detailTemplateEntity(options: DetailTemplateEntityOptions) {\n const viewRole = options.viewRole ?? 'editor'\n const updateRole = options.updateRole ?? 'editor'\n const adminRole = options.adminRole ?? 'admin'\n const targets = detailTemplateTargets(options.for)\n\n return defineEntity({\n name: DETAIL_TEMPLATE_ENTITY_NAME,\n // Per site, like chrome: each site has its own detail pages.\n site: 'bound',\n kind: 'collection',\n fields: {\n // THE key. Not translatable — which entity a template draws is a property\n // of the document, not of a translation. `unique: true` is the whole\n // one-template-per-entity-type rule, enforced by the database for every\n // writer.\n target: field.select({\n options: targets,\n required: true,\n unique: true,\n }),\n label: field.text({ required: true }),\n layout: field.blockTree({\n translatable: true,\n rowsFrom: 'target',\n ...(options.environment !== undefined && { environment: options.environment }),\n }),\n },\n behaviors: [\n auditable(),\n publishable(),\n versionable({ maxPerDoc: options.maxVersions ?? 30 }),\n undeletable,\n ...(options.behaviors ?? []),\n ],\n scope: 'global',\n access: {\n view: viewRole,\n create: adminRole,\n update: updateRole,\n delete: adminRole,\n },\n admin: {\n icon: 'layout-template',\n description:\n 'One page layout per kind of synced record, drawn for every record of that kind.',\n // `target` is deliberately absent, as `placement` is on chrome: pointing\n // a room template at legal documents by editing a dropdown would collide\n // with the row already holding that target.\n rootFields: ['label'],\n fieldOverrides: mergeFieldOverrides(options.admin?.fieldOverrides),\n titleField: 'label',\n group: 'Site',\n disableCreate: true,\n // Nothing to list until a project has a template-drawn entity.\n ...(targets.length === 0 && { hidden: true }),\n },\n })\n}\n\n/**\n * COMPILE-TIME guard that the name stays a literal — see `site-chrome.ts`'s\n * twin for why it lives in `src/`.\n */\ntype DetailTemplateName = ReturnType<typeof detailTemplateEntity>['name']\nconst _nameIsLiteral: string extends DetailTemplateName ? never : true = true\nvoid _nameIsLiteral\n"],"mappings":"gSAiGA,MAAM,EAA4E,CAChF,MAAO,CACL,MAAO,OACP,YAAa,yEACf,CACF,EAEA,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,CAGA,IAAa,EAAb,cAAsD,CAAe,CACnE,aAAc,CACZ,MACE,yKAEF,EACA,KAAK,KAAO,kCACd,CACF,EAMA,MAAM,EAAqF,CACzF,KAAM,sCACN,MAAO,CACL,aAAc,SAAY,CACxB,MAAM,IAAI,CACZ,CACF,CACF,EAGA,SAAgB,EACd,EACU,CACV,IAAM,EAAkB,CAAC,EACzB,IAAK,IAAM,KAAU,EAAU,CAC7B,GAAI,CAAC,EAAgB,CAAM,EACzB,MAAU,MACR,0BAA0B,EAAO,KAAK,iIAExC,EAEG,EAAM,SAAS,EAAO,IAAI,GAAG,EAAM,KAAK,EAAO,IAAI,CAC1D,CACA,OAAO,CACT,CAMA,SAAgB,EAAqB,EAAsC,CACzE,IAAM,EAAW,EAAQ,UAAY,SAC/B,EAAa,EAAQ,YAAc,SACnC,EAAY,EAAQ,WAAa,QACjC,EAAU,EAAsB,EAAQ,GAAG,EAEjD,OAAO,EAAa,CAClB,KAAM,EAEN,KAAM,QACN,KAAM,aACN,OAAQ,CAKN,OAAQ,EAAM,OAAO,CACnB,QAAS,EACT,SAAU,GACV,OAAQ,EACV,CAAC,EACD,MAAO,EAAM,KAAK,CAAE,SAAU,EAAK,CAAC,EACpC,OAAQ,EAAM,UAAU,CACtB,aAAc,GACd,SAAU,SACV,GAAI,EAAQ,cAAgB,IAAA,IAAa,CAAE,YAAa,EAAQ,WAAY,CAC9E,CAAC,CACH,EACA,UAAW,CACT,EAAU,EACV,EAAY,EACZ,EAAY,CAAE,UAAW,EAAQ,aAAe,EAAG,CAAC,EACpD,EACA,GAAI,EAAQ,WAAa,CAAC,CAC5B,EACA,MAAO,SACP,OAAQ,CACN,KAAM,EACN,OAAQ,EACR,OAAQ,EACR,OAAQ,CACV,EACA,MAAO,CACL,KAAM,kBACN,YACE,kFAIF,WAAY,CAAC,OAAO,EACpB,eAAgB,EAAoB,EAAQ,OAAO,cAAc,EACjE,WAAY,QACZ,MAAO,OACP,cAAe,GAEf,GAAI,EAAQ,SAAW,GAAK,CAAE,OAAQ,EAAK,CAC7C,CACF,CAAC,CACH"}
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./versionable-Br_G5VGR.mjs";import{r as t}from"./errors-NRNILhhm.mjs";import{r as n,t as r}from"./locale-entity-CoSq3C77.mjs";import{n as i,t as a}from"./content-config-store-8omQF5p5.mjs";import{r as o}from"./routing-strategy-D6B4ngT9.mjs";import{n as s,t as c}from"./registry-CaPbY5lm.mjs";import"./locale-utils-B0mFoV47.mjs";import{n as l}from"./chrome-entity-names-zYJzP-_U.mjs";import{r as u}from"./detail-template-names-CD1Y9n1U.mjs";import{c as d,i as f,n as p,o as m,r as h,t as g}from"./schema-BYrYzsmO.mjs";import{l as _,t as v,u as y}from"./schema-IML1rRU_.mjs";import{r as b,t as x}from"./routes-BmNSYyuT.mjs";import{n as S}from"./locks-5779ugSW.mjs";import{MAX_PATH_DEPTH as C,buildPartitionCondition as w,hasPathParent as T,isSiteBound as E,resolveEffectiveSlugMode as D,resolvePattern as O}from"@murumets-ee/entity";import{definePlugin as k,getApp as A,getAppOrUndefined as j,getContext as M,getCurrentSite as N,requireRequestSite as P,requireSiteFor as F,resolveImplicitSite as I,runAsCli as L,runInEverySite as R,runInSite as z}from"@murumets-ee/core";import{column as B,defineTable as V,schemaRegistry as H}from"@murumets-ee/db";import{and as U,asc as ee,eq as W,getTableColumns as te,inArray as ne,sql as re}from"drizzle-orm";import{z as G}from"zod";import{createHash as ie}from"node:crypto";const ae=Symbol.for(`@murumets-ee/content:locale-status-writer`);async function K(e,t,n,r){let i=r===`published`?new Date:null;await e.upsert({entityId:t,locale:n,status:r,publishedAt:i},{target:[`entityId`,`locale`],set:{status:r,publishedAt:i}})}function q(e){globalThis[ae]=(n,r)=>{let{localeStatus:i}=c(n.name);if(i)return async(n,a,o,s)=>{let c=M()?.availableLocales??e;if(!c.includes(a))throw new t(a,[...c]);await K(i.makeClient(r,s),n,a,o)}}}async function J(e){let t=M()?.app??j();if(!t)return;let n=t.entities.get(e);if(n)return F(n,t)}function oe(){return{async writeDraft({documentType:e,documentId:t,locale:n,dataPatch:r,user:i,tx:a}){let{drafts:o}=c(e);if(!o)return!1;let s=o.makeClient(a,await J(e)),l=new Date;return await s.upsert({entityId:t,locale:n,data:r,createdBy:i.id,createdByName:i.name??null,createdAt:l,updatedAt:l},{target:[`entityId`,`locale`],set:{updatedAt:l},mergeJsonb:[`data`]}),!0},async readDraft({documentType:e,documentId:t,locale:n,db:r}){let{drafts:i}=c(e);if(!i)return null;let a=await i.makeClient(r,await J(e)).findOne({entityId:t,locale:n});return a?{data:a.data??{},createdBy:a.createdBy??null,createdByName:a.createdByName??null,updatedAt:a.updatedAt??null}:null},async readLocaleStatus({documentType:e,documentId:t,locale:n,db:r}){let{localeStatus:i}=c(e);if(!i)return null;let a=await i.makeClient(r,await J(e)).findOne({entityId:t,locale:n});return a?a.status===`published`?`published`:`draft`:null},async hasAnyDraft({documentType:e,documentId:t,db:n}){let{drafts:r}=c(e);return r?r.makeClient(n,await J(e)).exists({entityId:t}):!1},async hasAnyLocaleActivity({documentType:e,documentId:t,db:n}){let{localeStatus:r}=c(e),i=await J(e);if(r&&await r.makeClient(n,i).exists({entityId:t}))return!0;let a=H.get(`${e}_translations`);if(a){let r=a.entityId;if(r!==void 0){let o=i===void 0?W(r,t):U(W(r,t),w(a,{site:i},e));if((await n.select({one:re`1`}).from(a).where(o).limit(1)).length>0)return!0}}return!1}}}function se(e){let{defaultLocale:t}=a();return e===`_`||e===t?{defaultLocale:t}:{locale:e,defaultLocale:t}}function ce(e){let{defaultLocale:t}=a();return e===`_`||e===t?`_`:e}async function le(e,t,n,r){let{drafts:i}=c(e);if(!i)return;let a=await i.makeClient(r,await J(e)).findOne({entityId:t,locale:n});return a?a.data??void 0:void 0}function ue(){return{async snapshotCurrentState({documentType:t,documentId:n,locale:r,user:i}){let a=A(),o=a.entities.get(t);if(!o)return!1;let s=e(o.behaviors);if(!s)return!1;let c=a.getClient(o),l=c.getDb(),u=ce(r),d=await c.findById(n,se(u));if(!d)return!1;let f=await le(t,n,u,l),p=f?{...d,...f}:{...d},[{createContentContext:m},{createVersionSnapshot:h}]=await Promise.all([import(`./context-D7eEItCY.mjs`).then(e=>e.t),import(`./versions-CD2Tnqla.mjs`).then(e=>e.c)]);return await h(m({admin:c,entity:o},s),n,p,d,u,{id:i.id,...i.name==null?{}:{name:i.name}}),!0}}}async function Y(e,t,n){let r=te(g),i=r[h],a=r.entityType,o=r.entityId;if(!i||!a||!o)throw Error(`toolkit_paths is missing '${h}'/entity columns`);let s=await e.selectDistinct({site:i}).from(g).where(U(W(a,t),W(o,n))).limit(2);if(s.length>1)return null;let c=s[0]?.site;return typeof c==`string`?c:void 0}const de=Symbol.for(`@murumets-ee/content:path-writers-factory`),X=Symbol.for(`@murumets-ee/content:cascade-trigger-factory`);function fe(e){globalThis[X]=e}function pe(){let e=globalThis[X];return typeof e==`function`?e:void 0}function me(e,t){return async r=>{let i=await he(e,t,r);if(i===null)return;let a=p.makeClient(e.db.readWrite,i),o=N();if(o!==void 0&&o!==i&&!(r.action===`delete`||r.action===`release`)){e.logger.warn({entity:r.entityName,id:r.id,site:i,requestSite:o},"toolkit_paths: a cascade reached a document on another site — its path is left for `lumi paths:rebuild` on that site rather than recomputed from this request");return}return o===void 0&&r.viaCascade===!0&&E(t)?z(i,()=>n(a,r)):n(a,r)};async function n(n,r){if(r.action===`delete`){await n.deleteMany({entityType:r.entityName,entityId:r.id});return}if(r.action===`release`){await n.deleteMany({entityType:r.entityName,entityId:r.id,isCanonical:!0});return}let i=a(),o=i.defaultLocale,s=i.reservedPathPrefixes??[],c=r.locale?[r.locale]:i.locales.map(e=>e.code),l=r.viaCascade===!0?e.getSystemClient(t):e.getClient(t),u=async n=>{if(!T(t))return;let i=pe();if(i)try{await i(e,t,r.id,n)}catch(t){e.logger.warn({err:t,entity:r.entityName,id:r.id,locale:n},`toolkit_paths: descendant-cascade trigger threw — the entity write already committed, so tolerating and continuing (paths are rebuildable, D005)`)}};for(let i of c){let a=i===r.locale?r.row??null:await l.findById(r.id,{locale:i,defaultLocale:o});if(!a){await m({pathsClient:n,entityType:r.entityName,entityId:r.id,locale:i,path:null,skipReason:`row-unreadable`,reservedPathPrefixes:s,logger:e.logger});continue}let c=await _({entity:t,row:a,locale:i,defaultLocale:o,pathsClient:n}),d=await m({pathsClient:n,entityType:r.entityName,entityId:r.id,locale:i,path:c.path,...c.skipReason!==void 0&&{skipReason:c.skipReason},reservedPathPrefixes:s,logger:e.logger});(d.outcome===`renamed`||r.action===`restore`&&d.outcome===`created`)&&await u(i)}}}async function he(e,t,n){if(n.viaCascade===!0&&E(t)){let t=await Y(e.db.readWrite,n.entityName,n.id);if(t===null)return e.logger.warn({entity:n.entityName,id:n.id},"toolkit_paths: a document has path rows on SEVERAL sites — refusing to guess which one this cascade maintains; `lumi paths:rebuild` per site repairs it"),null;if(t!==void 0)return t;let r=N();if(r!==void 0)return r;let i=await I(e).catch(()=>void 0);return i===void 0?(e.logger.warn({entity:n.entityName,id:n.id,action:n.action},"toolkit_paths: cascade child has no path rows and the request names no site — nothing to maintain here; `lumi paths:rebuild` recomputes it per site"),null):i}return P(`toolkit_paths`,e)}function ge(e,t){return async n=>{let r=p.makeClient(e.db.readWrite,await P(`toolkit_paths`,e)),i=a(),o=i.defaultLocale,s=i.reservedPathPrefixes??[],c=n.locale??o,l=null;n.id!==void 0&&(l=await e.getClient(t).findById(n.id,{locale:c,defaultLocale:o}));let u=D(n.data.slugMode,l?.slugMode);await y({entity:t,data:n.data,currentRow:l,id:n.id,locale:c,defaultLocale:o,slugMode:u,reservedPathPrefixes:s,pathsClient:r})}}function _e(){globalThis[de]=(e,t)=>({pathIndexWriter:me(e,t),pathConflictGuard:ge(e,t)})}const Z=G.object({entityType:G.string().min(1),parentId:G.string().uuid(),locale:G.string().min(1)}),Q=G.object({entity:G.string().min(1).optional(),locale:G.string().min(1).optional(),dryRun:G.boolean()});async function ve(e,t,n={}){let{entityType:r,parentId:i,locale:o}=t,s=n.maxItems??2e3,c=n.concurrency??5,l={processed:0,regenerated:0,unchanged:0,skipped:0,failed:0,truncated:!1},u=e=>({phase:e,...l}),f=()=>({entityType:r,parentId:i,locale:o,cap:s,...l}),h=e.entities.get(r);if(!h)return e.logger.warn({entityType:r,parentId:i,locale:o},`paths-cascade: unknown entity type — nothing to regenerate`),n.onProgress?.(u(`done`)),f();if(!T(h))return e.logger.warn({entityType:r,parentId:i,locale:o},`paths-cascade: entity is not a pathParent() hierarchy — nothing to regenerate`),n.onProgress?.(u(`done`)),f();let g=a(),v=g.defaultLocale,y=g.reservedPathPrefixes??[],b=p.makeClient(e.db.readWrite,await P(`toolkit_paths`,e)),x=e.getClient(h),S=x.getTable().parent,C=async t=>{let n=t.id;try{let i=await _({entity:h,row:t,locale:o,defaultLocale:v,pathsClient:b});switch((await m({pathsClient:b,entityType:r,entityId:n,locale:o,path:i.path,...i.skipReason!==void 0&&{skipReason:i.skipReason},reservedPathPrefixes:y,logger:e.logger})).outcome){case`created`:case`renamed`:l.regenerated+=1;break;case`unchanged`:l.unchanged+=1;break;default:l.skipped+=1;break}}catch(t){l.failed+=1,e.logger.warn({err:t,entityType:r,id:n,locale:o},`paths-cascade: failed to regenerate one descendant path — continuing (per-item isolation)`)}},w=[i];for(;w.length>0;){let e=s-l.processed;if(e<=0){l.truncated=!0;break}let t=await x.findMany({where:ne(S,w),locale:o,defaultLocale:v,limit:e+1});if(t.length===0)break;let r=t;if(t.length>e&&(r=t.slice(0,e),l.truncated=!0),await d(r,c,C),l.processed+=r.length,n.onProgress?.(u(`walking`)),l.truncated)break;w=r.map(e=>e.id)}return l.truncated&&e.logger.warn({entityType:r,parentId:i,locale:o,processed:l.processed,cap:s},`paths-cascade: descendant subtree exceeded the cap — deeper/remaining descendants were NOT regenerated. Run \`lumi paths:rebuild --entity ${r}\` for guaranteed full coverage.`),n.onProgress?.(u(`done`)),f()}async function ye(e,t,n={}){let r=n.pageSize??200,i=n.concurrency??5,o=n.sampleCap??25,s=t.dryRun,c=a(),l=c.defaultLocale,u=c.reservedPathPrefixes??[],f=c.locales.map(e=>e.code),h=t.locale?f.filter(e=>e===t.locale):f,g=[...e.entities.values()].filter(e=>!!e.frontend?.path&&(t.entity===void 0||e.name===t.entity)),v=t.entity!==void 0&&g.length===0,y=t.locale!==void 0&&h.length===0;v&&e.logger.warn({entity:t.entity,dryRun:s},`paths-rebuild: --entity matches no routable entity (one declaring frontend.path) — nothing to rebuild (likely a typo in the entity name)`),y&&e.logger.warn({locale:t.locale,configuredLocales:f,dryRun:s},`paths-rebuild: --locale matches no configured locale — nothing to rebuild (likely a typo in the locale code)`);let b=v||y?{entity:v,locale:y}:void 0,x=g.some(e=>T(e)),S=s&&x?`Preview reflects a SINGLE pass; a pathParent() hierarchy drifted at multiple levels may need the real (non-dry-run) rebuild's multi-pass convergence to fully resolve. The real run always converges.`:void 0,w=p.makeClient(e.db.readWrite,await P(`toolkit_paths`,e)),E={scanned:0,changed:0,applied:0,failed:0},D=new Map,O={missing:0,conflicts:0,unchanged:0,skipped:0},k=(e,t)=>{let n=D.get(e);n!==t&&(n!==void 0&&n!==`resolved`&&--O[n],D.set(e,t),t!==`resolved`&&(O[t]+=1))},A=[],j=!1,M=e=>{A.length<o?A.push(e):j=!0},N=0,F=e=>({phase:e,dryRun:s,scope:{entity:t.entity??null,locale:t.locale??null},scanned:E.scanned,changed:E.changed,applied:E.applied,failed:E.failed,missing:O.missing,conflicts:O.conflicts,unchanged:O.unchanged,skipped:O.skipped,passes:N,sample:A,sampleTruncated:j,...S===void 0?{}:{hierarchyNote:S},...b===void 0?{}:{unmatchedScope:b}}),I=async(t,n,r,i)=>{let a=r.id,o=!1;try{let c=await _({entity:t,row:r,locale:i,defaultLocale:l,pathsClient:w}),d=await m({pathsClient:w,entityType:n,entityId:a,locale:i,path:c.path,...c.skipReason!==void 0&&{skipReason:c.skipReason},suppressNoteworthy:[`parent-not-indexed`],reservedPathPrefixes:u,logger:e.logger,dryRun:s}),f=`${n}:${a}:${i}`;switch(d.outcome){case`unchanged`:k(f,`unchanged`);break;case`skipped-null`:case`skipped-reserved`:k(f,`skipped`);break;case`conflict`:k(f,`conflicts`),M({kind:`conflict`,entityType:n,entityId:a,locale:i,oldPath:d.oldPath,newPath:d.newPath,...d.conflictWith?{conflictWith:d.conflictWith.entityId}:{}});break;case`created`:s?k(f,`missing`):(E.applied+=1,o=!0,k(f,`resolved`)),M({kind:`missing`,entityType:n,entityId:a,locale:i,oldPath:null,newPath:d.newPath});break;case`renamed`:E.changed+=1,s||(E.applied+=1,o=!0),k(f,`resolved`),M({kind:`changed`,entityType:n,entityId:a,locale:i,oldPath:d.oldPath,newPath:d.newPath});break}}catch(t){E.failed+=1,e.logger.warn({err:t,entityType:n,id:a,locale:i},`paths-rebuild: failed to process one document — continuing (per-item isolation)`)}return o},L=async t=>{let a=0;for(let o of t){let t=e.getClient(o),s=o.name,c=t.getTable().id;for(let e of h){let u;for(;;){let f=await t.findMany({locale:e,defaultLocale:l,limit:r,orderBy:ee(c),...u===void 0?{}:{cursor:{field:`id`,value:u,direction:`asc`}}});if(f.length===0)break;await d(f,i,async t=>{await I(o,s,t,e)&&(a+=1)}),E.scanned+=f.length;let p=f[f.length-1];if(p&&(u=p.id),n.onProgress?.(F(`scanning`)),f.length<r)break}}}return a},R=async t=>{let r=[];for(let[n,i]of D){if(i!==`conflicts`&&i!==`skipped`&&i!==`missing`)continue;let a=n.indexOf(`:`),o=n.lastIndexOf(`:`);if(a<=0||o<=a)continue;let s=n.slice(0,a);if(t.has(s))continue;let c=e.entities.get(s);c!==void 0&&r.push({entity:c,entityType:s,entityId:n.slice(a+1,o),locale:n.slice(o+1)})}if(r.length===0)return 0;let a=0;return await d(r,i,async t=>{let n=await e.getClient(t.entity).findById(t.entityId,{locale:t.locale,defaultLocale:l});n!==null&&(E.scanned+=1,await I(t.entity,t.entityType,n,t.locale)&&(a+=1))}),n.onProgress?.(F(`scanning`)),a},z=g.filter(e=>T(e)),B=new Set(z.map(e=>e.name)),V=s||!x?1:C;for(let e=0;e<V;e++){N=e+1;let t=await L(e===0?g:z);if(e>0&&(t+=await R(B)),t===0)break}j&&e.logger.warn({scanned:E.scanned,changed:E.changed,missing:O.missing,conflicts:O.conflicts,sampleCap:o},`paths-rebuild: more diff entries than the progress sample can hold — see the aggregate counts; the stored sample is truncated`);let H=F(`done`);n.onProgress?.(H);let{phase:U,...W}=H;return W}async function be(e){let{defineJob:t,registerJob:n,QueueClient:r}=await import(`@murumets-ee/queue/client`),i=t({name:`content:paths-cascade`,description:`Regenerate toolkit_paths canonical rows for the descendants of a renamed routable-hierarchy ancestor (one locale). Walks the full bounded subtree in a single invocation.`,schema:Z}),a=t({name:`content:paths-rebuild`,description:`Recompute every routable document's canonical toolkit_paths row (optionally scoped to one entity/locale); dryRun reports the diff without writing. On-demand (lumi paths:rebuild).`,schema:Q});n(i,async t=>{await L(async()=>{let n=await ve(e,Z.parse(t.payload),{onProgress:e=>t.updateProgress(e)});e.logger.info(n,`paths-cascade: descendant regeneration complete`)})}),n(a,async t=>{await L(()=>R(async()=>{let n=await ye(e,Q.parse(t.payload),{onProgress:e=>t.updateProgress({...e,site:N()??null})});e.logger.info({dryRun:n.dryRun,scanned:n.scanned,changed:n.changed,missing:n.missing,conflicts:n.conflicts,applied:n.applied,failed:n.failed,passes:n.passes,...n.hierarchyNote===void 0?{}:{hierarchyNote:n.hierarchyNote}},`paths-rebuild: sweep complete`)},{app:e,label:`paths-rebuild`}))}),fe(async(e,t,n,a)=>{await new r({app:e,logger:e.logger}).enqueue(i,{entityType:t.name,parentId:n,locale:a})})}const $=V({name:`toolkit_path_patterns`,columns:{id:B.uuid({primaryKey:!0,defaultRandom:!0}),entityType:B.varchar({length:100,notNull:!0,pgName:`entity_type`}),locale:B.varchar({length:10,notNull:!0}),patternHash:B.varchar({length:64,notNull:!0,pgName:`pattern_hash`}),updatedAt:B.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityType`,`locale`]}]}),xe=$.table;function Se(e,t,n){let r=O(e,t,n);if(r===null)return null;let i=typeof r==`function`?r.toString():JSON.stringify(r);return ie(`sha256`).update(i).digest(`hex`)}async function Ce(e){let t=a(),n=t.defaultLocale,r=t.locales.map(e=>e.code),i=$.makeClient(e.db.readWrite);for(let[t,a]of e.entities){let o=a.frontend?.path;if(o!==void 0)for(let a of r){let r=Se(o,a,n);if(r===null)continue;let s=await i.findOne({entityType:t,locale:a});s!==null&&s.patternHash===r||(s!==null&&e.logger.warn({entity:t,locale:a,previousHash:s.patternHash,currentHash:r},`toolkit_paths: frontend.path pattern for '${t}' (locale '${a}') changed since last boot — stale toolkit_paths rows may exist; run \`lumi paths:rebuild --entity ${t}\``),await i.upsert({entityType:t,locale:a,patternHash:r},{target:[`entityType`,`locale`],set:{patternHash:r,updatedAt:new Date}}))}}}const we=async(e,t,n)=>{if(t.length===0)return new Map;let[{buildEnumerateDeps:r},{getCanonicalPaths:i}]=await Promise.all([import(`./resolve-context-Cvc1aNJe.mjs`).then(e=>e.l),import(`./enumerate-CYHJKQz6.mjs`).then(e=>e.n)]),a=r(A()),o=await a.filterPublished(e,t,n);return o.size===0?new Map:i(e,[...o],n,a)};function Te(e){return f(e,{name:`${e.name}_drafts`,columns:{id:B.uuid({primaryKey:!0,defaultRandom:!0}),entityId:B.uuid({notNull:!0,pgName:`entity_id`}),locale:B.varchar({length:10,notNull:!0,default:`_`}),data:B.jsonb({notNull:!0}),createdBy:B.varchar({length:255,notNull:!0,pgName:`created_by`}),createdByName:B.varchar({length:255,pgName:`created_by_name`}),createdAt:B.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),updatedAt:B.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityId`,`locale`]}]})}function Ee(e){return f(e,{name:`${e.name}_locale_status`,columns:{id:B.uuid({primaryKey:!0,defaultRandom:!0}),entityId:B.uuid({notNull:!0,pgName:`entity_id`}),locale:B.varchar({length:10,notNull:!0}),status:B.varchar({length:20,notNull:!0,default:`draft`}),publishedAt:B.timestamp({withTimezone:!0,pgName:`published_at`})},unique:[{on:[`entityId`,`locale`]}]})}function De(e){return f(e,{name:`${e.name}_versions`,columns:{id:B.uuid({primaryKey:!0,defaultRandom:!0}),entityId:B.uuid({notNull:!0,pgName:`entity_id`}),version:B.integer({notNull:!0}),locale:B.varchar({length:10,notNull:!0,default:`_`}),data:B.jsonb({notNull:!0}),delta:B.jsonb(),status:B.varchar({length:20}),createdBy:B.varchar({length:255,pgName:`created_by`}),createdByName:B.varchar({length:255,pgName:`created_by_name`}),createdAt:B.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),isAutosave:B.boolean({notNull:!0,default:!1,pgName:`is_autosave`})},unique:[{on:[`entityId`,`version`,`locale`]}]})}function Oe(e){if(!e.locales||e.locales.length===0)throw Error(`content() requires at least one locale`);if(!e.defaultLocale)throw Error(`content() requires a defaultLocale`);if(!e.locales.some(t=>t.code===e.defaultLocale))throw Error(`defaultLocale "${e.defaultLocale}" not found in locales: [${e.locales.map(e=>e.code).join(`, `)}]`);if(e.reservedPathPrefixes){for(let t of e.reservedPathPrefixes)if(typeof t!=`string`||!t.startsWith(`/`))throw Error(`content(): reservedPathPrefixes entries must be absolute paths starting with '/', got ${JSON.stringify(t)}`)}let t=e.autoTrash??!0,a=t===!1?void 0:{publishable:!0,...typeof t==`object`&&t.exclude?{exclude:t.exclude}:{}};return k({name:`@murumets-ee/content`,contributes:{"blocks.documentDraftStore":oe(),"blocks.documentVersionStore":ue(),"blocks.publishedPathResolver":we,...a?{"trash.autoEnable":a}:{},...e.reservedPathPrefixes?{"routing.reservedPathPrefixes":e.reservedPathPrefixes}:{}},shared:{settings:[n],features:[{resource:`redirect`,actions:[`manage`],defaultRoles:[`admin`],description:`Create, edit, and delete manual URL redirects (toolkit_redirects).`},{resource:`path`,actions:[`resolve`],defaultRoles:[`admin`,`editor`],description:`Resolve a site URL path to the CMS document that serves it.`}]},server:{entities:[r,o],tables:{toolkitContentLocks:S,toolkitPaths:g,toolkitPathPatterns:xe,toolkitRedirects:v},routes:[...x(),...b()],seeders:[{name:`content:site-chrome`,description:`Create the empty site_chrome row for each placement (header, footer), as a DRAFT, so they can be edited on the page canvas. Publishing one is what makes /v1/_chrome serve it instead of the delivery template's own fallback. Idempotent — an existing row for a placement is left alone.`,available:l,bootstrap:!0,run:async()=>{let{seedSiteChrome:e}=await import(`@murumets-ee/content/seed`);return await e()}},{name:`content:detail-templates`,description:`Create the DRAFT detail template for each entity drawn by one (block-library D009), so its page layout can be edited on the canvas. Until a template is published its rows keep the plain fallback page. Idempotent — an existing template is left alone.`,available:u,bootstrap:!0,run:async()=>{let{seedDetailTemplates:e}=await import(`@murumets-ee/content/seed`);return await e()}}],init:async t=>{i(e);let n=0,r=0,a=0;for(let[e,i]of t.entities){if(i.behaviors?.some(e=>e.name===`versionable`)){let t=`${e}_versions`;if(!H.has(t)){let n=De(i);H.register(t,n.table),s(e,{versions:n})}n++}let t=i.behaviors?.some(e=>e.name===`publishable`),o=i.allFields,c=Object.values(o).some(e=>e.translatable);if(t&&c){let t=`${e}_locale_status`;if(!H.has(t)){let n=Ee(i);H.register(t,n.table),s(e,{localeStatus:n})}r++}if(t){let t=`${e}_drafts`;if(!H.has(t)){let n=Te(i);H.register(t,n.table),s(e,{drafts:n})}a++}}q(e.locales.map(e=>e.code)),_e();try{await be(t)}catch(e){t.logger.warn({err:e},"content: failed to register routing path jobs — descendant cascade + `lumi paths:rebuild` will be unavailable (path writes still succeed)")}t.logger.info({locales:e.locales.map(e=>e.code),defaultLocale:e.defaultLocale,versionableEntities:n,localeStatusEntities:r,draftEntities:a},`Content plugin initialized`)},onReady:async e=>{try{await Ce(e)}catch(t){e.logger.error({err:t},"content: boot-time frontend-path pattern-drift check failed — continuing app boot without it. Inspect the logged `err` for the cause: a transient DB error can be retried (and `lumi paths:rebuild` re-derives the index if a pattern actually changed), whereas a structural cause — e.g. a missing migration leaving `toolkit_path_patterns` absent — will recur on every boot and needs `lumi migrate`")}}}})}export{K as n,Oe as t};
2
+ //# sourceMappingURL=plugin-BpkCaBrp.mjs.map