@murumets-ee/content 0.93.0 → 0.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.mjs +1 -1
- package/dist/block-resolver.d.mts +17 -3
- package/dist/block-resolver.d.mts.map +1 -1
- package/dist/block-resolver.mjs +1 -1
- package/dist/block-resolver.mjs.map +1 -1
- package/dist/breadcrumbs-BqSuMNac.mjs +2 -0
- package/dist/breadcrumbs-BqSuMNac.mjs.map +1 -0
- package/dist/client.mjs +1 -1
- package/dist/enumerate-CYHJKQz6.mjs.map +1 -1
- package/dist/{plugin-BpkCaBrp.mjs → plugin-ChCEq_Qy.mjs} +2 -2
- package/dist/{plugin-BpkCaBrp.mjs.map → plugin-ChCEq_Qy.mjs.map} +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/resolve-context-D8UXwWwr.mjs +2 -0
- package/dist/resolve-context-D8UXwWwr.mjs.map +1 -0
- package/dist/resolve.d.mts +4 -2
- package/dist/resolve.d.mts.map +1 -1
- package/dist/resolve.mjs +2 -2
- package/dist/{routes-BmNSYyuT.mjs → routes-DlvqU8bO.mjs} +2 -2
- package/dist/{routes-BmNSYyuT.mjs.map → routes-DlvqU8bO.mjs.map} +1 -1
- package/dist/{versions-CD2Tnqla.mjs → versions-B8feZTS9.mjs} +2 -2
- package/dist/{versions-CD2Tnqla.mjs.map → versions-B8feZTS9.mjs.map} +1 -1
- package/package.json +10 -10
- package/dist/resolve-context-Cvc1aNJe.mjs +0 -2
- package/dist/resolve-context-Cvc1aNJe.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breadcrumbs-BqSuMNac.mjs","names":[],"sources":["../src/paths/breadcrumbs.ts"],"sourcesContent":["/**\n * The breadcrumb trail of one document, derived from the path index\n * (block-library D003).\n *\n * A document at `/tooted/spreiolid/rapsioli-sprei` sits under `/tooted` and\n * `/tooted/spreiolid`. If the index holds a PUBLISHED document at each of those\n * paths, each is a step of the trail; a prefix with no published document\n * behind it is skipped (the trail is \"what the visitor can click\", not \"what\n * the URL looks like\"). The document itself ends the trail, as a step with no\n * link.\n *\n * ## Cost\n *\n * Bounded by {@link MAX_BREADCRUMBS}, not by anything a visitor controls:\n * one index read for the document's own path, ONE index read for every\n * ancestor path at once (`path IN (…)`, never one per ancestor), then one title\n * read per ENTITY TYPE found among the ancestors — at most a handful, because a\n * site's hierarchy is made of a few routable types. The title read is the same\n * publish-gated read the path resolvers use, so an unpublished or trashed\n * ancestor yields no step.\n *\n * ## Site\n *\n * `pathsClient` is the SITE-CONFINED reader (`sitePathsReader`): a site's\n * ancestor lookup cannot resolve another site's path. This module takes the\n * reader as a parameter so it imports nothing from the app graph.\n */\n\nimport type { PathEnumReader } from './enumerate.js'\n\n/** Most steps a trail carries, the document's own included. Mirrors `@murumets-ee/blocks`. */\nexport const MAX_BREADCRUMBS = 8\n\n/** One step of a trail, as the blocks package's `DocumentBreadcrumb`. */\nexport interface TrailStep {\n readonly label: string\n readonly href?: string\n}\n\n/** A trail, and the documents its ancestor steps came from (what a cached page depends on). */\nexport interface BreadcrumbTrail {\n readonly steps: readonly TrailStep[]\n readonly ancestors: readonly { readonly entity: string; readonly id: string }[]\n}\n\n/** What the trail needs from the app — injected so the logic is testable without one. */\nexport interface BreadcrumbTrailDeps {\n readonly pathsClient: PathEnumReader\n /**\n * Titles of the PUBLISHED rows among `ids` of one entity type in `locale`, as\n * `id → title`. A row that is unpublished, trashed or title-less is absent.\n */\n readonly publishedTitles: (\n entityType: string,\n ids: readonly string[],\n locale: string,\n ) => Promise<ReadonlyMap<string, string>>\n}\n\n/**\n * The paths above `path`, nearest-to-the-top first, excluding `/` itself\n * (the site's home is the block's own \"show home\" option) and `path`.\n * `/a/b/c` → `['/a', '/a/b']`.\n */\nexport function ancestorPaths(path: string): string[] {\n if (!path.startsWith('/')) return []\n const segments = path.split('/').filter((segment) => segment !== '')\n const out: string[] = []\n for (let depth = 1; depth < segments.length; depth++) {\n out.push(`/${segments.slice(0, depth).join('/')}`)\n }\n return out\n}\n\n/**\n * The trail for the document `entityType`/`id` in `locale`, or `undefined` when\n * the document has no canonical path (not routable, or not indexed yet) or is\n * the site's top page (nothing above it).\n *\n * `label` names the document's own step — the caller reads it from the row it\n * is drawing. Without it the trail ends at the last ancestor.\n */\nexport async function resolveBreadcrumbTrail(\n request: {\n readonly entityType: string\n readonly id: string\n readonly locale: string\n readonly label?: string | undefined\n },\n deps: BreadcrumbTrailDeps,\n): Promise<BreadcrumbTrail | undefined> {\n const own = await deps.pathsClient.findMany({\n where: {\n entityType: request.entityType,\n entityId: { in: [request.id] },\n locale: request.locale,\n isCanonical: true,\n },\n limit: 1,\n })\n const currentPath = own[0]?.path\n if (currentPath === undefined) return undefined\n\n // The nearest ancestors win when a path is deeper than the cap allows.\n const wanted = ancestorPaths(currentPath).slice(-(MAX_BREADCRUMBS - 1))\n const steps: TrailStep[] = []\n const ancestors: { entity: string; id: string }[] = []\n if (wanted.length > 0) {\n const rows = await deps.pathsClient.findMany({\n where: { locale: request.locale, isCanonical: true, path: { in: wanted } },\n limit: wanted.length,\n })\n const idsByType = new Map<string, string[]>()\n for (const row of rows) {\n const ids = idsByType.get(row.entityType)\n if (ids === undefined) idsByType.set(row.entityType, [row.entityId])\n else ids.push(row.entityId)\n }\n // Serial and bounded: one read per entity type among at most seven paths.\n const titles = new Map<string, string>()\n for (const [entityType, ids] of idsByType) {\n for (const [id, title] of await deps.publishedTitles(entityType, ids, request.locale)) {\n titles.set(`${entityType}\\u0000${id}`, title)\n }\n }\n const byPath = new Map(rows.map((row) => [row.path, row]))\n // Every document found at an ancestor path is something the trail DEPENDS\n // on — including one that is unpublished or untitled today and so adds no\n // step: publishing it tomorrow must refresh a cached page.\n for (const row of rows) ancestors.push({ entity: row.entityType, id: row.entityId })\n for (const path of wanted) {\n const row = byPath.get(path)\n if (row === undefined) continue\n const title = titles.get(`${row.entityType}\\u0000${row.entityId}`)\n if (title === undefined) continue\n steps.push({ label: title, href: path })\n }\n }\n // A trail is \"where this page sits UNDER something\": with no resolvable\n // ancestor there is none to draw (a lone current item is a landmark pointing\n // at itself), though what was found still gates the page's cache.\n if (steps.length === 0) return undefined\n if (request.label !== undefined) steps.push({ label: request.label })\n return { steps, ancestors }\n}\n"],"mappings":"sIAgEA,SAAgB,EAAc,EAAwB,CACpD,GAAI,CAAC,EAAK,WAAW,GAAG,EAAG,MAAO,CAAC,EACnC,IAAM,EAAW,EAAK,MAAM,GAAG,CAAC,CAAC,OAAQ,GAAY,IAAY,EAAE,EAC7D,EAAgB,CAAC,EACvB,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAS,OAAQ,IAC3C,EAAI,KAAK,IAAI,EAAS,MAAM,EAAG,CAAK,CAAC,CAAC,KAAK,GAAG,GAAG,EAEnD,OAAO,CACT,CAUA,eAAsB,EACpB,EAMA,EACsC,CAUtC,IAAM,GAAc,MATF,EAAK,YAAY,SAAS,CAC1C,MAAO,CACL,WAAY,EAAQ,WACpB,SAAU,CAAE,GAAI,CAAC,EAAQ,EAAE,CAAE,EAC7B,OAAQ,EAAQ,OAChB,YAAa,EACf,EACA,MAAO,CACT,CAAC,EAAA,CACuB,EAAE,EAAE,KAC5B,GAAI,IAAgB,IAAA,GAAW,OAG/B,IAAM,EAAS,EAAc,CAAW,CAAC,CAAC,MAAM,EAAsB,EAChE,EAAqB,CAAC,EACtB,EAA8C,CAAC,EACrD,GAAI,EAAO,OAAS,EAAG,CACrB,IAAM,EAAO,MAAM,EAAK,YAAY,SAAS,CAC3C,MAAO,CAAE,OAAQ,EAAQ,OAAQ,YAAa,GAAM,KAAM,CAAE,GAAI,CAAO,CAAE,EACzE,MAAO,EAAO,MAChB,CAAC,EACK,EAAY,IAAI,IACtB,IAAK,IAAM,KAAO,EAAM,CACtB,IAAM,EAAM,EAAU,IAAI,EAAI,UAAU,EACpC,IAAQ,IAAA,GAAW,EAAU,IAAI,EAAI,WAAY,CAAC,EAAI,QAAQ,CAAC,EAC9D,EAAI,KAAK,EAAI,QAAQ,CAC5B,CAEA,IAAM,EAAS,IAAI,IACnB,IAAK,GAAM,CAAC,EAAY,KAAQ,EAC9B,IAAK,GAAM,CAAC,EAAI,KAAU,MAAM,EAAK,gBAAgB,EAAY,EAAK,EAAQ,MAAM,EAClF,EAAO,IAAI,GAAG,EAAW,QAAQ,IAAM,CAAK,EAGhD,IAAM,EAAS,IAAI,IAAI,EAAK,IAAK,GAAQ,CAAC,EAAI,KAAM,CAAG,CAAC,CAAC,EAIzD,IAAK,IAAM,KAAO,EAAM,EAAU,KAAK,CAAE,OAAQ,EAAI,WAAY,GAAI,EAAI,QAAS,CAAC,EACnF,IAAK,IAAM,KAAQ,EAAQ,CACzB,IAAM,EAAM,EAAO,IAAI,CAAI,EAC3B,GAAI,IAAQ,IAAA,GAAW,SACvB,IAAM,EAAQ,EAAO,IAAI,GAAG,EAAI,WAAW,QAAQ,EAAI,UAAU,EAC7D,IAAU,IAAA,IACd,EAAM,KAAK,CAAE,MAAO,EAAO,KAAM,CAAK,CAAC,CACzC,CACF,CAII,KAAM,SAAW,EAErB,OADI,EAAQ,QAAU,IAAA,IAAW,EAAM,KAAK,CAAE,MAAO,EAAQ,KAAM,CAAC,EAC7D,CAAE,QAAO,WAAU,CAC5B"}
|
package/dist/client.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./versionable-Br_G5VGR.mjs";import{n as t,r as n,t as r}from"./errors-NRNILhhm.mjs";import{t as i}from"./content-config-store-8omQF5p5.mjs";import{a,d as o,f as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,r as h,s as g,t as _,u as v}from"./versions-
|
|
1
|
+
import{t as e}from"./versionable-Br_G5VGR.mjs";import{n as t,r as n,t as r}from"./errors-NRNILhhm.mjs";import{t as i}from"./content-config-store-8omQF5p5.mjs";import{a,d as o,f as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,r as h,s as g,t as _,u as v}from"./versions-B8feZTS9.mjs";import{n as y}from"./plugin-ChCEq_Qy.mjs";import{t as b}from"./locale-utils-B0mFoV47.mjs";import{i as x,n as S,r as C}from"./context-D7eEItCY.mjs";import{blockTreeFieldsOf as w,buildPartitionCondition as T}from"@murumets-ee/entity";import{getApp as E,getContext as D,requireSiteFor as O}from"@murumets-ee/core";import{schemaRegistry as k}from"@murumets-ee/db";import{and as A,inArray as j}from"drizzle-orm";import"server-only";import{createAdminClient as M}from"@murumets-ee/core/clients";function N(e){return{id:e.id,entityId:e.entityId,locale:e.locale,data:e.data,createdBy:e.createdBy,createdByName:e.createdByName??null,createdAt:e.createdAt,updatedAt:e.updatedAt}}async function P(e,n,r,i=`_`,a){b(i);let s=await C(e);if(!s)throw new t(e.entity.name,`drafts`);let c=o(e.entity,r);e.admin.assertBlockTreesValid(c);let l=w(e.entity).some(({name:e})=>Object.hasOwn(c,e))?await s.findOne({entityId:n,locale:i}):null;await e.admin.assertBlockTreesStrictForCaller(c,()=>l?.data),await e.admin.assertMediaKindsValid(c,l?.data);let u=new Date,d=a?.id??``,f=a?.name??null;return N(await s.upsert({entityId:n,locale:i,data:c,createdBy:d,createdByName:f,createdAt:u,updatedAt:u},{target:[`entityId`,`locale`],set:{data:c,updatedAt:u}}))}const F=[`slug`,`slugMode`,`parent`];async function I(e,n,r,i=`_`,a){b(i);let s=await C(e);if(!s)throw new t(e.entity.name,`drafts`);let c=o(e.entity,r);if(Object.keys(c).length===0){let e=await s.findOne({entityId:n,locale:i});return e?N(e):null}e.admin.assertBlockTreesValid(c),e.admin.assertUpdateValid(c);let l=w(e.entity).some(({name:e})=>Object.hasOwn(c,e)),u=e.entity.frontend?.path!==void 0&&F.some(e=>Object.hasOwn(c,e)),d=l||u?await s.findOne({entityId:n,locale:i}):null;if(await e.admin.assertBlockTreesStrictForCaller(c,()=>d?.data),await e.admin.assertMediaKindsValid(c,d?.data),u){let t={...d?.data??{},...c};if(i===`_`)await e.admin.assertPathAvailable(n,t);else{let r=e.entity.allFields,a={},o={};for(let[e,n]of Object.entries(t))r[e]?.translatable===!0?a[e]=n:o[e]=n;let s=e=>F.some(t=>Object.hasOwn(e,t)&&Object.hasOwn(c,t));s(a)&&await e.admin.assertPathAvailable(n,a,i),s(o)&&await e.admin.assertPathAvailable(n,o)}}let f=new Date;return N(await s.upsert({entityId:n,locale:i,data:c,createdBy:a?.id??``,createdByName:a?.name??null,createdAt:f,updatedAt:f},{target:[`entityId`,`locale`],set:{updatedAt:f},mergeJsonb:[`data`]}))}async function L(e,t,n=`_`){b(n);let r=await C(e);if(!r)return null;let i=await r.findOne({entityId:t,locale:n});return i?N(i):null}async function R(e,t,n=`_`){b(n);let r=await C(e);return r?(await r.deleteMany({entityId:t,locale:n})).length>0:!1}async function z(e,n,i=`_`,a){if(b(i),!await C(e))throw new t(e.entity.name,`drafts`);let c=e.versionableOptions?await e.admin.findById(n,m(i)):null;return e.db.transaction(async l=>{let u={...e,db:l},d=await C(u);if(!d)throw new t(e.entity.name,`drafts`);let[p]=await d.deleteMany({entityId:n,locale:i});if(!p)throw new r(n,i);let{publishedAt:m,...h}=p.data??{},g=await s(u,n,o(e.entity,{...h,status:`published`}),i,l),_=B(e.entity,h);return _!==void 0&&await e.admin.updateInternal(n,{_overrides:_},{tx:l}),e.versionableOptions&&await f(u,n,g,c,i,a),g})}function B(e,t){if(!(e.behaviors??[]).some(e=>e.name===`withOverrides`))return;let n=e.allFields._overrides;if(n===void 0||n.internal!==!0)return;let r=t._overrides;if(!(typeof r!=`object`||!r||Array.isArray(r)))return r}async function V(e,t,n=`_`){b(n);let r=await C(e);return r?r.exists({entityId:t,locale:n}):!1}async function H(e,t=`_`){b(t);let n=await C(e);return n?n.count({locale:t}):0}async function U(e,t,n=`_`){b(n);let r=new Set;if(t.length===0)return r;let i=await C(e);if(!i)return r;let a=await i.findMany({where:{entityId:{in:t},locale:n}});for(let e of a)r.add(e.entityId);return r}async function W(e,t,n=`_`){b(n);let r=await L(e,t,n);if(!r)return null;let a=i().defaultLocale,o=n===`_`?{defaultLocale:a}:{locale:n,defaultLocale:a},s=await e.admin.findById(t,o);if(!s)return null;let l=e.entity.allFields,u=s,f=r.data,p={};for(let e of Object.keys(f))p[e]=u[e];let m=c(p,f,l);for(let e of m)e.type===`blockTree`&&(e.blockDiffs=d(Array.isArray(e.from)?e.from:[],Array.isArray(e.to)?e.to:[]));return{diffs:m,current:s,draft:r}}async function G(e,t){let n=(await q(e,[t])).get(t);return n?Object.values(n):[]}async function K(e,n,r,a){b(r);let o=i();if(r===o.defaultLocale){let t={status:a};a===`published`&&(t.publishedAt=new Date),await e.admin.update(n,t);return}let s=await x(e);if(!s)throw new t(e.entity.name,`localeStatus`);let c={status:a};await e.admin.updateForLocale(n,c,r,{defaultLocale:o.defaultLocale,localeStatusWriter:(e,t,n)=>y(s,e,t,n)})}async function q(e,t){let n=new Map;if(t.length===0)return n;let r=await x(e);if(!r)return n;let i=await r.findMany({where:{entityId:{in:t}}});for(let e of i){let t=e.entityId,r=e.locale,i=n.get(t);i||(i={},n.set(t,i)),i[r]={locale:r,status:e.status,publishedAt:e.publishedAt??null}}return n}function J(){let e=i(),t=D();return{locales:(t?.availableLocales??e.locales.map(e=>e.code)).map(e=>({code:e})),defaultLocale:t?.defaultLocale??e.defaultLocale}}function Y(e,t,n,r){let i={};for(let a of n){if(a.code===r)continue;let n=t?.get(a.code)??{},o=[];for(let t of e)(n[t]===void 0||n[t]===null)&&o.push(t);i[a.code]={complete:o.length===0,total:e.length,translated:e.length-o.length,missing:o}}return i}async function X(e,t){return(await Z(e,[t])).get(t)??{}}async function Z(e,t){let n=new Map;if(t.length===0)return n;let{locales:r,defaultLocale:i}=J(),a=Object.entries(e.entity.allFields).filter(([e,t])=>t.translatable).map(([e])=>e);if(a.length===0)return n;let o=k.get(`${e.entity.name}_translations`);if(!o)return n;let s=await O(e.entity),c=j(o.entityId,t),l=await e.db.select().from(o).where(s===void 0?c:A(c,T(o,{site:s},e.entity.name))),u=new Map;for(let e of l){let t=e,n=t.entityId,r=t.locale,i={};for(let e of a)i[e]=t[e];let o=u.get(n);o||(o=new Map,u.set(n,o)),o.set(r,i)}for(let e of t)n.set(e,Y(a,u.get(e),r,i));return n}async function Q(e,t){let{locales:r,defaultLocale:i}=J(),a=t?.locale??r.find(e=>e.code!==i)?.code;if(!a)return{total:0,complete:0,partial:0,missing:0};if(!r.some(e=>e.code===a))throw new n(a,r.map(e=>e.code));let o=await e.admin.findMany();if(o.length===0)return{total:0,complete:0,partial:0,missing:0};let s=await Z(e,o.map(e=>e.id)),c=0,l=0,u=0;for(let e of o){let t=s.get(e.id)?.[a];!t||t.translated===0?u++:t.complete?c++:l++}return{total:o.length,complete:c,partial:l,missing:u}}var $=class{ctx;constructor(t){let n=e(t.entity.behaviors);this.ctx=S(t,n)}async create(e){let t=await this.ctx.admin.create(e);return this.ctx.versionableOptions&&await g(this.ctx,t.id,t,null,!1),t}findById(e,t){return this.ctx.admin.findById(e,t)}findMany(e){return this.ctx.admin.findMany(e)}async update(e,t){let n=null;this.ctx.versionableOptions&&(n=await this.ctx.admin.findById(e));let r=await this.ctx.admin.update(e,t);if(this.ctx.versionableOptions&&n){let t=v(n,r),i=await g(this.ctx,e,r,t,!1);if(i!==void 0)return await u(this.ctx,e,i),await a(this.ctx,e),{...r,_version:i};await a(this.ctx,e)}return r}delete(e){return this.ctx.admin.delete(e)}saveTranslation(e,t,n){return this.ctx.admin.saveTranslation(e,t,n)}deleteTranslation(e,t){return this.ctx.admin.deleteTranslation(e,t)}getVersions(e,t){return l(this.ctx,e,t)}getVersion(e,t,n){return h(this.ctx,e,t,n)}compareVersions(e,t,n,r){return _(this.ctx,e,t,n,r)}restoreVersion(e,t,n){return p(this.ctx,e,t,n)}pruneVersions(e,t){return a(this.ctx,e,t)}createVersionSnapshot(e,t,n,r,i){return f(this.ctx,e,t,n,r,i)}getTranslationStatus(e){return X(this.ctx,e)}getTranslationStatusBatch(e){return Z(this.ctx,e)}getTranslationReport(e){return Q(this.ctx,e)}getLocaleStatus(e){return G(this.ctx,e)}setLocaleStatus(e,t,n){return K(this.ctx,e,t,n)}getLocaleStatusBatch(e){return q(this.ctx,e)}saveDraft(e,t,n,r){return P(this.ctx,e,t,n,r)}mergeDraft(e,t,n,r){return I(this.ctx,e,t,n,r)}getDraft(e,t){return L(this.ctx,e,t)}deleteDraft(e,t){return R(this.ctx,e,t)}publishDraft(e,t,n){return z(this.ctx,e,t,n)}hasDraft(e,t){return V(this.ctx,e,t)}countDrafts(e){return H(this.ctx,e)}hasDraftBatch(e,t){return U(this.ctx,e,t)}compareDraftWithCurrent(e,t){return W(this.ctx,e,t)}};function ee(e){return new $({admin:M(e),entity:e,logger:E().logger.child({content:e.name})})}export{$ as ContentClient,ee as createContentClient};
|
|
2
2
|
//# sourceMappingURL=client.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enumerate-CYHJKQz6.mjs","names":[],"sources":["../src/paths/enumerate.ts"],"sourcesContent":["/**\n * Path enumeration for SSG / sitemaps (plan/routing PR06; phase\n * `05-resolution-and-enumeration-api.md` §2).\n *\n * `enumeratePaths` streams the PUBLISHED canonical `(path, entityType, id,\n * updatedAt)` rows for one locale. It powers `getStaticPaths` + the sitemap\n * helper, so it must NEVER leak an unpublished document (CLAUDE.md #1 — the\n * output feeds public build artifacts).\n *\n * **Security + availability.** Two properties matter here:\n * 1. *No unpublished leak.* `toolkit_paths` carries no publish-status column,\n * so each page's rows are publish-checked against the live entity via the\n * injected {@link EnumeratePathsDeps.filterPublished} — an unpublished (or\n * trashed) document is dropped from the stream.\n * 2. *No unbounded fan-out* (CLAUDE.md \"no amplification\"). Work is bounded by\n * PAGE SIZE, not by total document count: rows are keyset-paginated on the\n * immutable `id` PK (one index query per page), and the publish check is\n * BATCHED — ONE call per distinct entity-type bucket per page (a handful of\n * types at most; the routable set is config-time-fixed, never\n * attacker-influenced), never one call per row.\n *\n * **Leaf module.** Every cross-package dependency (the index reader, the publish\n * filter) is injected via `deps`; this module imports no `@murumets-ee/core` and\n * no `server-only` graph. Concrete deps are built from an app in\n * `./resolve-context.ts` (`buildEnumerateDeps`), keeping the resolution surface\n * off the `lumi` CLI command graph (F027 guardrail).\n *\n * Function-form-path entities need NO special handling — their rows were already\n * materialized into `toolkit_paths` at write time (F004 / phase 05 §2), so a\n * plain index read includes them.\n */\n\nimport { mapBounded } from './apply-path.js'\n\n// ---------------------------------------------------------------------------\n// Read surface (a `type` alias, per index-reader.ts's Deviation-1 rationale, so\n// the real `ToolkitPathsClient` stays assignable to it without any cast).\n// ---------------------------------------------------------------------------\n\n/** The projected `toolkit_paths` row the enumeration + batch lookup read. */\nexport type PathEnumRow = {\n id: string\n entityType: string\n entityId: string\n locale: string\n path: string\n updatedAt: Date\n}\n\n/**\n * The (operator-carrying) where shapes enumeration issues: keyset `id > cursor`,\n * `entityId IN (...)` for the batch lookup, plus plain-value equality filters.\n * A subset of the real client's `ColumnOperators` where-clause, so the real\n * `ToolkitPathsClient` is assignable to {@link PathEnumReader}.\n */\nexport type PathEnumWhere = {\n id?: { gt: string }\n entityType?: string\n entityId?: { in: string[] }\n locale?: string\n path?: string\n isCanonical?: boolean\n}\n\n/**\n * The batched keyset read surface {@link enumeratePaths} / {@link getCanonicalPaths}\n * need. NARROWER than Task 1's point-lookup `PathIndexReader` (which only orders\n * by `updatedAt` and takes plain-value where): enumeration needs `id`-ordering,\n * a `{ gt }` cursor, and `{ in }` batching. Construct the real client directly\n * (`toolkitPathsTable.makeClient(db)`) — it satisfies this type.\n */\nexport type PathEnumReader = {\n findMany(options: {\n where?: PathEnumWhere\n orderBy?: Array<{ column: 'id'; dir?: 'asc' | 'desc' }>\n limit?: number\n }): Promise<PathEnumRow[]>\n}\n\n/**\n * Batch publish check: return the SUBSET of `entityIds` that is published in\n * `locale`. Called ONCE per entity-type group per page. The concrete\n * implementation (see `resolve-context.ts`) runs an elevated-context\n * `QueryClient.findMany({ where: inArray(getTable().id, ids), locale })` — so no\n * unpublished/trashed id comes back. Since D017 the publish filter is\n * CONDITIONAL (it holds for any principal without `view_unpublished`); what\n * keeps that true here is that the elevated context is a REACH context which\n * withholds that action, not that the filter is unconditional.\n */\nexport type PublishFilter = (\n entityType: string,\n entityIds: readonly string[],\n locale: string,\n) => Promise<ReadonlySet<string>>\n\n// ---------------------------------------------------------------------------\n// Public surface\n// ---------------------------------------------------------------------------\n\n/** One enumerated path row (phase 05 §2 — exact shape). */\nexport interface PathEntry {\n path: string\n entityType: string\n id: string\n updatedAt: Date\n}\n\n/** Options for {@link enumeratePaths} (phase 05 §2 — exact shape). */\nexport interface EnumeratePathsOptions {\n /** Enumerate this locale's canonical rows only. */\n locale: string\n /** Optionally narrow to a single routable entity type. */\n entityType?: string\n /**\n * Resume the keyset AFTER this `toolkit_paths` row id (exclusive); omit to\n * start from the beginning. The value is the opaque `toolkit_paths` PK the\n * internal page cursor advances on — NOT the yielded `PathEntry.id`, which is\n * the DOCUMENT (entity) id. A cross-request paginating caller (e.g. the\n * content-api `_paths` endpoint) resolves the last consumed entry's row PK and\n * passes it here to continue exactly where the prior page stopped, with no row\n * skipped or repeated. Fully backward-compatible: existing callers omit it and\n * traverse from the start unchanged.\n */\n afterId?: string\n}\n\n/** Injected dependencies for {@link enumeratePaths}. */\nexport interface EnumeratePathsDeps {\n pathsClient: PathEnumReader\n filterPublished: PublishFilter\n}\n\n/** Internal tuning seam (page size / batch concurrency). Real callers omit it. */\nexport interface EnumeratePathsTuning {\n /** Rows per index page. Defaults to {@link ENUMERATE_PAGE_SIZE}. Tests force a small value. */\n pageSize?: number\n /** Peak concurrent publish-check groups. Defaults to {@link PUBLISH_CHECK_CONCURRENCY}. */\n publishConcurrency?: number\n}\n\n/**\n * Index page size — mirrors the rebuild job's `REBUILD_PAGE_SIZE`. Must stay\n * ≤ the `defineTable` `MAX_LIMIT` (1000), which THROWS rather than clamps.\n */\nconst ENUMERATE_PAGE_SIZE = 200\n\n/**\n * Peak concurrent publish-check groups per page. A page holds a handful of\n * distinct entity types at most, so this bound is rarely reached — it exists so\n * the fan-out is EXPLICITLY capped (CLAUDE.md), never `Promise.all` over an\n * unbounded set.\n */\nconst PUBLISH_CHECK_CONCURRENCY = 5\n\n/**\n * Stream the published canonical paths for `opts.locale` (optionally narrowed to\n * `opts.entityType`), keyset-paginated with a per-page batched publish filter.\n * See the module header for the security + fan-out guarantees.\n */\nexport async function* enumeratePaths(\n opts: EnumeratePathsOptions,\n deps: EnumeratePathsDeps,\n tuning: EnumeratePathsTuning = {},\n): AsyncGenerator<PathEntry> {\n const pageSize = tuning.pageSize ?? ENUMERATE_PAGE_SIZE\n const concurrency = tuning.publishConcurrency ?? PUBLISH_CHECK_CONCURRENCY\n const { locale, entityType } = opts\n\n // Seed from `opts.afterId` so a caller can resume mid-stream; undefined starts\n // at the beginning (every existing caller's behavior, unchanged).\n let cursorId: string | undefined = opts.afterId\n for (;;) {\n // Keyset cursor on the immutable `id` PK (the rebuild job's precedent):\n // visits every canonical row exactly once even under concurrent mutation,\n // which OFFSET paging cannot guarantee.\n const where: PathEnumWhere = { locale, isCanonical: true }\n if (entityType !== undefined) where.entityType = entityType\n if (cursorId !== undefined) where.id = { gt: cursorId }\n\n const page = await deps.pathsClient.findMany({\n where,\n orderBy: [{ column: 'id', dir: 'asc' }],\n limit: pageSize,\n })\n if (page.length === 0) break\n\n // Group this page's rows by entity type (bounded: the routable-type set is\n // small + config-fixed), then ONE publish check per group.\n const byType = new Map<string, PathEnumRow[]>()\n for (const row of page) {\n const bucket = byType.get(row.entityType)\n if (bucket) bucket.push(row)\n else byType.set(row.entityType, [row])\n }\n\n const publishedByType = new Map<string, ReadonlySet<string>>()\n await mapBounded([...byType.entries()], concurrency, async ([type, rows]) => {\n const published = await deps.filterPublished(\n type,\n rows.map((r) => r.entityId),\n locale,\n )\n publishedByType.set(type, published)\n })\n\n // Yield only the page's PUBLISHED rows, preserving the id-ascending order.\n for (const row of page) {\n if (publishedByType.get(row.entityType)?.has(row.entityId)) {\n yield { path: row.path, entityType: row.entityType, id: row.entityId, updatedAt: row.updatedAt }\n }\n }\n\n // Advance the cursor to the last (max-id) row of this raw page. `page` is\n // non-empty here (length-0 broke above). Publish-filtering is a POST-filter,\n // so the cursor always steps over every index row regardless of publish state.\n const lastRow = page[page.length - 1]\n if (!lastRow) break\n cursorId = lastRow.id\n if (page.length < pageSize) break\n }\n}\n\n// ---------------------------------------------------------------------------\n// getCanonicalPaths — batched forward lookup (search url re-point), NO publish filter\n// ---------------------------------------------------------------------------\n\n/** Deps for {@link getCanonicalPaths}. */\nexport interface CanonicalPathsBatchDeps {\n pathsClient: PathEnumReader\n}\n\n/**\n * Max ids per batched index query — well under `defineTable`'s `MAX_LIMIT`\n * (1000, which throws rather than clamps). Search result pages are far smaller\n * than this, so this is one query in practice; larger id sets chunk safely.\n */\nconst CANONICAL_BATCH_SIZE = 500\n\n/**\n * The current canonical paths for many `(entityType, entityId)` in ONE locale,\n * as an `entityId → path` map — the batched sibling of `getCanonicalPath`.\n *\n * **No publish filter — deliberate**, matching `getCanonicalPath` (and the\n * search url builder it re-points): a search result's url is display-only and\n * the caller already gates row visibility. One index query per {@link\n * CANONICAL_BATCH_SIZE}-sized chunk (one query total for a normal search page),\n * so it never fans out per row.\n */\nexport async function getCanonicalPaths(\n entityType: string,\n entityIds: readonly string[],\n locale: string,\n deps: CanonicalPathsBatchDeps,\n): Promise<Map<string, string>> {\n const result = new Map<string, string>()\n for (let i = 0; i < entityIds.length; i += CANONICAL_BATCH_SIZE) {\n const chunk = entityIds.slice(i, i + CANONICAL_BATCH_SIZE)\n const rows = await deps.pathsClient.findMany({\n where: { entityType, locale, isCanonical: true, entityId: { in: [...chunk] } },\n limit: chunk.length,\n })\n for (const row of rows) result.set(row.entityId, row.path)\n }\n return result\n}\n"],"mappings":"sJA+JA,eAAuB,EACrB,EACA,EACA,EAA+B,CAAC,EACL,CAC3B,IAAM,EAAW,EAAO,UAAY,IAC9B,EAAc,EAAO,oBAAsB,EAC3C,CAAE,SAAQ,cAAe,EAI3B,EAA+B,EAAK,QACxC,OAAS,CAIP,IAAM,EAAuB,CAAE,SAAQ,YAAa,EAAK,EACrD,IAAe,IAAA,KAAW,EAAM,WAAa,GAC7C,IAAa,IAAA,KAAW,EAAM,GAAK,CAAE,GAAI,CAAS,GAEtD,IAAM,EAAO,MAAM,EAAK,YAAY,SAAS,CAC3C,QACA,QAAS,CAAC,CAAE,OAAQ,KAAM,IAAK,KAAM,CAAC,EACtC,MAAO,CACT,CAAC,EACD,GAAI,EAAK,SAAW,EAAG,MAIvB,IAAM,EAAS,IAAI,IACnB,IAAK,IAAM,KAAO,EAAM,CACtB,IAAM,EAAS,EAAO,IAAI,EAAI,UAAU,EACpC,EAAQ,EAAO,KAAK,CAAG,EACtB,EAAO,IAAI,EAAI,WAAY,CAAC,CAAG,CAAC,CACvC,CAEA,IAAM,EAAkB,IAAI,IAC5B,MAAM,EAAW,CAAC,GAAG,EAAO,QAAQ,CAAC,EAAG,EAAa,MAAO,CAAC,EAAM,KAAU,CAC3E,IAAM,EAAY,MAAM,EAAK,gBAC3B,EACA,EAAK,IAAK,GAAM,EAAE,QAAQ,EAC1B,CACF,EACA,EAAgB,IAAI,EAAM,CAAS,CACrC,CAAC,EAGD,IAAK,IAAM,KAAO,EACZ,EAAgB,IAAI,EAAI,UAAU,CAAC,EAAE,IAAI,EAAI,QAAQ,IACvD,KAAM,CAAE,KAAM,EAAI,KAAM,WAAY,EAAI,WAAY,GAAI,EAAI,SAAU,UAAW,EAAI,SAAU,GAOnG,IAAM,EAAU,EAAK,EAAK,OAAS,GAGnC,GAFI,CAAC,IACL,EAAW,EAAQ,GACf,EAAK,OAAS,GAAU,KAC9B,CACF,CA4BA,eAAsB,EACpB,EACA,EACA,EACA,EAC8B,CAC9B,IAAM,EAAS,IAAI,IACnB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAU,OAAQ,GAAK,IAAsB,CAC/D,IAAM,EAAQ,EAAU,MAAM,EAAG,EAAI,GAAoB,EACnD,EAAO,MAAM,EAAK,YAAY,SAAS,CAC3C,MAAO,CAAE,aAAY,SAAQ,YAAa,GAAM,SAAU,CAAE,GAAI,CAAC,GAAG,CAAK,CAAE,CAAE,EAC7E,MAAO,EAAM,MACf,CAAC,EACD,IAAK,IAAM,KAAO,EAAM,EAAO,IAAI,EAAI,SAAU,EAAI,IAAI,CAC3D,CACA,OAAO,CACT"}
|
|
1
|
+
{"version":3,"file":"enumerate-CYHJKQz6.mjs","names":[],"sources":["../src/paths/enumerate.ts"],"sourcesContent":["/**\n * Path enumeration for SSG / sitemaps (plan/routing PR06; phase\n * `05-resolution-and-enumeration-api.md` §2).\n *\n * `enumeratePaths` streams the PUBLISHED canonical `(path, entityType, id,\n * updatedAt)` rows for one locale. It powers `getStaticPaths` + the sitemap\n * helper, so it must NEVER leak an unpublished document (CLAUDE.md #1 — the\n * output feeds public build artifacts).\n *\n * **Security + availability.** Two properties matter here:\n * 1. *No unpublished leak.* `toolkit_paths` carries no publish-status column,\n * so each page's rows are publish-checked against the live entity via the\n * injected {@link EnumeratePathsDeps.filterPublished} — an unpublished (or\n * trashed) document is dropped from the stream.\n * 2. *No unbounded fan-out* (CLAUDE.md \"no amplification\"). Work is bounded by\n * PAGE SIZE, not by total document count: rows are keyset-paginated on the\n * immutable `id` PK (one index query per page), and the publish check is\n * BATCHED — ONE call per distinct entity-type bucket per page (a handful of\n * types at most; the routable set is config-time-fixed, never\n * attacker-influenced), never one call per row.\n *\n * **Leaf module.** Every cross-package dependency (the index reader, the publish\n * filter) is injected via `deps`; this module imports no `@murumets-ee/core` and\n * no `server-only` graph. Concrete deps are built from an app in\n * `./resolve-context.ts` (`buildEnumerateDeps`), keeping the resolution surface\n * off the `lumi` CLI command graph (F027 guardrail).\n *\n * Function-form-path entities need NO special handling — their rows were already\n * materialized into `toolkit_paths` at write time (F004 / phase 05 §2), so a\n * plain index read includes them.\n */\n\nimport { mapBounded } from './apply-path.js'\n\n// ---------------------------------------------------------------------------\n// Read surface (a `type` alias, per index-reader.ts's Deviation-1 rationale, so\n// the real `ToolkitPathsClient` stays assignable to it without any cast).\n// ---------------------------------------------------------------------------\n\n/** The projected `toolkit_paths` row the enumeration + batch lookup read. */\nexport type PathEnumRow = {\n id: string\n entityType: string\n entityId: string\n locale: string\n path: string\n updatedAt: Date\n}\n\n/**\n * The (operator-carrying) where shapes enumeration issues: keyset `id > cursor`,\n * `entityId IN (...)` for the batch lookup, plus plain-value equality filters.\n * A subset of the real client's `ColumnOperators` where-clause, so the real\n * `ToolkitPathsClient` is assignable to {@link PathEnumReader}.\n */\nexport type PathEnumWhere = {\n id?: { gt: string }\n entityType?: string\n entityId?: { in: string[] }\n locale?: string\n /** `{ in }` for a batched membership read (`resolvePublishedPathsAmong`). */\n path?: string | { in: string[] }\n isCanonical?: boolean\n}\n\n/**\n * The batched keyset read surface {@link enumeratePaths} / {@link getCanonicalPaths}\n * need. NARROWER than Task 1's point-lookup `PathIndexReader` (which only orders\n * by `updatedAt` and takes plain-value where): enumeration needs `id`-ordering,\n * a `{ gt }` cursor, and `{ in }` batching. Construct the real client directly\n * (`toolkitPathsTable.makeClient(db)`) — it satisfies this type.\n */\nexport type PathEnumReader = {\n findMany(options: {\n where?: PathEnumWhere\n orderBy?: Array<{ column: 'id'; dir?: 'asc' | 'desc' }>\n limit?: number\n }): Promise<PathEnumRow[]>\n}\n\n/**\n * Batch publish check: return the SUBSET of `entityIds` that is published in\n * `locale`. Called ONCE per entity-type group per page. The concrete\n * implementation (see `resolve-context.ts`) runs an elevated-context\n * `QueryClient.findMany({ where: inArray(getTable().id, ids), locale })` — so no\n * unpublished/trashed id comes back. Since D017 the publish filter is\n * CONDITIONAL (it holds for any principal without `view_unpublished`); what\n * keeps that true here is that the elevated context is a REACH context which\n * withholds that action, not that the filter is unconditional.\n */\nexport type PublishFilter = (\n entityType: string,\n entityIds: readonly string[],\n locale: string,\n) => Promise<ReadonlySet<string>>\n\n// ---------------------------------------------------------------------------\n// Public surface\n// ---------------------------------------------------------------------------\n\n/** One enumerated path row (phase 05 §2 — exact shape). */\nexport interface PathEntry {\n path: string\n entityType: string\n id: string\n updatedAt: Date\n}\n\n/** Options for {@link enumeratePaths} (phase 05 §2 — exact shape). */\nexport interface EnumeratePathsOptions {\n /** Enumerate this locale's canonical rows only. */\n locale: string\n /** Optionally narrow to a single routable entity type. */\n entityType?: string\n /**\n * Resume the keyset AFTER this `toolkit_paths` row id (exclusive); omit to\n * start from the beginning. The value is the opaque `toolkit_paths` PK the\n * internal page cursor advances on — NOT the yielded `PathEntry.id`, which is\n * the DOCUMENT (entity) id. A cross-request paginating caller (e.g. the\n * content-api `_paths` endpoint) resolves the last consumed entry's row PK and\n * passes it here to continue exactly where the prior page stopped, with no row\n * skipped or repeated. Fully backward-compatible: existing callers omit it and\n * traverse from the start unchanged.\n */\n afterId?: string\n}\n\n/** Injected dependencies for {@link enumeratePaths}. */\nexport interface EnumeratePathsDeps {\n pathsClient: PathEnumReader\n filterPublished: PublishFilter\n}\n\n/** Internal tuning seam (page size / batch concurrency). Real callers omit it. */\nexport interface EnumeratePathsTuning {\n /** Rows per index page. Defaults to {@link ENUMERATE_PAGE_SIZE}. Tests force a small value. */\n pageSize?: number\n /** Peak concurrent publish-check groups. Defaults to {@link PUBLISH_CHECK_CONCURRENCY}. */\n publishConcurrency?: number\n}\n\n/**\n * Index page size — mirrors the rebuild job's `REBUILD_PAGE_SIZE`. Must stay\n * ≤ the `defineTable` `MAX_LIMIT` (1000), which THROWS rather than clamps.\n */\nconst ENUMERATE_PAGE_SIZE = 200\n\n/**\n * Peak concurrent publish-check groups per page. A page holds a handful of\n * distinct entity types at most, so this bound is rarely reached — it exists so\n * the fan-out is EXPLICITLY capped (CLAUDE.md), never `Promise.all` over an\n * unbounded set.\n */\nconst PUBLISH_CHECK_CONCURRENCY = 5\n\n/**\n * Stream the published canonical paths for `opts.locale` (optionally narrowed to\n * `opts.entityType`), keyset-paginated with a per-page batched publish filter.\n * See the module header for the security + fan-out guarantees.\n */\nexport async function* enumeratePaths(\n opts: EnumeratePathsOptions,\n deps: EnumeratePathsDeps,\n tuning: EnumeratePathsTuning = {},\n): AsyncGenerator<PathEntry> {\n const pageSize = tuning.pageSize ?? ENUMERATE_PAGE_SIZE\n const concurrency = tuning.publishConcurrency ?? PUBLISH_CHECK_CONCURRENCY\n const { locale, entityType } = opts\n\n // Seed from `opts.afterId` so a caller can resume mid-stream; undefined starts\n // at the beginning (every existing caller's behavior, unchanged).\n let cursorId: string | undefined = opts.afterId\n for (;;) {\n // Keyset cursor on the immutable `id` PK (the rebuild job's precedent):\n // visits every canonical row exactly once even under concurrent mutation,\n // which OFFSET paging cannot guarantee.\n const where: PathEnumWhere = { locale, isCanonical: true }\n if (entityType !== undefined) where.entityType = entityType\n if (cursorId !== undefined) where.id = { gt: cursorId }\n\n const page = await deps.pathsClient.findMany({\n where,\n orderBy: [{ column: 'id', dir: 'asc' }],\n limit: pageSize,\n })\n if (page.length === 0) break\n\n // Group this page's rows by entity type (bounded: the routable-type set is\n // small + config-fixed), then ONE publish check per group.\n const byType = new Map<string, PathEnumRow[]>()\n for (const row of page) {\n const bucket = byType.get(row.entityType)\n if (bucket) bucket.push(row)\n else byType.set(row.entityType, [row])\n }\n\n const publishedByType = new Map<string, ReadonlySet<string>>()\n await mapBounded([...byType.entries()], concurrency, async ([type, rows]) => {\n const published = await deps.filterPublished(\n type,\n rows.map((r) => r.entityId),\n locale,\n )\n publishedByType.set(type, published)\n })\n\n // Yield only the page's PUBLISHED rows, preserving the id-ascending order.\n for (const row of page) {\n if (publishedByType.get(row.entityType)?.has(row.entityId)) {\n yield {\n path: row.path,\n entityType: row.entityType,\n id: row.entityId,\n updatedAt: row.updatedAt,\n }\n }\n }\n\n // Advance the cursor to the last (max-id) row of this raw page. `page` is\n // non-empty here (length-0 broke above). Publish-filtering is a POST-filter,\n // so the cursor always steps over every index row regardless of publish state.\n const lastRow = page[page.length - 1]\n if (!lastRow) break\n cursorId = lastRow.id\n if (page.length < pageSize) break\n }\n}\n\n// ---------------------------------------------------------------------------\n// getCanonicalPaths — batched forward lookup (search url re-point), NO publish filter\n// ---------------------------------------------------------------------------\n\n/** Deps for {@link getCanonicalPaths}. */\nexport interface CanonicalPathsBatchDeps {\n pathsClient: PathEnumReader\n}\n\n/**\n * Max ids per batched index query — well under `defineTable`'s `MAX_LIMIT`\n * (1000, which throws rather than clamps). Search result pages are far smaller\n * than this, so this is one query in practice; larger id sets chunk safely.\n */\nconst CANONICAL_BATCH_SIZE = 500\n\n/**\n * The current canonical paths for many `(entityType, entityId)` in ONE locale,\n * as an `entityId → path` map — the batched sibling of `getCanonicalPath`.\n *\n * **No publish filter — deliberate**, matching `getCanonicalPath` (and the\n * search url builder it re-points): a search result's url is display-only and\n * the caller already gates row visibility. One index query per {@link\n * CANONICAL_BATCH_SIZE}-sized chunk (one query total for a normal search page),\n * so it never fans out per row.\n */\nexport async function getCanonicalPaths(\n entityType: string,\n entityIds: readonly string[],\n locale: string,\n deps: CanonicalPathsBatchDeps,\n): Promise<Map<string, string>> {\n const result = new Map<string, string>()\n for (let i = 0; i < entityIds.length; i += CANONICAL_BATCH_SIZE) {\n const chunk = entityIds.slice(i, i + CANONICAL_BATCH_SIZE)\n const rows = await deps.pathsClient.findMany({\n where: { entityType, locale, isCanonical: true, entityId: { in: [...chunk] } },\n limit: chunk.length,\n })\n for (const row of rows) result.set(row.entityId, row.path)\n }\n return result\n}\n"],"mappings":"sJAgKA,eAAuB,EACrB,EACA,EACA,EAA+B,CAAC,EACL,CAC3B,IAAM,EAAW,EAAO,UAAY,IAC9B,EAAc,EAAO,oBAAsB,EAC3C,CAAE,SAAQ,cAAe,EAI3B,EAA+B,EAAK,QACxC,OAAS,CAIP,IAAM,EAAuB,CAAE,SAAQ,YAAa,EAAK,EACrD,IAAe,IAAA,KAAW,EAAM,WAAa,GAC7C,IAAa,IAAA,KAAW,EAAM,GAAK,CAAE,GAAI,CAAS,GAEtD,IAAM,EAAO,MAAM,EAAK,YAAY,SAAS,CAC3C,QACA,QAAS,CAAC,CAAE,OAAQ,KAAM,IAAK,KAAM,CAAC,EACtC,MAAO,CACT,CAAC,EACD,GAAI,EAAK,SAAW,EAAG,MAIvB,IAAM,EAAS,IAAI,IACnB,IAAK,IAAM,KAAO,EAAM,CACtB,IAAM,EAAS,EAAO,IAAI,EAAI,UAAU,EACpC,EAAQ,EAAO,KAAK,CAAG,EACtB,EAAO,IAAI,EAAI,WAAY,CAAC,CAAG,CAAC,CACvC,CAEA,IAAM,EAAkB,IAAI,IAC5B,MAAM,EAAW,CAAC,GAAG,EAAO,QAAQ,CAAC,EAAG,EAAa,MAAO,CAAC,EAAM,KAAU,CAC3E,IAAM,EAAY,MAAM,EAAK,gBAC3B,EACA,EAAK,IAAK,GAAM,EAAE,QAAQ,EAC1B,CACF,EACA,EAAgB,IAAI,EAAM,CAAS,CACrC,CAAC,EAGD,IAAK,IAAM,KAAO,EACZ,EAAgB,IAAI,EAAI,UAAU,CAAC,EAAE,IAAI,EAAI,QAAQ,IACvD,KAAM,CACJ,KAAM,EAAI,KACV,WAAY,EAAI,WAChB,GAAI,EAAI,SACR,UAAW,EAAI,SACjB,GAOJ,IAAM,EAAU,EAAK,EAAK,OAAS,GAGnC,GAFI,CAAC,IACL,EAAW,EAAQ,GACf,EAAK,OAAS,GAAU,KAC9B,CACF,CA4BA,eAAsB,EACpB,EACA,EACA,EACA,EAC8B,CAC9B,IAAM,EAAS,IAAI,IACnB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAU,OAAQ,GAAK,IAAsB,CAC/D,IAAM,EAAQ,EAAU,MAAM,EAAG,EAAI,GAAoB,EACnD,EAAO,MAAM,EAAK,YAAY,SAAS,CAC3C,MAAO,CAAE,aAAY,SAAQ,YAAa,GAAM,SAAU,CAAE,GAAI,CAAC,GAAG,CAAK,CAAE,CAAE,EAC7E,MAAO,EAAM,MACf,CAAC,EACD,IAAK,IAAM,KAAO,EAAM,EAAO,IAAI,EAAI,SAAU,EAAI,IAAI,CAC3D,CACA,OAAO,CACT"}
|
|
@@ -1,2 +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-
|
|
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-DlvqU8bO.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-B8feZTS9.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 de(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 fe=Symbol.for(`@murumets-ee/content:path-writers-factory`),Y=Symbol.for(`@murumets-ee/content:cascade-trigger-factory`);function pe(e){globalThis[Y]=e}function X(){let e=globalThis[Y];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=X();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 de(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[fe]=(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`}))}),pe(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-D8UXwWwr.mjs`).then(e=>e.u),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)},Te=async(e,t)=>{if(e.length===0)return new Set;let{buildEnumerateDeps:n}=await import(`./resolve-context-D8UXwWwr.mjs`).then(e=>e.u),r=n(A()),i=[...new Set(e)],a=i.slice(0,48);i.length>a.length&&A().logger.warn({paths:i.length,max:48},`content: publishedPathsAmong was asked for more paths than it reads; the rest count as unpublished`);let o=await r.pathsClient.findMany({where:{locale:t,isCanonical:!0,path:{in:a}},limit:a.length}),s=new Map;for(let e of o){let t=s.get(e.entityType);t===void 0&&(t=new Map,s.set(e.entityType,t)),t.set(e.entityId,e.path)}let c=new Set;for(let[e,n]of s){let i=await r.filterPublished(e,[...n.keys()],t);for(let[e,t]of n)i.has(e)&&c.add(t)}return c},Ee=async e=>{let[{buildBreadcrumbDeps:t},{resolveBreadcrumbTrail:n}]=await Promise.all([import(`./resolve-context-D8UXwWwr.mjs`).then(e=>e.u),import(`./breadcrumbs-BqSuMNac.mjs`).then(e=>e.t)]);return(await n(e,t(A())))?.steps};function De(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 Oe(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 ke(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 Ae(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,"blocks.publishedPathsAmong":Te,"blocks.breadcrumbTrail":Ee,...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=ke(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=Oe(i);H.register(t,n.table),s(e,{localeStatus:n})}r++}if(t){let t=`${e}_drafts`;if(!H.has(t)){let n=De(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,Ae as t};
|
|
2
|
+
//# sourceMappingURL=plugin-ChCEq_Qy.mjs.map
|