@murumets-ee/content 0.75.1 → 0.77.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.
Files changed (39) hide show
  1. package/dist/admin.mjs +1 -1
  2. package/dist/block-resolver.d.mts +38 -1
  3. package/dist/block-resolver.d.mts.map +1 -1
  4. package/dist/block-resolver.mjs +1 -1
  5. package/dist/block-resolver.mjs.map +1 -1
  6. package/dist/client.mjs +1 -1
  7. package/dist/enumerate-CBqu63Kp.mjs +2 -0
  8. package/dist/{enumerate-B5tWe9JC.mjs.map → enumerate-CBqu63Kp.mjs.map} +1 -1
  9. package/dist/index-C2G4IJck.d.mts.map +1 -1
  10. package/dist/index.mjs +1 -1
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/plugin-BxOHoKPR.mjs +2 -0
  13. package/dist/{plugin-DEYaEeWC.mjs.map → plugin-BxOHoKPR.mjs.map} +1 -1
  14. package/dist/plugin.mjs +1 -1
  15. package/dist/resolve-context-MRZZ3rQl.mjs +2 -0
  16. package/dist/{resolve-context-rr-Eanwp.mjs.map → resolve-context-MRZZ3rQl.mjs.map} +1 -1
  17. package/dist/resolve.mjs +2 -2
  18. package/dist/resolve.mjs.map +1 -1
  19. package/dist/routes-C9I1iZw8.mjs +2 -0
  20. package/dist/{routes-DuJa-w4T.mjs.map → routes-C9I1iZw8.mjs.map} +1 -1
  21. package/dist/schema-0zayzpD_.mjs +2 -0
  22. package/dist/schema-0zayzpD_.mjs.map +1 -0
  23. package/dist/schema-D8sBQozw.mjs +2 -0
  24. package/dist/schema-D8sBQozw.mjs.map +1 -0
  25. package/dist/seed.d.mts +20 -1
  26. package/dist/seed.d.mts.map +1 -1
  27. package/dist/seed.mjs +1 -1
  28. package/dist/seed.mjs.map +1 -1
  29. package/dist/{versions-qld3n7B-.mjs → versions-BDxB6k_S.mjs} +2 -2
  30. package/dist/{versions-qld3n7B-.mjs.map → versions-BDxB6k_S.mjs.map} +1 -1
  31. package/package.json +9 -9
  32. package/dist/apply-path-s-YeCUXP.mjs +0 -2
  33. package/dist/apply-path-s-YeCUXP.mjs.map +0 -1
  34. package/dist/enumerate-B5tWe9JC.mjs +0 -2
  35. package/dist/plugin-DEYaEeWC.mjs +0 -2
  36. package/dist/resolve-context-rr-Eanwp.mjs +0 -2
  37. package/dist/routes-DuJa-w4T.mjs +0 -2
  38. package/dist/schema-BPDn8ajz.mjs +0 -2
  39. package/dist/schema-BPDn8ajz.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index-C2G4IJck.d.mts","names":[],"sources":["../src/behaviors/with-overrides.ts","../src/behaviors/workflowable.ts","../src/capabilities.ts","../src/diff/types.ts","../src/errors.ts","../src/locale-registry.ts","../src/message-registry.ts"],"mappings":";;;;;;;;;;;KAqDK,iBAAA;EACC,IAAA;AAAA;EAEA,IAAA;EACA,KAAA,GAAQ,MAAA;EACR,MAAA,GAAS,MAAA,SAAe,MAAA;AAAA;EAExB,IAAA;EAAiB,KAAA,EAAO,MAAA;AAAA;AAAA,KAEzB,YAAA,GAAe,MAAM,SAAS,iBAAA;AAAA,KAEvB,mBAAA;EAcV,UAAA,EAAY,SAAS;AAAA;;;AC/B4B;AASnD;iBD4LgB,aAAA,CAAA,GAAiB,QAAQ,CAAC,mBAAA;;;;;;iBA4C1B,gBAAA,CACd,SAAA,EAAW,aAAa;EAAG,IAAA;AAAA,IAAiB,MAAA;;;cCnPjC,eAAA;AAAA,KACD,aAAA,WAAwB,eAAe;ADqMnD;;;;AAA6D;AA4C7D;;AA5CA,cC5La,oBAAA;EAAA;;;;;;ADyOqE;;;AAAA,iBC9NlE,mBAAA,CAAoB,IAAA,EAAM,aAAA,EAAe,EAAA,EAAI,aAAa;AArB1E;;;;AAAA,cA+Ba,uBAAA,SAAgC,KAAA;EAAA,SAClC,IAAA,EAAM,aAAA;EAAA,SACN,EAAA,EAAI,aAAA;cAED,IAAA,EAAM,aAAA,EAAe,EAAA,EAAI,aAAA;AAAA;AAAA,KAW3B,kBAAA;EACV,eAAA,EAAiB,WAAA;IAAgB,QAAA;EAAA;EACjC,iBAAA,EAAmB,SAAA;EACnB,aAAA,EAAe,SAAA;EACf,kBAAA,EAAoB,SAAA;AAAA;AAAA,iBAGN,YAAA,CAAA,GAAgB,QAAQ,CAAC,kBAAA;;;KC7E7B,gBAAA;;;;;;iBAOI,cAAA,CAAe,MAAA,EAAQ,MAAA,EAAQ,UAAA,EAAY,gBAAgB;;;;;;;UCxB1D,WAAA;EACf,IAAA;EACA,KAAK;AAAA;AHsCuD;AAAA,UGlC7C,SAAA;EACf,KAAA;EACA,IAAA;EAWA,IAAA;EACA,EAAA;EHmC4B;EGjC5B,QAAA,GAAW,WAAA;EHiCuB;EG/BlC,UAAA,GAAa,cAAc;AAAA;;KAIjB,eAAA;;UAGK,cAAA;EACf,OAAA;EACA,SAAA;EACA,MAAA,EAAQ,eAAA;EACR,SAAA;EACA,OAAA;EHmBkC;EGjBlC,SAAA,GAAY,QAAQ;AAAA;;UAIL,QAAA;EACf,KAAA;EACA,IAAA;EACA,EAAA;;EAEA,QAAA,GAAW,WAAW;AAAA;;;;;;;;;;;AHPsC;;;;cIhCjD,qBAAA,SAA8B,KAAK;EAAA,SACrC,UAAA;EAAA,SACA,UAAA;cAGP,UAAA,UACA,UAAA,EAAY,qBAAA,gBACZ,OAAA;AAAA;;;;;;;;;;cAsBS,kBAAA,SAA2B,KAAK;EAAA,SAClC,QAAA;EAAA,SACA,MAAA;cAEG,QAAA,UAAkB,MAAA;AAAA;;;AJgBoB;AAEpD;;;;AAcuB;AAsKvB;;;;cI5Ka,oBAAA,SAA6B,KAAK;EAAA,SACpC,QAAA;EAAA,SACA,OAAA;EAAA,SACA,MAAA;cAEG,QAAA,UAAkB,OAAA,UAAiB,MAAA;AAAA;;;;;AJoNiC;;;;ACnPlF;cGiDa,kBAAA,SAA2B,KAAK;EAAA,SAClC,MAAA;EAAA,SACA,OAAA;cAEG,MAAA,UAAgB,OAAA;AAAA;;;;;;;;iBCZR,uBAAA,CAAwB,GAAA,EAAK,UAAA,GAAa,OAAA,CAAQ,WAAA;;iBAoBxD,kBAAA,CAAmB,OAA+B,WAAb,WAAW;;;AL/C5B;AAAA;;;;iBK0Dd,iBAAA,CACpB,GAAA,EAAK,UAAA,EACL,OAAA,YAAmB,WAAA,KAClB,OAAA,CAAQ,UAAA;;;;;;;;iBC1EW,uBAAA,CACpB,GAAA,EAAK,UAAA,EACL,MAAA,WACC,OAAA,CAAQ,YAAA"}
1
+ {"version":3,"file":"index-C2G4IJck.d.mts","names":[],"sources":["../src/behaviors/with-overrides.ts","../src/behaviors/workflowable.ts","../src/capabilities.ts","../src/diff/types.ts","../src/errors.ts","../src/locale-registry.ts","../src/message-registry.ts"],"mappings":";;;;;;;;;;;KAqDK,iBAAA;EACC,IAAA;AAAA;EAEA,IAAA;EACA,KAAA,GAAQ,MAAA;EACR,MAAA,GAAS,MAAA,SAAe,MAAA;AAAA;EAExB,IAAA;EAAiB,KAAA,EAAO,MAAA;AAAA;AAAA,KAEzB,YAAA,GAAe,MAAM,SAAS,iBAAA;AAAA,KAEvB,mBAAA;EAcV,UAAA,EAAY,SAAS;AAAA;;;AC/B4B;AASnD;iBD4LgB,aAAA,CAAA,GAAiB,QAAQ,CAAC,mBAAA;;;;;;iBAmD1B,gBAAA,CACd,SAAA,EAAW,aAAa;EAAG,IAAA;AAAA,IAAiB,MAAA;;;cC1PjC,eAAA;AAAA,KACD,aAAA,WAAwB,eAAe;ADqMnD;;;;AAA6D;AAmD7D;;AAnDA,cC5La,oBAAA;EAAA;;;;;;ADgPqE;;;AAAA,iBCrOlE,mBAAA,CAAoB,IAAA,EAAM,aAAA,EAAe,EAAA,EAAI,aAAa;AArB1E;;;;AAAA,cA+Ba,uBAAA,SAAgC,KAAA;EAAA,SAClC,IAAA,EAAM,aAAA;EAAA,SACN,EAAA,EAAI,aAAA;cAED,IAAA,EAAM,aAAA,EAAe,EAAA,EAAI,aAAA;AAAA;AAAA,KAW3B,kBAAA;EACV,eAAA,EAAiB,WAAA;IAAgB,QAAA;EAAA;EACjC,iBAAA,EAAmB,SAAA;EACnB,aAAA,EAAe,SAAA;EACf,kBAAA,EAAoB,SAAA;AAAA;AAAA,iBAGN,YAAA,CAAA,GAAgB,QAAQ,CAAC,kBAAA;;;KC7E7B,gBAAA;;;;;;iBAOI,cAAA,CAAe,MAAA,EAAQ,MAAA,EAAQ,UAAA,EAAY,gBAAgB;;;;;;;UCxB1D,WAAA;EACf,IAAA;EACA,KAAK;AAAA;AHsCuD;AAAA,UGlC7C,SAAA;EACf,KAAA;EACA,IAAA;EAWA,IAAA;EACA,EAAA;EHmC4B;EGjC5B,QAAA,GAAW,WAAA;EHiCuB;EG/BlC,UAAA,GAAa,cAAc;AAAA;;KAIjB,eAAA;;UAGK,cAAA;EACf,OAAA;EACA,SAAA;EACA,MAAA,EAAQ,eAAA;EACR,SAAA;EACA,OAAA;EHmBkC;EGjBlC,SAAA,GAAY,QAAQ;AAAA;;UAIL,QAAA;EACf,KAAA;EACA,IAAA;EACA,EAAA;;EAEA,QAAA,GAAW,WAAW;AAAA;;;;;;;;;;;AHPsC;;;;cIhCjD,qBAAA,SAA8B,KAAK;EAAA,SACrC,UAAA;EAAA,SACA,UAAA;cAGP,UAAA,UACA,UAAA,EAAY,qBAAA,gBACZ,OAAA;AAAA;;;;;;;;;;cAsBS,kBAAA,SAA2B,KAAK;EAAA,SAClC,QAAA;EAAA,SACA,MAAA;cAEG,QAAA,UAAkB,MAAA;AAAA;;;AJgBoB;AAEpD;;;;AAcuB;AAsKvB;;;;cI5Ka,oBAAA,SAA6B,KAAK;EAAA,SACpC,QAAA;EAAA,SACA,OAAA;EAAA,SACA,MAAA;cAEG,QAAA,UAAkB,OAAA,UAAiB,MAAA;AAAA;;;;;AJ2NiC;;;;AC1PlF;cGiDa,kBAAA,SAA2B,KAAK;EAAA,SAClC,MAAA;EAAA,SACA,OAAA;cAEG,MAAA,UAAgB,OAAA;AAAA;;;;;;;;iBCZR,uBAAA,CAAwB,GAAA,EAAK,UAAA,GAAa,OAAA,CAAQ,WAAA;;iBAoBxD,kBAAA,CAAmB,OAA+B,WAAb,WAAW;;;AL/C5B;AAAA;;;;iBK0Dd,iBAAA,CACpB,GAAA,EAAK,UAAA,EACL,OAAA,YAAmB,WAAA,KAClB,OAAA,CAAQ,UAAA;;;;;;;;iBC1EW,uBAAA,CACpB,GAAA,EAAK,UAAA,EACL,MAAA,WACC,OAAA,CAAQ,YAAA"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{n as e,t}from"./versionable-Br_G5VGR.mjs";import{i as n,n as r,r as i,t as a}from"./errors-NRNILhhm.mjs";import{a as o,i as s,n as c,o as l,r as u,t as d}from"./routing-strategy-DQGPPBlW.mjs";import{t as f}from"./content-config-store-8omQF5p5.mjs";import{USER_ID_MAX_LENGTH as p,field as m}from"@murumets-ee/entity";import{createSystemReachResolver as h,mergeConfigOverrides as g}from"@murumets-ee/core";import{QueryClient as _}from"@murumets-ee/entity/query";import{schemaRegistry as v}from"@murumets-ee/db";import{eq as y}from"drizzle-orm";const b=new Set([`hide`,`patch`,`replace`]),x=Symbol.for(`@murumets-ee/content:withOverrides`),S=new Set([`__proto__`,`constructor`,`prototype`]);function C(e,t,n=0){if(e!=null&&!(typeof e!=`object`||Array.isArray(e)))for(let r of Object.keys(e)){if(S.has(r))throw Error(`withOverrides: reserved key '${r}' at ${t} is not allowed.`);n<3&&C(e[r],`${t}.${r}`,n+1)}}function w(e){if(e==null)return{};if(typeof e!=`object`||Array.isArray(e))throw Error(`withOverrides: _overrides must be a plain object map.`);let t=e,n=Object.keys(t);if(n.length>100)throw Error(`withOverrides: _overrides has ${n.length} entries; cap is 100.`);let r={};for(let e of n){if(S.has(e))throw Error(`withOverrides: reserved key '${e}' is not allowed.`);let n=t[e];if(typeof n!=`object`||!n||Array.isArray(n))throw Error(`withOverrides: _overrides['${e}'] must be a directive object.`);let i=n.kind;if(typeof i!=`string`||!b.has(i))throw Error(`withOverrides: _overrides['${e}'].kind must be one of [hide, patch, replace]; got ${String(i)}.`);if(i===`patch`){let t=n;T(t.props,`_overrides['${e}'].props`),T(t.traits,`_overrides['${e}'].traits`)}if(i===`replace`){let t=n.block;if(typeof t!=`object`||!t||Array.isArray(t)||typeof t.id!=`string`)throw Error(`withOverrides: _overrides['${e}'] replace payload must include an object 'block' with a string 'id'.`)}C(n,`_overrides['${e}']`),r[e]=n}return r}function T(e,t){if(e!==void 0&&(typeof e!=`object`||!e||Array.isArray(e)))throw Error(`withOverrides: ${t} must be an object.`)}function E(){return{name:`withOverrides`,fields:{_overrides:m.json({default:()=>({}),internal:!0})},hooks:{beforeCreate:async e=>(e._overrides===void 0?e._overrides={}:e._overrides=w(e._overrides),e),beforeUpdate:async(e,t)=>(t._overrides===void 0||(t._overrides=w(t._overrides)),t)},[x]:!0}}function D(e){return e?e.some(e=>!!e?.[x]):!1}const O=[`draft`,`in_review`,`approved`,`rejected`,`published`],k={draft:[`in_review`],in_review:[`approved`,`rejected`],approved:[`published`,`in_review`],rejected:[`in_review`,`draft`],published:[`draft`]};function A(e,t){return e===t?!0:k[e]?.includes(t)??!1}var j=class extends Error{from;to;constructor(e,t){super(`Invalid workflow transition: ${e} → ${t}. Allowed transitions from "${e}": [${(k[e]??[]).join(`, `)}].`),this.name=`WorkflowTransitionError`,this.from=e,this.to=t}};function M(){return{name:`workflowable`,fields:{_workflowStatus:m.select({options:O,default:`draft`,indexed:!0,required:!0,internal:!0}),_workflowAssignee:m.text({required:!1,internal:!0,maxLength:p}),_workflowNote:m.text({required:!1,internal:!0}),_workflowUpdatedAt:m.date({internal:!0})},hooks:{beforeCreate:async e=>(e._workflowStatus=`draft`,e._workflowUpdatedAt=new Date,e),beforeUpdate:async(e,t,n)=>{let r=t._workflowStatus;if(r===void 0)return t;if(!O.includes(r))throw new j(`draft`,r);let i=(await n?.loadCurrent?.())?._workflowStatus??`draft`,a=r;if(!A(i,a))throw new j(i,a);return t._workflowUpdatedAt=new Date,t}}}}function N(e,t){switch(t){case`drafts`:return P(e,`publishable`);case`versions`:return P(e,`versionable`);case`localeStatus`:return P(e,`publishable`)&&F(e);case`translations`:return F(e)}}function P(e,t){return e.behaviors?.some(e=>e.name===t)??!1}function F(e){let t=e.allFields;for(let e of Object.keys(t)){let n=t[e];if(n&&n.translatable===!0)return!0}return!1}const I=h(`locale-registry`);function L(){return f().locales.map((e,t)=>({code:e.code,label:e.label,enabled:!0,direction:`ltr`,sortOrder:t}))}function R(e,t){return e.sortOrder-t.sortOrder||e.code.localeCompare(t.code)}async function z(e){return e.entities?.has(`locale`)?(await new _({entity:s,db:e.db.readOnly,logger:e.logger.child({registry:`locale`}),contextResolver:I}).findMany({limit:1e3})).map(e=>{let t=e;return{code:String(t.code),label:typeof t.label==`string`&&t.label.length>0?t.label:String(t.code),enabled:t.enabled!==!1,direction:t.direction===`rtl`?`rtl`:`ltr`,sortOrder:typeof t.sortOrder==`number`?t.sortOrder:0}}):[]}async function B(e){let t;try{t=await z(e)}catch(t){return e.logger.warn({err:t},`locale registry read failed — falling back to content() defaults`),[...L()].sort(R)}return g({defaults:L(),overrides:t,idOfDefault:e=>e.code,idOfOverride:e=>e.code}).sort(R)}function V(e){return e.filter(e=>e.enabled).map(e=>e.code)}async function H(e,t){let n=V(t??await B(e)),r=f().defaultLocale,i={};try{let{createSettingsClient:t}=await import(`@murumets-ee/settings/runtime`);i=await t(l,{app:e}).getAll()??{}}catch(t){e.logger.warn({err:t},`i18n settings read failed — falling back to content() defaults`)}let a=typeof i.defaultLocale==`string`&&i.defaultLocale.length>0?i.defaultLocale:r,s=n.includes(a)?a:n.includes(r)?r:n[0]??r,c=i.routingStrategy;return{defaultLocale:s,routingStrategy:o.includes(typeof c==`string`?c:``)?c:`no-prefix-default`}}const U=h(`message-registry`),W=2e4;async function G(e,t){if(!e.entities?.has(`message`))return[];let n=v.get(`message`);if(!n)return[];let r=new _({entity:u,db:e.db.readOnly,logger:e.logger.child({registry:`message`}),contextResolver:U});try{let i=await r.findMany({where:y(n.locale,t),limit:W});return i.length>=W&&e.logger.warn({locale:t,cap:W},`message override read hit the row cap — some overrides were not applied`),i.map(e=>{let t=e;return{namespace:String(t.namespace),key:String(t.key),value:String(t.value)}})}catch(n){return e.logger.warn({err:n,locale:t},`message registry read failed — falling back to shipped defaults only`),[]}}export{a as DraftNotFoundError,r as EntityCapabilityError,i as InvalidLocaleError,s as Locale,u as Message,o as ROUTING_STRATEGIES,n as VersionNotFoundError,O as WORKFLOW_STATES,k as WORKFLOW_TRANSITIONS,j as WorkflowTransitionError,V as enabledLocaleCodes,N as entitySupports,t as getVersionableOptions,D as hasWithOverrides,l as i18nSettings,A as isAllowedTransition,d as localePrefixForStrategy,c as prefixDefaultLocaleForStrategy,B as resolveEffectiveLocales,H as resolveI18nConfig,G as resolveMessageOverrides,e as versionable,E as withOverrides,M as workflowable};
1
+ import{n as e,t}from"./versionable-Br_G5VGR.mjs";import{i as n,n as r,r as i,t as a}from"./errors-NRNILhhm.mjs";import{a as o,i as s,n as c,o as l,r as u,t as d}from"./routing-strategy-DQGPPBlW.mjs";import{t as f}from"./content-config-store-8omQF5p5.mjs";import{USER_ID_MAX_LENGTH as p,field as m}from"@murumets-ee/entity";import{createSystemReachResolver as h,mergeConfigOverrides as g}from"@murumets-ee/core";import{QueryClient as _}from"@murumets-ee/entity/query";import{schemaRegistry as v}from"@murumets-ee/db";import{eq as y}from"drizzle-orm";const b=new Set([`hide`,`patch`,`replace`]),x=Symbol.for(`@murumets-ee/content:withOverrides`),S=new Set([`__proto__`,`constructor`,`prototype`]);function C(e,t,n=0){if(e!=null&&!(typeof e!=`object`||Array.isArray(e)))for(let r of Object.keys(e)){if(S.has(r))throw Error(`withOverrides: reserved key '${r}' at ${t} is not allowed.`);n<3&&C(e[r],`${t}.${r}`,n+1)}}function w(e){if(e==null)return{};if(typeof e!=`object`||Array.isArray(e))throw Error(`withOverrides: _overrides must be a plain object map.`);let t=e,n=Object.keys(t);if(n.length>100)throw Error(`withOverrides: _overrides has ${n.length} entries; cap is 100.`);let r={};for(let e of n){if(S.has(e))throw Error(`withOverrides: reserved key '${e}' is not allowed.`);let n=t[e];if(typeof n!=`object`||!n||Array.isArray(n))throw Error(`withOverrides: _overrides['${e}'] must be a directive object.`);let i=n.kind;if(typeof i!=`string`||!b.has(i))throw Error(`withOverrides: _overrides['${e}'].kind must be one of [hide, patch, replace]; got ${String(i)}.`);if(i===`patch`){let t=n;T(t.props,`_overrides['${e}'].props`),T(t.traits,`_overrides['${e}'].traits`)}if(i===`replace`){let t=n.block;if(typeof t!=`object`||!t||Array.isArray(t)||typeof t.id!=`string`)throw Error(`withOverrides: _overrides['${e}'] replace payload must include an object 'block' with a string 'id'.`)}C(n,`_overrides['${e}']`),r[e]=n}return r}function T(e,t){if(e!==void 0&&(typeof e!=`object`||!e||Array.isArray(e)))throw Error(`withOverrides: ${t} must be an object.`)}function E(){return{name:`withOverrides`,fields:{_overrides:m.json({default:()=>({}),internal:!0,refScan:!0})},hooks:{beforeCreate:async e=>(e._overrides===void 0?e._overrides={}:e._overrides=w(e._overrides),e),beforeUpdate:async(e,t)=>(t._overrides===void 0||(t._overrides=w(t._overrides)),t)},[x]:!0}}function D(e){return e?e.some(e=>!!e?.[x]):!1}const O=[`draft`,`in_review`,`approved`,`rejected`,`published`],k={draft:[`in_review`],in_review:[`approved`,`rejected`],approved:[`published`,`in_review`],rejected:[`in_review`,`draft`],published:[`draft`]};function A(e,t){return e===t?!0:k[e]?.includes(t)??!1}var j=class extends Error{from;to;constructor(e,t){super(`Invalid workflow transition: ${e} → ${t}. Allowed transitions from "${e}": [${(k[e]??[]).join(`, `)}].`),this.name=`WorkflowTransitionError`,this.from=e,this.to=t}};function M(){return{name:`workflowable`,fields:{_workflowStatus:m.select({options:O,default:`draft`,indexed:!0,required:!0,internal:!0}),_workflowAssignee:m.text({required:!1,internal:!0,maxLength:p}),_workflowNote:m.text({required:!1,internal:!0}),_workflowUpdatedAt:m.date({internal:!0})},hooks:{beforeCreate:async e=>(e._workflowStatus=`draft`,e._workflowUpdatedAt=new Date,e),beforeUpdate:async(e,t,n)=>{let r=t._workflowStatus;if(r===void 0)return t;if(!O.includes(r))throw new j(`draft`,r);let i=(await n?.loadCurrent?.())?._workflowStatus??`draft`,a=r;if(!A(i,a))throw new j(i,a);return t._workflowUpdatedAt=new Date,t}}}}function N(e,t){switch(t){case`drafts`:return P(e,`publishable`);case`versions`:return P(e,`versionable`);case`localeStatus`:return P(e,`publishable`)&&F(e);case`translations`:return F(e)}}function P(e,t){return e.behaviors?.some(e=>e.name===t)??!1}function F(e){let t=e.allFields;for(let e of Object.keys(t)){let n=t[e];if(n&&n.translatable===!0)return!0}return!1}const I=h(`locale-registry`);function L(){return f().locales.map((e,t)=>({code:e.code,label:e.label,enabled:!0,direction:`ltr`,sortOrder:t}))}function R(e,t){return e.sortOrder-t.sortOrder||e.code.localeCompare(t.code)}async function z(e){return e.entities?.has(`locale`)?(await new _({entity:s,db:e.db.readOnly,logger:e.logger.child({registry:`locale`}),contextResolver:I}).findMany({limit:1e3})).map(e=>{let t=e;return{code:String(t.code),label:typeof t.label==`string`&&t.label.length>0?t.label:String(t.code),enabled:t.enabled!==!1,direction:t.direction===`rtl`?`rtl`:`ltr`,sortOrder:typeof t.sortOrder==`number`?t.sortOrder:0}}):[]}async function B(e){let t;try{t=await z(e)}catch(t){return e.logger.warn({err:t},`locale registry read failed — falling back to content() defaults`),[...L()].sort(R)}return g({defaults:L(),overrides:t,idOfDefault:e=>e.code,idOfOverride:e=>e.code}).sort(R)}function V(e){return e.filter(e=>e.enabled).map(e=>e.code)}async function H(e,t){let n=V(t??await B(e)),r=f().defaultLocale,i={};try{let{createSettingsClient:t}=await import(`@murumets-ee/settings/runtime`);i=await t(l,{app:e}).getAll()??{}}catch(t){e.logger.warn({err:t},`i18n settings read failed — falling back to content() defaults`)}let a=typeof i.defaultLocale==`string`&&i.defaultLocale.length>0?i.defaultLocale:r,s=n.includes(a)?a:n.includes(r)?r:n[0]??r,c=i.routingStrategy;return{defaultLocale:s,routingStrategy:o.includes(typeof c==`string`?c:``)?c:`no-prefix-default`}}const U=h(`message-registry`),W=2e4;async function G(e,t){if(!e.entities?.has(`message`))return[];let n=v.get(`message`);if(!n)return[];let r=new _({entity:u,db:e.db.readOnly,logger:e.logger.child({registry:`message`}),contextResolver:U});try{let i=await r.findMany({where:y(n.locale,t),limit:W});return i.length>=W&&e.logger.warn({locale:t,cap:W},`message override read hit the row cap — some overrides were not applied`),i.map(e=>{let t=e;return{namespace:String(t.namespace),key:String(t.key),value:String(t.value)}})}catch(n){return e.logger.warn({err:n,locale:t},`message registry read failed — falling back to shipped defaults only`),[]}}export{a as DraftNotFoundError,r as EntityCapabilityError,i as InvalidLocaleError,s as Locale,u as Message,o as ROUTING_STRATEGIES,n as VersionNotFoundError,O as WORKFLOW_STATES,k as WORKFLOW_TRANSITIONS,j as WorkflowTransitionError,V as enabledLocaleCodes,N as entitySupports,t as getVersionableOptions,D as hasWithOverrides,l as i18nSettings,A as isAllowedTransition,d as localePrefixForStrategy,c as prefixDefaultLocaleForStrategy,B as resolveEffectiveLocales,H as resolveI18nConfig,G as resolveMessageOverrides,e as versionable,E as withOverrides,M as workflowable};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["SYSTEM_CONTEXT"],"sources":["../src/behaviors/with-overrides.ts","../src/behaviors/workflowable.ts","../src/capabilities.ts","../src/locale-registry.ts","../src/message-registry.ts"],"sourcesContent":["/**\n * `withOverrides()` — opt-in behavior wiring a document entity into the\n * page-scoped override resolver shipped by phase 05.\n *\n * Adds a single internal field `_overrides: Record<string, OverrideDirective>`\n * to the host entity, defaulted to an empty object. The composition step\n * (`composeChromeRegion` in `@murumets-ee/blocks`) reads this field and calls\n * `applyOverrides()` on each chrome region AFTER its blocks have been\n * scope-tagged.\n *\n * Apps opt-in:\n *\n * @example\n * ```ts\n * defineEntity({\n * name: 'page',\n * fields: { ... },\n * behaviors: [\n * publishable(),\n * versionable({ maxPerDoc: 30 }),\n * workflowable(),\n * withOpLog({ retentionDays: 7 }),\n * withOverrides(), // ← enables page-level overrides on scoped blocks\n * ],\n * })\n * ```\n *\n * Why a behavior (not a plain field):\n * - Discoverability — `entitySupports(entity, 'overrides')` lets the boot\n * orphan-sweep find every overrides-eligible entity without hard-coding\n * a list.\n * - Validation — the behavior's `beforeUpdate` hook validates the shape\n * of the `_overrides` map (Zod-equivalent runtime check) so a malformed\n * PATCH body can't poison the field.\n * - Defensive defaults — `_overrides: {}` is auto-seeded on `beforeCreate`\n * so consumers always see a value, never `undefined`.\n *\n * `internal: true` on the field keeps it out of HTTP PATCH input — the\n * `doc:scope-override:set` op is the only path that can mutate it (writes\n * are routed through `AdminClient.updateInternal()` after permission\n * gating).\n *\n * See `plan/blocks/05-scoping-overrides.md` §5 + `progress/findings/F009`.\n */\n\nimport type { Behavior, JsonField } from '@murumets-ee/entity'\nimport { field } from '@murumets-ee/entity'\n\n/**\n * Override-directive shape. Mirrored from `@murumets-ee/blocks` to avoid\n * a circular dep (content → blocks → content); content stores the\n * persisted JSON, blocks owns the resolver. Keep the two shapes in sync.\n */\ntype OverrideDirective =\n | { kind: 'hide' }\n | {\n kind: 'patch'\n props?: Record<string, unknown>\n traits?: Record<string, Record<string, unknown>>\n }\n | { kind: 'replace'; block: Record<string, unknown> }\n\ntype OverridesMap = Record<string, OverrideDirective>\n\nexport type WithOverridesFields = {\n // `required: false` at the type level — the `beforeCreate` hook\n // seeds the field with `{}` when callers omit it, so making the\n // INPUT optional is more ergonomic than threading `_overrides: {}`\n // through every consumer's create-input.\n //\n // NOTE: the DB column is NULLABLE in the generated migration\n // (`_overrides jsonb`) — the JsonField `default` is a runtime\n // function the entity layer calls on create, NOT a SQL DEFAULT\n // clause the migration generator sees. Readers MUST treat NULL\n // as equivalent to `{}` (see `compose-chrome.ts`'s `asOverridesMap`\n // for the canonical pattern). A row inserted\n // via raw SQL or migration that bypasses the entity layer can\n // leave NULL; that's safe.\n _overrides: JsonField\n}\n\n/**\n * Allowed directive kinds — closed set. Unknown discriminators are rejected\n * at the behavior boundary so a malformed PATCH (or future plugin)\n * can't write opaque shapes that the resolver silently ignores.\n */\nconst ALLOWED_KINDS: ReadonlySet<string> = new Set(['hide', 'patch', 'replace'])\n\n/**\n * Maximum number of override entries per document. Bounded to keep the\n * page row's JSON column compact + the rendering hot path predictable.\n * Per phase 05 §9: a page overriding more than ~50 scoped blocks is a\n * smell — at that point the shared chrome is the wrong chrome for that\n * page, and the answer is a different `site_chrome` row or page-local blocks,\n * not more patching.\n */\nconst MAX_OVERRIDES_PER_DOC = 100\n\n/** Marker symbol so introspection can pick out the behavior reliably. */\nconst WITH_OVERRIDES_MARKER = Symbol.for('@murumets-ee/content:withOverrides')\n\ninterface WithOverridesMarker {\n [WITH_OVERRIDES_MARKER]: true\n}\n\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * Walk a plain-object value's OWN keys and reject any that match a\n * prototype-pollution vector. JSON.parse can create own properties\n * with these names; the dispatcher already filters at the route +\n * op-handler boundary, but the behavior is the third layer of\n * defense (any persistence path that bypasses the dispatcher — DB\n * seed, raw SQL migration, future internal API — lands here on the\n * next update).\n *\n * The depth cap of 3 covers every documented over-the-wire shape:\n *\n * - patch.props.__proto__ → depth 1\n * - patch.traits.spacing.__proto__ → depth 2\n * - replace.block.props.__proto__ → depth 2\n * - replace.block.traits.spacing.__proto__ → depth 3\n *\n * (Counting from the directive root at depth 0. The recursion CHECKS\n * keys at the entered depth, then recurses while `depth < 3`, so the\n * deepest check happens at depth=3.)\n *\n * Beyond that depth the structure is field VALUES which are caller-\n * payload (block field type / trait field type) — those are the\n * field-type validator's job, not the override-map gate.\n *\n * `path` is appended to the thrown error for triage.\n */\nfunction assertNoForbiddenKeys(value: unknown, path: string, depth = 0): void {\n if (value === null || value === undefined) return\n if (typeof value !== 'object' || Array.isArray(value)) return\n for (const key of Object.keys(value as Record<string, unknown>)) {\n if (FORBIDDEN_KEYS.has(key)) {\n throw new Error(`withOverrides: reserved key '${key}' at ${path} is not allowed.`)\n }\n if (depth < 3) {\n assertNoForbiddenKeys((value as Record<string, unknown>)[key], `${path}.${key}`, depth + 1)\n }\n }\n}\n\n/**\n * Validate an override-map payload. Throws when malformed; returns the\n * (defensively-cloned, sanitised) value when ok. Called from the\n * `beforeUpdate` hook — `internal: true` blocks PATCH-side input but\n * the `doc:scope-override:set` op writes via `updateInternal` and so\n * still flows through here.\n *\n * Defense layered: outer scope-block-id keys + each directive's\n * recursive nested keys (patch.props / patch.traits / replace.block.props\n * / replace.block.traits) all walk through `assertNoForbiddenKeys`. The\n * dispatcher's own handler validates the same set; this is the\n * defense-in-depth replay at the persistence boundary.\n */\nfunction validateOverridesMap(value: unknown): OverridesMap {\n if (value === null || value === undefined) return {}\n if (typeof value !== 'object' || Array.isArray(value)) {\n throw new Error('withOverrides: _overrides must be a plain object map.')\n }\n const map = value as Record<string, unknown>\n const keys = Object.keys(map)\n if (keys.length > MAX_OVERRIDES_PER_DOC) {\n throw new Error(\n `withOverrides: _overrides has ${keys.length} entries; cap is ${MAX_OVERRIDES_PER_DOC}.`,\n )\n }\n const sanitised: OverridesMap = {}\n for (const key of keys) {\n // Skip prototype-pollution vectors at the OUTER (scopeBlockId)\n // key boundary first.\n if (FORBIDDEN_KEYS.has(key)) {\n throw new Error(`withOverrides: reserved key '${key}' is not allowed.`)\n }\n const directive = map[key]\n if (\n directive === null ||\n directive === undefined ||\n typeof directive !== 'object' ||\n Array.isArray(directive)\n ) {\n throw new Error(`withOverrides: _overrides['${key}'] must be a directive object.`)\n }\n const kind = (directive as { kind?: unknown }).kind\n if (typeof kind !== 'string' || !ALLOWED_KINDS.has(kind)) {\n throw new Error(\n `withOverrides: _overrides['${key}'].kind must be one of [hide, patch, replace]; got ${String(\n kind,\n )}.`,\n )\n }\n // Kind-specific shape enforcement (CR bot major) — `kind` alone\n // isn't enough; a malformed `{ kind: 'replace' }` (no block) or\n // `{ kind: 'patch', props: 123 }` would otherwise pass the gate\n // and land invalid data in persistence. Mirror the dispatcher\n // handler's shape checks at this third defense layer.\n if (kind === 'patch') {\n const d = directive as { props?: unknown; traits?: unknown }\n assertPlainObjectOrUndefined(d.props, `_overrides['${key}'].props`)\n assertPlainObjectOrUndefined(d.traits, `_overrides['${key}'].traits`)\n }\n if (kind === 'replace') {\n const block = (directive as { block?: unknown }).block\n if (\n block === null ||\n block === undefined ||\n typeof block !== 'object' ||\n Array.isArray(block) ||\n typeof (block as { id?: unknown }).id !== 'string'\n ) {\n throw new Error(\n `withOverrides: _overrides['${key}'] replace payload must include an object 'block' with a string 'id'.`,\n )\n }\n }\n // Recurse into the directive payload — patch.props / patch.traits\n // / replace.block.props / replace.block.traits — and reject any\n // forbidden own-property keys at every level.\n assertNoForbiddenKeys(directive, `_overrides['${key}']`)\n sanitised[key] = directive as OverrideDirective\n }\n return sanitised\n}\n\n/**\n * Reject a directive sub-field that's present but not a plain object\n * (array / primitive / null). `undefined` is allowed — the field is\n * optional. Mirrors the dispatcher handler's `assertPlainObjectOrUndefined`.\n */\nfunction assertPlainObjectOrUndefined(value: unknown, label: string): void {\n if (value === undefined) return\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new Error(`withOverrides: ${label} must be an object.`)\n }\n}\n\n/**\n * Mark an entity as participating in the page-scoped override resolver.\n * See module-level JSDoc for the full contract.\n */\nexport function withOverrides(): Behavior<WithOverridesFields, 'withOverrides'> {\n const behavior: Behavior<WithOverridesFields, 'withOverrides'> & WithOverridesMarker = {\n name: 'withOverrides',\n fields: {\n // `internal: true` blocks PATCH-input poisoning (the\n // `doc:scope-override:set` op writes via updateInternal which\n // bypasses the input filter). `default: () => ({})` seeds the\n // value on every create call, so callers can omit `_overrides`\n // entirely (no `required: true` needed at the input level).\n _overrides: field.json({\n default: () => ({}),\n internal: true,\n }),\n },\n hooks: {\n beforeCreate: async (data) => {\n // Seed the empty map on create so the column is never null in DB.\n // Caller-supplied `_overrides` on create is unusual (overrides\n // are typically dispatched op-by-op) but if they DO pass one,\n // validate it.\n if (data._overrides === undefined) {\n data._overrides = {}\n } else {\n data._overrides = validateOverridesMap(data._overrides)\n }\n return data\n },\n beforeUpdate: async (_id, data) => {\n // Only validate when the patch tries to write `_overrides`.\n if (data._overrides === undefined) return data\n data._overrides = validateOverridesMap(data._overrides)\n return data\n },\n },\n [WITH_OVERRIDES_MARKER]: true,\n }\n return behavior\n}\n\n/**\n * Check whether an entity declares `withOverrides()`. Returns `true`\n * when the entity's behaviors list contains a behavior tagged with\n * the marker symbol. Used by the boot orphan-sweep + admin UI gating.\n */\nexport function hasWithOverrides(\n behaviors: ReadonlyArray<{ name: string } & Record<symbol, unknown>> | undefined,\n): boolean {\n if (!behaviors) return false\n return behaviors.some((b) => Boolean(b?.[WITH_OVERRIDES_MARKER]))\n}\n\n/**\n * Export the directive type alias for downstream consumers that want\n * to type-narrow without depending on `@murumets-ee/blocks`. The\n * resolver lives in blocks; the persisted shape lives here.\n */\nexport type { OverrideDirective, OverridesMap }\n","/**\n * Workflowable behavior — editorial workflow with approval gates.\n *\n * Adds workflow status tracking fields to an entity and enforces the\n * documented transition state machine in `beforeUpdate`. Works alongside\n * `publishable()` — the workflow status tracks the editorial pipeline,\n * while publishable's `status` remains the public-facing state.\n *\n * Workflow state machine:\n *\n * draft ─submit─► in_review ─approve─► approved ─publish─► published\n * │ ▲ │\n * │ └────resubmit─────┐ │\n * └─reject─► rejected ─┘ │\n * │\n * unpublish ◄──────────────┘\n * (back to draft)\n *\n * Internal-write only: every workflow field is declared `internal: true`,\n * so the HTTP PATCH surface strips them from caller input. Workflow\n * transitions are performed by trusted server-side routes via\n * `AdminClient.updateInternal()` after authorizing the action with the\n * permission checker. The `beforeUpdate` hook validates that the\n * proposed transition is in the allowed table.\n *\n * Permission gating lives in the route handler (admin-ui workflow\n * routes), not in this behavior — by the time the hook fires, the\n * route has already authorized via `checkPermission`. The hook is\n * the structural defense: even a bug in the route layer cannot push\n * a workflow row into an illegal state.\n *\n * UI integration is tracked in GitHub issue #124. Until the workflow\n * UX ships, the behavior can still be added to entities — it'll simply\n * seed `_workflowStatus = 'draft'` on create and reject anyone trying\n * to write the field via PATCH.\n */\n\nimport type {\n Behavior,\n BehaviorContext,\n DateField,\n SelectField,\n TextField,\n} from '@murumets-ee/entity'\nimport { field, USER_ID_MAX_LENGTH } from '@murumets-ee/entity'\n\nexport const WORKFLOW_STATES = ['draft', 'in_review', 'approved', 'rejected', 'published'] as const\nexport type WorkflowState = (typeof WORKFLOW_STATES)[number]\n\n/**\n * Allowed transitions table. Each key is the *current* workflow status;\n * the value is the set of statuses it can move to.\n *\n * Encoded as a const-asserted record so a typo in the source produces\n * a compile error rather than a silent no-match at runtime.\n */\nexport const WORKFLOW_TRANSITIONS = {\n draft: ['in_review'],\n in_review: ['approved', 'rejected'],\n approved: ['published', 'in_review'],\n rejected: ['in_review', 'draft'],\n published: ['draft'],\n} as const satisfies Record<WorkflowState, readonly WorkflowState[]>\n\n/**\n * Is `to` an allowed transition from `from`? Pure, no I/O.\n */\nexport function isAllowedTransition(from: WorkflowState, to: WorkflowState): boolean {\n if (from === to) return true // no-op self-transition is fine\n const allowed = WORKFLOW_TRANSITIONS[from] as readonly WorkflowState[] | undefined\n return allowed?.includes(to) ?? false\n}\n\n/**\n * Thrown when `beforeUpdate` rejects a workflow transition. Distinct\n * subclass so admin-ui's API handler can map it to HTTP 400.\n */\nexport class WorkflowTransitionError extends Error {\n readonly from: WorkflowState\n readonly to: WorkflowState\n\n constructor(from: WorkflowState, to: WorkflowState) {\n super(\n `Invalid workflow transition: ${from} → ${to}. ` +\n `Allowed transitions from \"${from}\": [${(WORKFLOW_TRANSITIONS[from] ?? []).join(', ')}].`,\n )\n this.name = 'WorkflowTransitionError'\n this.from = from\n this.to = to\n }\n}\n\nexport type WorkflowableFields = {\n _workflowStatus: SelectField & { required: true }\n _workflowAssignee: TextField\n _workflowNote: TextField\n _workflowUpdatedAt: DateField\n}\n\nexport function workflowable(): Behavior<WorkflowableFields> {\n return {\n name: 'workflowable',\n fields: {\n _workflowStatus: field.select({\n options: WORKFLOW_STATES as unknown as string[],\n default: 'draft',\n indexed: true,\n required: true,\n internal: true,\n }) as SelectField & { required: true },\n // `_workflowAssignee` stores a better-auth user ID — varchar(255)\n // per CLAUDE.md §\"User IDs\", emitted via `maxLength: 255` (#430).\n // `_workflowNote` is free-text, so it stays unbounded `text`.\n _workflowAssignee: field.text({\n required: false,\n internal: true,\n maxLength: USER_ID_MAX_LENGTH,\n }),\n _workflowNote: field.text({ required: false, internal: true }),\n _workflowUpdatedAt: field.date({ internal: true }),\n },\n hooks: {\n beforeCreate: async (data) => {\n data._workflowStatus = 'draft'\n data._workflowUpdatedAt = new Date()\n return data\n },\n beforeUpdate: async (\n _id: string,\n data: Record<string, unknown>,\n ctx?: BehaviorContext,\n ): Promise<Record<string, unknown>> => {\n // Only validate when the patch tries to change the workflow status.\n // Updates that don't touch _workflowStatus pass through (the field's\n // `internal: true` already prevents it from sneaking in via PATCH;\n // only updateInternal callers reach this branch with a value).\n const proposed = data._workflowStatus\n if (proposed === undefined) return data\n\n if (!WORKFLOW_STATES.includes(proposed as WorkflowState)) {\n throw new WorkflowTransitionError('draft', proposed as WorkflowState)\n }\n\n // Need the current row to validate the from→to transition. The\n // lazy loader is provided by AdminClient — present on update,\n // absent on create.\n const current = await ctx?.loadCurrent?.()\n const fromState = (current?._workflowStatus as WorkflowState | undefined) ?? 'draft'\n const toState = proposed as WorkflowState\n\n if (!isAllowedTransition(fromState, toState)) {\n throw new WorkflowTransitionError(fromState, toState)\n }\n\n data._workflowUpdatedAt = new Date()\n return data\n },\n },\n }\n}\n","/**\n * Per-entity content capability gates.\n *\n * The content plugin attaches different infrastructure tables to an\n * entity based on which behaviors it has:\n * - `publishable()` → drafts table\n * - `versionable()` → versions table\n * - `publishable() + translatable fields` → locale_status table\n * - any translatable field → translations table (also written by the\n * entity package; included for symmetric capability checks)\n *\n * Calling a capability-specific ContentClient method (`saveDraft`,\n * `publishDraft`, `setLocaleStatus`, `restoreVersion`, ...) on an\n * entity that doesn't have the required behavior is a programming\n * error. ContentClient throws `EntityCapabilityError` on writes and\n * silently returns falsy on reads. Callers that want to gate ahead —\n * \"should I show the draft button on this entity row?\" — use\n * `entitySupports()` instead of catching the error.\n */\n\nimport type { Entity, FieldConfig } from '@murumets-ee/entity'\n\nexport type EntityCapability = 'drafts' | 'versions' | 'localeStatus' | 'translations'\n\n/**\n * Does this entity support the given content capability? Pure check\n * against the entity's declared behaviors and field translatability —\n * no plugin lookup, no DB call.\n */\nexport function entitySupports(entity: Entity, capability: EntityCapability): boolean {\n switch (capability) {\n case 'drafts':\n return hasBehavior(entity, 'publishable')\n case 'versions':\n return hasBehavior(entity, 'versionable')\n case 'localeStatus':\n return hasBehavior(entity, 'publishable') && hasTranslatableField(entity)\n case 'translations':\n return hasTranslatableField(entity)\n }\n}\n\nfunction hasBehavior(entity: Entity, name: string): boolean {\n return entity.behaviors?.some((b) => b.name === name) ?? false\n}\n\nfunction hasTranslatableField(entity: Entity): boolean {\n const fields = entity.allFields as Record<string, FieldConfig>\n for (const key of Object.keys(fields)) {\n const f = fields[key]\n if (f && (f as { translatable?: boolean }).translatable === true) return true\n }\n return false\n}\n","/**\n * The content-locale **registry resolvers** (plan/config phase 05 §7, D004/D009/SD006).\n *\n * The effective content-locale set = **read-time-merge** of the `content()` shipped\n * defaults ⊕ the DB `locale` config-entity override rows. Resolved **once per request**\n * at context establishment (the admin API handler + `withAdminContext`) and threaded\n * onto {@link RequestContext.availableLocales}, so `validateLocale`'s synchronous\n * whitelist reflects runtime-added / disabled locales with **no rebuild and no\n * globalThis cache** — multi-process / multi-tenant correct.\n *\n * The registry read is a **system-context** query: managing the language set is\n * admin-only (`locale:*`), but every role must resolve valid locales to edit content,\n * exactly like the app-wide roles read. Reads degrade to the static `content()`\n * defaults on any error (fresh app pre-migration, transient DB failure) — deny-by-\n * default is preserved (a not-yet-known locale simply isn't whitelisted).\n */\n\nimport { createSystemReachResolver, mergeConfigOverrides, type ToolkitApp } from '@murumets-ee/core'\nimport { QueryClient } from '@murumets-ee/entity/query'\nimport { getContentConfig } from './content-config-store.js'\nimport { i18nSettings, ROUTING_STRATEGIES } from './i18n-settings.js'\nimport { Locale } from './locale-entity.js'\nimport type { I18nConfig, LocaleDirection, LocaleEntry, RoutingStrategy } from './types.js'\n\n/**\n * Elevated **reach** resolver for the app-wide registry read (the roles-read\n * analog). It grants `view` and nothing else — this module only ever READS the\n * `locale` rows, so an all-allow checker would have handed every caller of this\n * resolver a write grant it has no use for. The named constructor owns that\n * exception centrally rather than leaving it to be re-derived per call site;\n * a hand-written `checker: () => true` here is refused by `verify:boundaries`.\n *\n * The explicit id is the audit identity — two elevated readers sharing the\n * default `'system'` are indistinguishable in the log.\n */\nconst SYSTEM_CONTEXT = createSystemReachResolver('locale-registry')\n\n/** The `content()`-shipped locales projected to full {@link LocaleEntry}s (all enabled). */\nfunction shippedDefaults(): LocaleEntry[] {\n return getContentConfig().locales.map((l, i) => ({\n code: l.code,\n label: l.label,\n enabled: true,\n direction: 'ltr' as LocaleDirection,\n sortOrder: i,\n }))\n}\n\n/** Ascending by `sortOrder`, then `code` for a stable tie-break. */\nfunction bySortOrder(a: LocaleEntry, b: LocaleEntry): number {\n return a.sortOrder - b.sortOrder || a.code.localeCompare(b.code)\n}\n\n/**\n * Defensive upper bound on `locale` rows to read (a tenant realistically has <20). The\n * Languages-editor route MUST reconcile against the SAME bound so it can't miss/strand\n * rows — see admin-ui `locale-routes` `MAX_EXISTING_LOCALE_ROWS` (kept in sync).\n */\nconst MAX_LOCALE_ROWS = 1000\n\n/** Read + normalize the DB `locale` override rows (system context). */\nasync function readLocaleRows(app: ToolkitApp): Promise<LocaleEntry[]> {\n if (!app.entities?.has('locale')) return []\n const client = new QueryClient({\n entity: Locale,\n db: app.db.readOnly,\n logger: app.logger.child({ registry: 'locale' }),\n contextResolver: SYSTEM_CONTEXT,\n })\n const rows = await client.findMany({ limit: MAX_LOCALE_ROWS })\n return rows.map((r) => {\n const row = r as Record<string, unknown>\n return {\n code: String(row.code),\n label: typeof row.label === 'string' && row.label.length > 0 ? row.label : String(row.code),\n enabled: row.enabled !== false,\n direction: row.direction === 'rtl' ? 'rtl' : 'ltr',\n sortOrder: typeof row.sortOrder === 'number' ? row.sortOrder : 0,\n }\n })\n}\n\n/**\n * The effective content-locale set — shipped defaults ⊕ DB overrides, sorted by\n * `sortOrder`. Includes **disabled** locales (listable in the Languages editor);\n * callers that want only switchable/valid locales use {@link enabledLocaleCodes}.\n */\nexport async function resolveEffectiveLocales(app: ToolkitApp): Promise<LocaleEntry[]> {\n let rows: LocaleEntry[]\n try {\n rows = await readLocaleRows(app)\n } catch (err) {\n app.logger.warn({ err }, 'locale registry read failed — falling back to content() defaults')\n return [...shippedDefaults()].sort(bySortOrder)\n }\n const merged = mergeConfigOverrides<LocaleEntry, LocaleEntry>({\n defaults: shippedDefaults(),\n overrides: rows,\n idOfDefault: (l) => l.code,\n idOfOverride: (r) => r.code,\n // No `isDisabled`: a disabled locale stays in the merged list so the editor\n // can re-enable it — the switcher + validateLocale filter on `enabled`.\n })\n return (merged as LocaleEntry[]).sort(bySortOrder)\n}\n\n/** The **enabled** codes from an effective set — the switcher + validateLocale whitelist. */\nexport function enabledLocaleCodes(entries: readonly LocaleEntry[]): string[] {\n return entries.filter((e) => e.enabled).map((e) => e.code)\n}\n\n/**\n * The resolved global i18n singletons (`i18n` settings ⊕ `content()` defaults).\n * `defaultLocale` is guaranteed to be an **enabled** locale: the configured value\n * wins if enabled, else the `content()` default if enabled, else the first enabled\n * locale (last-resort, deterministic). Pass `entries` to reuse an already-resolved\n * effective set (avoids a second registry read within one request).\n */\nexport async function resolveI18nConfig(\n app: ToolkitApp,\n entries?: readonly LocaleEntry[],\n): Promise<I18nConfig> {\n const effective = entries ?? (await resolveEffectiveLocales(app))\n const enabled = enabledLocaleCodes(effective)\n const shippedDefault = getContentConfig().defaultLocale\n\n let values: Record<string, unknown> = {}\n try {\n // Dynamic import: `@murumets-ee/settings/runtime` evaluates the settings schema\n // (`defineTable`) at module load; importing it eagerly would drag that graph into\n // `@murumets-ee/content`'s main index (where only `resolveEffectiveLocales` — which\n // needs no settings — is on the hot import path). Loaded lazily here, only when the\n // i18n singletons are actually read. (Same pattern as rent/ticketing/consent.)\n const { createSettingsClient } = await import('@murumets-ee/settings/runtime')\n const client = createSettingsClient(i18nSettings, { app })\n values = (await client.getAll()) ?? {}\n } catch (err) {\n app.logger.warn({ err }, 'i18n settings read failed — falling back to content() defaults')\n }\n\n const configuredDefault =\n typeof values.defaultLocale === 'string' && values.defaultLocale.length > 0\n ? values.defaultLocale\n : shippedDefault\n\n const defaultLocale = enabled.includes(configuredDefault)\n ? configuredDefault\n : enabled.includes(shippedDefault)\n ? shippedDefault\n : (enabled[0] ?? shippedDefault)\n\n const rawStrategy = values.routingStrategy\n const routingStrategy: RoutingStrategy = (ROUTING_STRATEGIES as readonly string[]).includes(\n typeof rawStrategy === 'string' ? rawStrategy : '',\n )\n ? (rawStrategy as RoutingStrategy)\n : 'no-prefix-default'\n\n return { defaultLocale, routingStrategy }\n}\n","/**\n * The interface-message **registry resolver** (plan/config phase 06, D004/D010).\n *\n * The effective interface strings = **read-time-merge** of the per-package / per-app\n * message JSON defaults ⊕ the DB `message` config-entity override rows. This resolver\n * returns the OVERRIDE rows for one locale as flat {@link MessageEntry}s; the caller\n * (the admin `i18n/request.ts`) nests them via `overridesToCatalog` and passes the\n * result as the last `mergeMessages(...)` argument so DB overrides win — live edits,\n * no rebuild.\n *\n * The read is a **system-context** query on the read-only connection: interface strings\n * must reflect customizations on EVERY request (incl. the unauthenticated login page's\n * `AuthUI` strings), exactly like the app-wide locale/roles reads. It degrades to \"no\n * overrides\" (shipped defaults only) on any error — a fresh app pre-migration or a\n * transient DB blip simply renders the shipped strings.\n */\n\nimport { createSystemReachResolver, type ToolkitApp } from '@murumets-ee/core'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { QueryClient } from '@murumets-ee/entity/query'\nimport type { MessageEntry } from '@murumets-ee/i18n'\nimport { eq } from 'drizzle-orm'\nimport { Message } from './message-entity.js'\n\n/**\n * Elevated **reach** resolver for the app-wide message read (the locale/roles-read\n * analog). Grants `view` and nothing else: this module only READS override rows,\n * and the editor route that writes them is separately gated. Named constructor\n * rather than a `checker: () => true` literal so the identity is enumerable —\n * see the elevation allowlist in `scripts/verify-package-boundaries.mjs`.\n */\nconst SYSTEM_CONTEXT = createSystemReachResolver('message-registry')\n\n/**\n * Defensive upper bound on override rows read for one locale. A fully-translated app\n * across several locales legitimately accumulates thousands of rows, but per-locale the\n * count is bounded by the number of *customized* strings. The read is `log()`-ged when it\n * hits the cap so a silent truncation can't read as \"all overrides applied\" (CLAUDE.md\n * no-unbounded-fan-out rule).\n */\nconst MAX_MESSAGE_OVERRIDE_ROWS = 20000\n\n/**\n * The DB `message` override rows for `locale`, as flat `(namespace, key, value)` entries.\n * Empty when the `message` entity isn't registered (no `messageEditor()` / pre-migration)\n * or on any read error.\n */\nexport async function resolveMessageOverrides(\n app: ToolkitApp,\n locale: string,\n): Promise<MessageEntry[]> {\n if (!app.entities?.has('message')) return []\n const table = schemaRegistry.get('message')\n if (!table) return []\n\n const client = new QueryClient({\n entity: Message,\n db: app.db.readOnly,\n logger: app.logger.child({ registry: 'message' }),\n contextResolver: SYSTEM_CONTEXT,\n })\n\n try {\n const rows = await client.findMany({\n where: eq(table.locale, locale),\n limit: MAX_MESSAGE_OVERRIDE_ROWS,\n })\n if (rows.length >= MAX_MESSAGE_OVERRIDE_ROWS) {\n app.logger.warn(\n { locale, cap: MAX_MESSAGE_OVERRIDE_ROWS },\n 'message override read hit the row cap — some overrides were not applied',\n )\n }\n return rows.map((r) => {\n const row = r as Record<string, unknown>\n return {\n namespace: String(row.namespace),\n key: String(row.key),\n value: String(row.value),\n }\n })\n } catch (err) {\n app.logger.warn(\n { err, locale },\n 'message registry read failed — falling back to shipped defaults only',\n )\n return []\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["SYSTEM_CONTEXT"],"sources":["../src/behaviors/with-overrides.ts","../src/behaviors/workflowable.ts","../src/capabilities.ts","../src/locale-registry.ts","../src/message-registry.ts"],"sourcesContent":["/**\n * `withOverrides()` — opt-in behavior wiring a document entity into the\n * page-scoped override resolver shipped by phase 05.\n *\n * Adds a single internal field `_overrides: Record<string, OverrideDirective>`\n * to the host entity, defaulted to an empty object. The composition step\n * (`composeChromeRegion` in `@murumets-ee/blocks`) reads this field and calls\n * `applyOverrides()` on each chrome region AFTER its blocks have been\n * scope-tagged.\n *\n * Apps opt-in:\n *\n * @example\n * ```ts\n * defineEntity({\n * name: 'page',\n * fields: { ... },\n * behaviors: [\n * publishable(),\n * versionable({ maxPerDoc: 30 }),\n * workflowable(),\n * withOpLog({ retentionDays: 7 }),\n * withOverrides(), // ← enables page-level overrides on scoped blocks\n * ],\n * })\n * ```\n *\n * Why a behavior (not a plain field):\n * - Discoverability — `entitySupports(entity, 'overrides')` lets the boot\n * orphan-sweep find every overrides-eligible entity without hard-coding\n * a list.\n * - Validation — the behavior's `beforeUpdate` hook validates the shape\n * of the `_overrides` map (Zod-equivalent runtime check) so a malformed\n * PATCH body can't poison the field.\n * - Defensive defaults — `_overrides: {}` is auto-seeded on `beforeCreate`\n * so consumers always see a value, never `undefined`.\n *\n * `internal: true` on the field keeps it out of HTTP PATCH input — the\n * `doc:scope-override:set` op is the only path that can mutate it (writes\n * are routed through `AdminClient.updateInternal()` after permission\n * gating).\n *\n * See `plan/blocks/05-scoping-overrides.md` §5 + `progress/findings/F009`.\n */\n\nimport type { Behavior, JsonField } from '@murumets-ee/entity'\nimport { field } from '@murumets-ee/entity'\n\n/**\n * Override-directive shape. Mirrored from `@murumets-ee/blocks` to avoid\n * a circular dep (content → blocks → content); content stores the\n * persisted JSON, blocks owns the resolver. Keep the two shapes in sync.\n */\ntype OverrideDirective =\n | { kind: 'hide' }\n | {\n kind: 'patch'\n props?: Record<string, unknown>\n traits?: Record<string, Record<string, unknown>>\n }\n | { kind: 'replace'; block: Record<string, unknown> }\n\ntype OverridesMap = Record<string, OverrideDirective>\n\nexport type WithOverridesFields = {\n // `required: false` at the type level — the `beforeCreate` hook\n // seeds the field with `{}` when callers omit it, so making the\n // INPUT optional is more ergonomic than threading `_overrides: {}`\n // through every consumer's create-input.\n //\n // NOTE: the DB column is NULLABLE in the generated migration\n // (`_overrides jsonb`) — the JsonField `default` is a runtime\n // function the entity layer calls on create, NOT a SQL DEFAULT\n // clause the migration generator sees. Readers MUST treat NULL\n // as equivalent to `{}` (see `compose-chrome.ts`'s `asOverridesMap`\n // for the canonical pattern). A row inserted\n // via raw SQL or migration that bypasses the entity layer can\n // leave NULL; that's safe.\n _overrides: JsonField\n}\n\n/**\n * Allowed directive kinds — closed set. Unknown discriminators are rejected\n * at the behavior boundary so a malformed PATCH (or future plugin)\n * can't write opaque shapes that the resolver silently ignores.\n */\nconst ALLOWED_KINDS: ReadonlySet<string> = new Set(['hide', 'patch', 'replace'])\n\n/**\n * Maximum number of override entries per document. Bounded to keep the\n * page row's JSON column compact + the rendering hot path predictable.\n * Per phase 05 §9: a page overriding more than ~50 scoped blocks is a\n * smell — at that point the shared chrome is the wrong chrome for that\n * page, and the answer is a different `site_chrome` row or page-local blocks,\n * not more patching.\n */\nconst MAX_OVERRIDES_PER_DOC = 100\n\n/** Marker symbol so introspection can pick out the behavior reliably. */\nconst WITH_OVERRIDES_MARKER = Symbol.for('@murumets-ee/content:withOverrides')\n\ninterface WithOverridesMarker {\n [WITH_OVERRIDES_MARKER]: true\n}\n\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * Walk a plain-object value's OWN keys and reject any that match a\n * prototype-pollution vector. JSON.parse can create own properties\n * with these names; the dispatcher already filters at the route +\n * op-handler boundary, but the behavior is the third layer of\n * defense (any persistence path that bypasses the dispatcher — DB\n * seed, raw SQL migration, future internal API — lands here on the\n * next update).\n *\n * The depth cap of 3 covers every documented over-the-wire shape:\n *\n * - patch.props.__proto__ → depth 1\n * - patch.traits.spacing.__proto__ → depth 2\n * - replace.block.props.__proto__ → depth 2\n * - replace.block.traits.spacing.__proto__ → depth 3\n *\n * (Counting from the directive root at depth 0. The recursion CHECKS\n * keys at the entered depth, then recurses while `depth < 3`, so the\n * deepest check happens at depth=3.)\n *\n * Beyond that depth the structure is field VALUES which are caller-\n * payload (block field type / trait field type) — those are the\n * field-type validator's job, not the override-map gate.\n *\n * `path` is appended to the thrown error for triage.\n */\nfunction assertNoForbiddenKeys(value: unknown, path: string, depth = 0): void {\n if (value === null || value === undefined) return\n if (typeof value !== 'object' || Array.isArray(value)) return\n for (const key of Object.keys(value as Record<string, unknown>)) {\n if (FORBIDDEN_KEYS.has(key)) {\n throw new Error(`withOverrides: reserved key '${key}' at ${path} is not allowed.`)\n }\n if (depth < 3) {\n assertNoForbiddenKeys((value as Record<string, unknown>)[key], `${path}.${key}`, depth + 1)\n }\n }\n}\n\n/**\n * Validate an override-map payload. Throws when malformed; returns the\n * (defensively-cloned, sanitised) value when ok. Called from the\n * `beforeUpdate` hook — `internal: true` blocks PATCH-side input but\n * the `doc:scope-override:set` op writes via `updateInternal` and so\n * still flows through here.\n *\n * Defense layered: outer scope-block-id keys + each directive's\n * recursive nested keys (patch.props / patch.traits / replace.block.props\n * / replace.block.traits) all walk through `assertNoForbiddenKeys`. The\n * dispatcher's own handler validates the same set; this is the\n * defense-in-depth replay at the persistence boundary.\n */\nfunction validateOverridesMap(value: unknown): OverridesMap {\n if (value === null || value === undefined) return {}\n if (typeof value !== 'object' || Array.isArray(value)) {\n throw new Error('withOverrides: _overrides must be a plain object map.')\n }\n const map = value as Record<string, unknown>\n const keys = Object.keys(map)\n if (keys.length > MAX_OVERRIDES_PER_DOC) {\n throw new Error(\n `withOverrides: _overrides has ${keys.length} entries; cap is ${MAX_OVERRIDES_PER_DOC}.`,\n )\n }\n const sanitised: OverridesMap = {}\n for (const key of keys) {\n // Skip prototype-pollution vectors at the OUTER (scopeBlockId)\n // key boundary first.\n if (FORBIDDEN_KEYS.has(key)) {\n throw new Error(`withOverrides: reserved key '${key}' is not allowed.`)\n }\n const directive = map[key]\n if (\n directive === null ||\n directive === undefined ||\n typeof directive !== 'object' ||\n Array.isArray(directive)\n ) {\n throw new Error(`withOverrides: _overrides['${key}'] must be a directive object.`)\n }\n const kind = (directive as { kind?: unknown }).kind\n if (typeof kind !== 'string' || !ALLOWED_KINDS.has(kind)) {\n throw new Error(\n `withOverrides: _overrides['${key}'].kind must be one of [hide, patch, replace]; got ${String(\n kind,\n )}.`,\n )\n }\n // Kind-specific shape enforcement (CR bot major) — `kind` alone\n // isn't enough; a malformed `{ kind: 'replace' }` (no block) or\n // `{ kind: 'patch', props: 123 }` would otherwise pass the gate\n // and land invalid data in persistence. Mirror the dispatcher\n // handler's shape checks at this third defense layer.\n if (kind === 'patch') {\n const d = directive as { props?: unknown; traits?: unknown }\n assertPlainObjectOrUndefined(d.props, `_overrides['${key}'].props`)\n assertPlainObjectOrUndefined(d.traits, `_overrides['${key}'].traits`)\n }\n if (kind === 'replace') {\n const block = (directive as { block?: unknown }).block\n if (\n block === null ||\n block === undefined ||\n typeof block !== 'object' ||\n Array.isArray(block) ||\n typeof (block as { id?: unknown }).id !== 'string'\n ) {\n throw new Error(\n `withOverrides: _overrides['${key}'] replace payload must include an object 'block' with a string 'id'.`,\n )\n }\n }\n // Recurse into the directive payload — patch.props / patch.traits\n // / replace.block.props / replace.block.traits — and reject any\n // forbidden own-property keys at every level.\n assertNoForbiddenKeys(directive, `_overrides['${key}']`)\n sanitised[key] = directive as OverrideDirective\n }\n return sanitised\n}\n\n/**\n * Reject a directive sub-field that's present but not a plain object\n * (array / primitive / null). `undefined` is allowed — the field is\n * optional. Mirrors the dispatcher handler's `assertPlainObjectOrUndefined`.\n */\nfunction assertPlainObjectOrUndefined(value: unknown, label: string): void {\n if (value === undefined) return\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new Error(`withOverrides: ${label} must be an object.`)\n }\n}\n\n/**\n * Mark an entity as participating in the page-scoped override resolver.\n * See module-level JSDoc for the full contract.\n */\nexport function withOverrides(): Behavior<WithOverridesFields, 'withOverrides'> {\n const behavior: Behavior<WithOverridesFields, 'withOverrides'> & WithOverridesMarker = {\n name: 'withOverrides',\n fields: {\n // `internal: true` blocks PATCH-input poisoning (the\n // `doc:scope-override:set` op writes via updateInternal which\n // bypasses the input filter). `default: () => ({})` seeds the\n // value on every create call, so callers can omit `_overrides`\n // entirely (no `required: true` needed at the input level).\n // `refScan: true` (images PR16): replacement blocks in the map hold media\n // refs — with a placement's own spot crop — exactly as the block tree\n // does, so the usage index records them. Without it an image placed only\n // in an override was used NOWHERE as far as delete protection and the\n // spot-crop sweep could tell, and the sweep would collect the files of a\n // crop a published page still shows.\n _overrides: field.json({\n default: () => ({}),\n internal: true,\n refScan: true,\n }),\n },\n hooks: {\n beforeCreate: async (data) => {\n // Seed the empty map on create so the column is never null in DB.\n // Caller-supplied `_overrides` on create is unusual (overrides\n // are typically dispatched op-by-op) but if they DO pass one,\n // validate it.\n if (data._overrides === undefined) {\n data._overrides = {}\n } else {\n data._overrides = validateOverridesMap(data._overrides)\n }\n return data\n },\n beforeUpdate: async (_id, data) => {\n // Only validate when the patch tries to write `_overrides`.\n if (data._overrides === undefined) return data\n data._overrides = validateOverridesMap(data._overrides)\n return data\n },\n },\n [WITH_OVERRIDES_MARKER]: true,\n }\n return behavior\n}\n\n/**\n * Check whether an entity declares `withOverrides()`. Returns `true`\n * when the entity's behaviors list contains a behavior tagged with\n * the marker symbol. Used by the boot orphan-sweep + admin UI gating.\n */\nexport function hasWithOverrides(\n behaviors: ReadonlyArray<{ name: string } & Record<symbol, unknown>> | undefined,\n): boolean {\n if (!behaviors) return false\n return behaviors.some((b) => Boolean(b?.[WITH_OVERRIDES_MARKER]))\n}\n\n/**\n * Export the directive type alias for downstream consumers that want\n * to type-narrow without depending on `@murumets-ee/blocks`. The\n * resolver lives in blocks; the persisted shape lives here.\n */\nexport type { OverrideDirective, OverridesMap }\n","/**\n * Workflowable behavior — editorial workflow with approval gates.\n *\n * Adds workflow status tracking fields to an entity and enforces the\n * documented transition state machine in `beforeUpdate`. Works alongside\n * `publishable()` — the workflow status tracks the editorial pipeline,\n * while publishable's `status` remains the public-facing state.\n *\n * Workflow state machine:\n *\n * draft ─submit─► in_review ─approve─► approved ─publish─► published\n * │ ▲ │\n * │ └────resubmit─────┐ │\n * └─reject─► rejected ─┘ │\n * │\n * unpublish ◄──────────────┘\n * (back to draft)\n *\n * Internal-write only: every workflow field is declared `internal: true`,\n * so the HTTP PATCH surface strips them from caller input. Workflow\n * transitions are performed by trusted server-side routes via\n * `AdminClient.updateInternal()` after authorizing the action with the\n * permission checker. The `beforeUpdate` hook validates that the\n * proposed transition is in the allowed table.\n *\n * Permission gating lives in the route handler (admin-ui workflow\n * routes), not in this behavior — by the time the hook fires, the\n * route has already authorized via `checkPermission`. The hook is\n * the structural defense: even a bug in the route layer cannot push\n * a workflow row into an illegal state.\n *\n * UI integration is tracked in GitHub issue #124. Until the workflow\n * UX ships, the behavior can still be added to entities — it'll simply\n * seed `_workflowStatus = 'draft'` on create and reject anyone trying\n * to write the field via PATCH.\n */\n\nimport type {\n Behavior,\n BehaviorContext,\n DateField,\n SelectField,\n TextField,\n} from '@murumets-ee/entity'\nimport { field, USER_ID_MAX_LENGTH } from '@murumets-ee/entity'\n\nexport const WORKFLOW_STATES = ['draft', 'in_review', 'approved', 'rejected', 'published'] as const\nexport type WorkflowState = (typeof WORKFLOW_STATES)[number]\n\n/**\n * Allowed transitions table. Each key is the *current* workflow status;\n * the value is the set of statuses it can move to.\n *\n * Encoded as a const-asserted record so a typo in the source produces\n * a compile error rather than a silent no-match at runtime.\n */\nexport const WORKFLOW_TRANSITIONS = {\n draft: ['in_review'],\n in_review: ['approved', 'rejected'],\n approved: ['published', 'in_review'],\n rejected: ['in_review', 'draft'],\n published: ['draft'],\n} as const satisfies Record<WorkflowState, readonly WorkflowState[]>\n\n/**\n * Is `to` an allowed transition from `from`? Pure, no I/O.\n */\nexport function isAllowedTransition(from: WorkflowState, to: WorkflowState): boolean {\n if (from === to) return true // no-op self-transition is fine\n const allowed = WORKFLOW_TRANSITIONS[from] as readonly WorkflowState[] | undefined\n return allowed?.includes(to) ?? false\n}\n\n/**\n * Thrown when `beforeUpdate` rejects a workflow transition. Distinct\n * subclass so admin-ui's API handler can map it to HTTP 400.\n */\nexport class WorkflowTransitionError extends Error {\n readonly from: WorkflowState\n readonly to: WorkflowState\n\n constructor(from: WorkflowState, to: WorkflowState) {\n super(\n `Invalid workflow transition: ${from} → ${to}. ` +\n `Allowed transitions from \"${from}\": [${(WORKFLOW_TRANSITIONS[from] ?? []).join(', ')}].`,\n )\n this.name = 'WorkflowTransitionError'\n this.from = from\n this.to = to\n }\n}\n\nexport type WorkflowableFields = {\n _workflowStatus: SelectField & { required: true }\n _workflowAssignee: TextField\n _workflowNote: TextField\n _workflowUpdatedAt: DateField\n}\n\nexport function workflowable(): Behavior<WorkflowableFields> {\n return {\n name: 'workflowable',\n fields: {\n _workflowStatus: field.select({\n options: WORKFLOW_STATES as unknown as string[],\n default: 'draft',\n indexed: true,\n required: true,\n internal: true,\n }) as SelectField & { required: true },\n // `_workflowAssignee` stores a better-auth user ID — varchar(255)\n // per CLAUDE.md §\"User IDs\", emitted via `maxLength: 255` (#430).\n // `_workflowNote` is free-text, so it stays unbounded `text`.\n _workflowAssignee: field.text({\n required: false,\n internal: true,\n maxLength: USER_ID_MAX_LENGTH,\n }),\n _workflowNote: field.text({ required: false, internal: true }),\n _workflowUpdatedAt: field.date({ internal: true }),\n },\n hooks: {\n beforeCreate: async (data) => {\n data._workflowStatus = 'draft'\n data._workflowUpdatedAt = new Date()\n return data\n },\n beforeUpdate: async (\n _id: string,\n data: Record<string, unknown>,\n ctx?: BehaviorContext,\n ): Promise<Record<string, unknown>> => {\n // Only validate when the patch tries to change the workflow status.\n // Updates that don't touch _workflowStatus pass through (the field's\n // `internal: true` already prevents it from sneaking in via PATCH;\n // only updateInternal callers reach this branch with a value).\n const proposed = data._workflowStatus\n if (proposed === undefined) return data\n\n if (!WORKFLOW_STATES.includes(proposed as WorkflowState)) {\n throw new WorkflowTransitionError('draft', proposed as WorkflowState)\n }\n\n // Need the current row to validate the from→to transition. The\n // lazy loader is provided by AdminClient — present on update,\n // absent on create.\n const current = await ctx?.loadCurrent?.()\n const fromState = (current?._workflowStatus as WorkflowState | undefined) ?? 'draft'\n const toState = proposed as WorkflowState\n\n if (!isAllowedTransition(fromState, toState)) {\n throw new WorkflowTransitionError(fromState, toState)\n }\n\n data._workflowUpdatedAt = new Date()\n return data\n },\n },\n }\n}\n","/**\n * Per-entity content capability gates.\n *\n * The content plugin attaches different infrastructure tables to an\n * entity based on which behaviors it has:\n * - `publishable()` → drafts table\n * - `versionable()` → versions table\n * - `publishable() + translatable fields` → locale_status table\n * - any translatable field → translations table (also written by the\n * entity package; included for symmetric capability checks)\n *\n * Calling a capability-specific ContentClient method (`saveDraft`,\n * `publishDraft`, `setLocaleStatus`, `restoreVersion`, ...) on an\n * entity that doesn't have the required behavior is a programming\n * error. ContentClient throws `EntityCapabilityError` on writes and\n * silently returns falsy on reads. Callers that want to gate ahead —\n * \"should I show the draft button on this entity row?\" — use\n * `entitySupports()` instead of catching the error.\n */\n\nimport type { Entity, FieldConfig } from '@murumets-ee/entity'\n\nexport type EntityCapability = 'drafts' | 'versions' | 'localeStatus' | 'translations'\n\n/**\n * Does this entity support the given content capability? Pure check\n * against the entity's declared behaviors and field translatability —\n * no plugin lookup, no DB call.\n */\nexport function entitySupports(entity: Entity, capability: EntityCapability): boolean {\n switch (capability) {\n case 'drafts':\n return hasBehavior(entity, 'publishable')\n case 'versions':\n return hasBehavior(entity, 'versionable')\n case 'localeStatus':\n return hasBehavior(entity, 'publishable') && hasTranslatableField(entity)\n case 'translations':\n return hasTranslatableField(entity)\n }\n}\n\nfunction hasBehavior(entity: Entity, name: string): boolean {\n return entity.behaviors?.some((b) => b.name === name) ?? false\n}\n\nfunction hasTranslatableField(entity: Entity): boolean {\n const fields = entity.allFields as Record<string, FieldConfig>\n for (const key of Object.keys(fields)) {\n const f = fields[key]\n if (f && (f as { translatable?: boolean }).translatable === true) return true\n }\n return false\n}\n","/**\n * The content-locale **registry resolvers** (plan/config phase 05 §7, D004/D009/SD006).\n *\n * The effective content-locale set = **read-time-merge** of the `content()` shipped\n * defaults ⊕ the DB `locale` config-entity override rows. Resolved **once per request**\n * at context establishment (the admin API handler + `withAdminContext`) and threaded\n * onto {@link RequestContext.availableLocales}, so `validateLocale`'s synchronous\n * whitelist reflects runtime-added / disabled locales with **no rebuild and no\n * globalThis cache** — multi-process / multi-tenant correct.\n *\n * The registry read is a **system-context** query: managing the language set is\n * admin-only (`locale:*`), but every role must resolve valid locales to edit content,\n * exactly like the app-wide roles read. Reads degrade to the static `content()`\n * defaults on any error (fresh app pre-migration, transient DB failure) — deny-by-\n * default is preserved (a not-yet-known locale simply isn't whitelisted).\n */\n\nimport { createSystemReachResolver, mergeConfigOverrides, type ToolkitApp } from '@murumets-ee/core'\nimport { QueryClient } from '@murumets-ee/entity/query'\nimport { getContentConfig } from './content-config-store.js'\nimport { i18nSettings, ROUTING_STRATEGIES } from './i18n-settings.js'\nimport { Locale } from './locale-entity.js'\nimport type { I18nConfig, LocaleDirection, LocaleEntry, RoutingStrategy } from './types.js'\n\n/**\n * Elevated **reach** resolver for the app-wide registry read (the roles-read\n * analog). It grants `view` and nothing else — this module only ever READS the\n * `locale` rows, so an all-allow checker would have handed every caller of this\n * resolver a write grant it has no use for. The named constructor owns that\n * exception centrally rather than leaving it to be re-derived per call site;\n * a hand-written `checker: () => true` here is refused by `verify:boundaries`.\n *\n * The explicit id is the audit identity — two elevated readers sharing the\n * default `'system'` are indistinguishable in the log.\n */\nconst SYSTEM_CONTEXT = createSystemReachResolver('locale-registry')\n\n/** The `content()`-shipped locales projected to full {@link LocaleEntry}s (all enabled). */\nfunction shippedDefaults(): LocaleEntry[] {\n return getContentConfig().locales.map((l, i) => ({\n code: l.code,\n label: l.label,\n enabled: true,\n direction: 'ltr' as LocaleDirection,\n sortOrder: i,\n }))\n}\n\n/** Ascending by `sortOrder`, then `code` for a stable tie-break. */\nfunction bySortOrder(a: LocaleEntry, b: LocaleEntry): number {\n return a.sortOrder - b.sortOrder || a.code.localeCompare(b.code)\n}\n\n/**\n * Defensive upper bound on `locale` rows to read (a tenant realistically has <20). The\n * Languages-editor route MUST reconcile against the SAME bound so it can't miss/strand\n * rows — see admin-ui `locale-routes` `MAX_EXISTING_LOCALE_ROWS` (kept in sync).\n */\nconst MAX_LOCALE_ROWS = 1000\n\n/** Read + normalize the DB `locale` override rows (system context). */\nasync function readLocaleRows(app: ToolkitApp): Promise<LocaleEntry[]> {\n if (!app.entities?.has('locale')) return []\n const client = new QueryClient({\n entity: Locale,\n db: app.db.readOnly,\n logger: app.logger.child({ registry: 'locale' }),\n contextResolver: SYSTEM_CONTEXT,\n })\n const rows = await client.findMany({ limit: MAX_LOCALE_ROWS })\n return rows.map((r) => {\n const row = r as Record<string, unknown>\n return {\n code: String(row.code),\n label: typeof row.label === 'string' && row.label.length > 0 ? row.label : String(row.code),\n enabled: row.enabled !== false,\n direction: row.direction === 'rtl' ? 'rtl' : 'ltr',\n sortOrder: typeof row.sortOrder === 'number' ? row.sortOrder : 0,\n }\n })\n}\n\n/**\n * The effective content-locale set — shipped defaults ⊕ DB overrides, sorted by\n * `sortOrder`. Includes **disabled** locales (listable in the Languages editor);\n * callers that want only switchable/valid locales use {@link enabledLocaleCodes}.\n */\nexport async function resolveEffectiveLocales(app: ToolkitApp): Promise<LocaleEntry[]> {\n let rows: LocaleEntry[]\n try {\n rows = await readLocaleRows(app)\n } catch (err) {\n app.logger.warn({ err }, 'locale registry read failed — falling back to content() defaults')\n return [...shippedDefaults()].sort(bySortOrder)\n }\n const merged = mergeConfigOverrides<LocaleEntry, LocaleEntry>({\n defaults: shippedDefaults(),\n overrides: rows,\n idOfDefault: (l) => l.code,\n idOfOverride: (r) => r.code,\n // No `isDisabled`: a disabled locale stays in the merged list so the editor\n // can re-enable it — the switcher + validateLocale filter on `enabled`.\n })\n return (merged as LocaleEntry[]).sort(bySortOrder)\n}\n\n/** The **enabled** codes from an effective set — the switcher + validateLocale whitelist. */\nexport function enabledLocaleCodes(entries: readonly LocaleEntry[]): string[] {\n return entries.filter((e) => e.enabled).map((e) => e.code)\n}\n\n/**\n * The resolved global i18n singletons (`i18n` settings ⊕ `content()` defaults).\n * `defaultLocale` is guaranteed to be an **enabled** locale: the configured value\n * wins if enabled, else the `content()` default if enabled, else the first enabled\n * locale (last-resort, deterministic). Pass `entries` to reuse an already-resolved\n * effective set (avoids a second registry read within one request).\n */\nexport async function resolveI18nConfig(\n app: ToolkitApp,\n entries?: readonly LocaleEntry[],\n): Promise<I18nConfig> {\n const effective = entries ?? (await resolveEffectiveLocales(app))\n const enabled = enabledLocaleCodes(effective)\n const shippedDefault = getContentConfig().defaultLocale\n\n let values: Record<string, unknown> = {}\n try {\n // Dynamic import: `@murumets-ee/settings/runtime` evaluates the settings schema\n // (`defineTable`) at module load; importing it eagerly would drag that graph into\n // `@murumets-ee/content`'s main index (where only `resolveEffectiveLocales` — which\n // needs no settings — is on the hot import path). Loaded lazily here, only when the\n // i18n singletons are actually read. (Same pattern as rent/ticketing/consent.)\n const { createSettingsClient } = await import('@murumets-ee/settings/runtime')\n const client = createSettingsClient(i18nSettings, { app })\n values = (await client.getAll()) ?? {}\n } catch (err) {\n app.logger.warn({ err }, 'i18n settings read failed — falling back to content() defaults')\n }\n\n const configuredDefault =\n typeof values.defaultLocale === 'string' && values.defaultLocale.length > 0\n ? values.defaultLocale\n : shippedDefault\n\n const defaultLocale = enabled.includes(configuredDefault)\n ? configuredDefault\n : enabled.includes(shippedDefault)\n ? shippedDefault\n : (enabled[0] ?? shippedDefault)\n\n const rawStrategy = values.routingStrategy\n const routingStrategy: RoutingStrategy = (ROUTING_STRATEGIES as readonly string[]).includes(\n typeof rawStrategy === 'string' ? rawStrategy : '',\n )\n ? (rawStrategy as RoutingStrategy)\n : 'no-prefix-default'\n\n return { defaultLocale, routingStrategy }\n}\n","/**\n * The interface-message **registry resolver** (plan/config phase 06, D004/D010).\n *\n * The effective interface strings = **read-time-merge** of the per-package / per-app\n * message JSON defaults ⊕ the DB `message` config-entity override rows. This resolver\n * returns the OVERRIDE rows for one locale as flat {@link MessageEntry}s; the caller\n * (the admin `i18n/request.ts`) nests them via `overridesToCatalog` and passes the\n * result as the last `mergeMessages(...)` argument so DB overrides win — live edits,\n * no rebuild.\n *\n * The read is a **system-context** query on the read-only connection: interface strings\n * must reflect customizations on EVERY request (incl. the unauthenticated login page's\n * `AuthUI` strings), exactly like the app-wide locale/roles reads. It degrades to \"no\n * overrides\" (shipped defaults only) on any error — a fresh app pre-migration or a\n * transient DB blip simply renders the shipped strings.\n */\n\nimport { createSystemReachResolver, type ToolkitApp } from '@murumets-ee/core'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { QueryClient } from '@murumets-ee/entity/query'\nimport type { MessageEntry } from '@murumets-ee/i18n'\nimport { eq } from 'drizzle-orm'\nimport { Message } from './message-entity.js'\n\n/**\n * Elevated **reach** resolver for the app-wide message read (the locale/roles-read\n * analog). Grants `view` and nothing else: this module only READS override rows,\n * and the editor route that writes them is separately gated. Named constructor\n * rather than a `checker: () => true` literal so the identity is enumerable —\n * see the elevation allowlist in `scripts/verify-package-boundaries.mjs`.\n */\nconst SYSTEM_CONTEXT = createSystemReachResolver('message-registry')\n\n/**\n * Defensive upper bound on override rows read for one locale. A fully-translated app\n * across several locales legitimately accumulates thousands of rows, but per-locale the\n * count is bounded by the number of *customized* strings. The read is `log()`-ged when it\n * hits the cap so a silent truncation can't read as \"all overrides applied\" (CLAUDE.md\n * no-unbounded-fan-out rule).\n */\nconst MAX_MESSAGE_OVERRIDE_ROWS = 20000\n\n/**\n * The DB `message` override rows for `locale`, as flat `(namespace, key, value)` entries.\n * Empty when the `message` entity isn't registered (no `messageEditor()` / pre-migration)\n * or on any read error.\n */\nexport async function resolveMessageOverrides(\n app: ToolkitApp,\n locale: string,\n): Promise<MessageEntry[]> {\n if (!app.entities?.has('message')) return []\n const table = schemaRegistry.get('message')\n if (!table) return []\n\n const client = new QueryClient({\n entity: Message,\n db: app.db.readOnly,\n logger: app.logger.child({ registry: 'message' }),\n contextResolver: SYSTEM_CONTEXT,\n })\n\n try {\n const rows = await client.findMany({\n where: eq(table.locale, locale),\n limit: MAX_MESSAGE_OVERRIDE_ROWS,\n })\n if (rows.length >= MAX_MESSAGE_OVERRIDE_ROWS) {\n app.logger.warn(\n { locale, cap: MAX_MESSAGE_OVERRIDE_ROWS },\n 'message override read hit the row cap — some overrides were not applied',\n )\n }\n return rows.map((r) => {\n const row = r as Record<string, unknown>\n return {\n namespace: String(row.namespace),\n key: String(row.key),\n value: String(row.value),\n }\n })\n } catch (err) {\n app.logger.warn(\n { err, locale },\n 'message registry read failed — falling back to shipped defaults only',\n )\n return []\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./versionable-Br_G5VGR.mjs";import{r as t}from"./errors-NRNILhhm.mjs";import{i as n,o as r,r as i}from"./routing-strategy-DQGPPBlW.mjs";import{n as a,t as o}from"./content-config-store-8omQF5p5.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{a as u,n as d,r as f,t as p}from"./schema-D8sBQozw.mjs";import{l as m,t as h,u as g}from"./schema-0zayzpD_.mjs";import{r as _,t as v}from"./routes-C9I1iZw8.mjs";import{n as y}from"./locks-5779ugSW.mjs";import{MAX_PATH_DEPTH as b,hasPathParent as x,resolveEffectiveSlugMode as S,resolvePattern as C}from"@murumets-ee/entity";import{definePlugin as w,getApp as T,getContext as E,runAsCli as D}from"@murumets-ee/core";import{column as O,defineTable as k,schemaRegistry as A}from"@murumets-ee/db";import{asc as j,eq as M,inArray as N,sql as P}from"drizzle-orm";import{z as F}from"zod";import{createHash as I}from"node:crypto";const L=Symbol.for(`@murumets-ee/content:locale-status-writer`);async function R(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 z(e){globalThis[L]=(n,r)=>{let{localeStatus:i}=c(n.name);if(i)return async(n,a,o)=>{let s=E()?.availableLocales??e;if(!s.includes(a))throw new t(a,[...s]);await R(i.makeClient(r),n,a,o)}}}function B(){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),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).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).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).exists({entityId:t}):!1},async hasAnyLocaleActivity({documentType:e,documentId:t,db:n}){let{localeStatus:r}=c(e);if(r&&await r.makeClient(n).exists({entityId:t}))return!0;let i=A.get(`${e}_translations`);if(i){let e=i.entityId;if(e!==void 0&&(await n.select({one:P`1`}).from(i).where(M(e,t)).limit(1)).length>0)return!0}return!1}}}function V(e){let{defaultLocale:t}=o();return e===`_`||e===t?{defaultLocale:t}:{locale:e,defaultLocale:t}}function H(e){let{defaultLocale:t}=o();return e===`_`||e===t?`_`:e}async function U(e,t,n,r){let{drafts:i}=c(e);if(!i)return;let a=await i.makeClient(r).findOne({entityId:t,locale:n});return a?a.data??void 0:void 0}function W(){return{async snapshotCurrentState({documentType:t,documentId:n,locale:r,user:i}){let a=T(),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=H(r),d=await c.findById(n,V(u));if(!d)return!1;let f=await U(t,n,u,l),p=f?{...d,...f}:{...d},[{createContentContext:m},{createVersionSnapshot:h}]=await Promise.all([import(`./context-Bw0n6P47.mjs`).then(e=>e.t),import(`./versions-BDxB6k_S.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}}}const G=Symbol.for(`@murumets-ee/content:path-writers-factory`),K=Symbol.for(`@murumets-ee/content:cascade-trigger-factory`);function q(e){globalThis[K]=e}function J(){let e=globalThis[K];return typeof e==`function`?e:void 0}function Y(e,t){let n=d.makeClient(e.db.readWrite);return async 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=o(),a=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(!x(t))return;let i=J();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 o=i===r.locale?r.row??null:await l.findById(r.id,{locale:i,defaultLocale:a});if(!o){await f({pathsClient:n,entityType:r.entityName,entityId:r.id,locale:i,path:null,skipReason:`row-unreadable`,reservedPathPrefixes:s,logger:e.logger});continue}let c=await m({entity:t,row:o,locale:i,defaultLocale:a,pathsClient:n}),d=await f({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)}}}function X(e,t){let n=d.makeClient(e.db.readWrite);return async r=>{let i=o(),a=i.defaultLocale,s=i.reservedPathPrefixes??[],c=r.locale??a,l=null;r.id!==void 0&&(l=await e.getClient(t).findById(r.id,{locale:c,defaultLocale:a}));let u=S(r.data.slugMode,l?.slugMode);await g({entity:t,data:r.data,currentRow:l,id:r.id,locale:c,defaultLocale:a,slugMode:u,reservedPathPrefixes:s,pathsClient:n})}}function ee(){globalThis[G]=(e,t)=>({pathIndexWriter:Y(e,t),pathConflictGuard:X(e,t)})}const Z=F.object({entityType:F.string().min(1),parentId:F.string().uuid(),locale:F.string().min(1)}),Q=F.object({entity:F.string().min(1).optional(),locale:F.string().min(1).optional(),dryRun:F.boolean()});async function te(e,t,n={}){let{entityType:r,parentId:i,locale:a}=t,s=n.maxItems??2e3,c=n.concurrency??5,l={processed:0,regenerated:0,unchanged:0,skipped:0,failed:0,truncated:!1},p=e=>({phase:e,...l}),h=()=>({entityType:r,parentId:i,locale:a,cap:s,...l}),g=e.entities.get(r);if(!g)return e.logger.warn({entityType:r,parentId:i,locale:a},`paths-cascade: unknown entity type — nothing to regenerate`),n.onProgress?.(p(`done`)),h();if(!x(g))return e.logger.warn({entityType:r,parentId:i,locale:a},`paths-cascade: entity is not a pathParent() hierarchy — nothing to regenerate`),n.onProgress?.(p(`done`)),h();let _=o(),v=_.defaultLocale,y=_.reservedPathPrefixes??[],b=d.makeClient(e.db.readWrite),S=e.getClient(g),C=S.getTable().parent,w=async t=>{let n=t.id;try{let i=await m({entity:g,row:t,locale:a,defaultLocale:v,pathsClient:b});switch((await f({pathsClient:b,entityType:r,entityId:n,locale:a,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:a},`paths-cascade: failed to regenerate one descendant path — continuing (per-item isolation)`)}},T=[i];for(;T.length>0;){let e=s-l.processed;if(e<=0){l.truncated=!0;break}let t=await S.findMany({where:N(C,T),locale:a,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 u(r,c,w),l.processed+=r.length,n.onProgress?.(p(`walking`)),l.truncated)break;T=r.map(e=>e.id)}return l.truncated&&e.logger.warn({entityType:r,parentId:i,locale:a,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?.(p(`done`)),h()}async function ne(e,t,n={}){let r=n.pageSize??200,i=n.concurrency??5,a=n.sampleCap??25,s=t.dryRun,c=o(),l=c.defaultLocale,p=c.reservedPathPrefixes??[],h=c.locales.map(e=>e.code),g=t.locale?h.filter(e=>e===t.locale):h,_=[...e.entities.values()].filter(e=>!!e.frontend?.path&&(t.entity===void 0||e.name===t.entity)),v=t.entity!==void 0&&_.length===0,y=t.locale!==void 0&&g.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:h,dryRun:s},`paths-rebuild: --locale matches no configured locale — nothing to rebuild (likely a typo in the locale code)`);let S=v||y?{entity:v,locale:y}:void 0,C=_.some(e=>x(e)),w=s&&C?`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,T=d.makeClient(e.db.readWrite),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=[],M=!1,N=e=>{A.length<a?A.push(e):M=!0},P=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:P,sample:A,sampleTruncated:M,...w===void 0?{}:{hierarchyNote:w},...S===void 0?{}:{unmatchedScope:S}}),I=async(t,n,r,i)=>{let a=r.id,o=!1;try{let c=await m({entity:t,row:r,locale:i,defaultLocale:l,pathsClient:T}),u=await f({pathsClient:T,entityType:n,entityId:a,locale:i,path:c.path,...c.skipReason!==void 0&&{skipReason:c.skipReason},suppressNoteworthy:[`parent-not-indexed`],reservedPathPrefixes:p,logger:e.logger,dryRun:s}),d=`${n}:${a}:${i}`;switch(u.outcome){case`unchanged`:k(d,`unchanged`);break;case`skipped-null`:case`skipped-reserved`:k(d,`skipped`);break;case`conflict`:k(d,`conflicts`),N({kind:`conflict`,entityType:n,entityId:a,locale:i,oldPath:u.oldPath,newPath:u.newPath,...u.conflictWith?{conflictWith:u.conflictWith.entityId}:{}});break;case`created`:s?k(d,`missing`):(E.applied+=1,o=!0,k(d,`resolved`)),N({kind:`missing`,entityType:n,entityId:a,locale:i,oldPath:null,newPath:u.newPath});break;case`renamed`:E.changed+=1,s||(E.applied+=1,o=!0),k(d,`resolved`),N({kind:`changed`,entityType:n,entityId:a,locale:i,oldPath:u.oldPath,newPath:u.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 g){let d;for(;;){let f=await t.findMany({locale:e,defaultLocale:l,limit:r,orderBy:j(c),...d===void 0?{}:{cursor:{field:`id`,value:d,direction:`asc`}}});if(f.length===0)break;await u(f,i,async t=>{await I(o,s,t,e)&&(a+=1)}),E.scanned+=f.length;let p=f[f.length-1];if(p&&(d=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 u(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=_.filter(e=>x(e)),B=new Set(z.map(e=>e.name)),V=s||!C?1:b;for(let e=0;e<V;e++){P=e+1;let t=await L(e===0?_:z);if(e>0&&(t+=await R(B)),t===0)break}M&&e.logger.warn({scanned:E.scanned,changed:E.changed,missing:O.missing,conflicts:O.conflicts,sampleCap:a},`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 re(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 D(async()=>{let n=await te(e,Z.parse(t.payload),{onProgress:e=>t.updateProgress(e)});e.logger.info(n,`paths-cascade: descendant regeneration complete`)})}),n(a,async t=>{await D(async()=>{let n=await ne(e,Q.parse(t.payload),{onProgress:e=>t.updateProgress(e)});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`)})}),q(async(e,t,n,a)=>{await new r({app:e,logger:e.logger}).enqueue(i,{entityType:t.name,parentId:n,locale:a})})}const $=k({name:`toolkit_path_patterns`,columns:{id:O.uuid({primaryKey:!0,defaultRandom:!0}),entityType:O.varchar({length:100,notNull:!0,pgName:`entity_type`}),locale:O.varchar({length:10,notNull:!0}),patternHash:O.varchar({length:64,notNull:!0,pgName:`pattern_hash`}),updatedAt:O.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityType`,`locale`]}]}),ie=$.table;function ae(e,t,n){let r=C(e,t,n);if(r===null)return null;let i=typeof r==`function`?r.toString():JSON.stringify(r);return I(`sha256`).update(i).digest(`hex`)}async function oe(e){let t=o(),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=ae(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 se=async(e,t,n)=>{if(t.length===0)return new Map;let[{buildEnumerateDeps:r},{getCanonicalPaths:i}]=await Promise.all([import(`./resolve-context-MRZZ3rQl.mjs`).then(e=>e.l),import(`./enumerate-CBqu63Kp.mjs`).then(e=>e.n)]),a=r(T()),o=await a.filterPublished(e,t,n);return o.size===0?new Map:i(e,[...o],n,a)};function ce(e){return k({name:`${e.name}_drafts`,columns:{id:O.uuid({primaryKey:!0,defaultRandom:!0}),entityId:O.uuid({notNull:!0,pgName:`entity_id`}),locale:O.varchar({length:10,notNull:!0,default:`_`}),data:O.jsonb({notNull:!0}),createdBy:O.varchar({length:255,notNull:!0,pgName:`created_by`}),createdByName:O.varchar({length:255,pgName:`created_by_name`}),createdAt:O.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),updatedAt:O.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityId`,`locale`]}]})}function le(e){return k({name:`${e.name}_locale_status`,columns:{id:O.uuid({primaryKey:!0,defaultRandom:!0}),entityId:O.uuid({notNull:!0,pgName:`entity_id`}),locale:O.varchar({length:10,notNull:!0}),status:O.varchar({length:20,notNull:!0,default:`draft`}),publishedAt:O.timestamp({withTimezone:!0,pgName:`published_at`})},unique:[{on:[`entityId`,`locale`]}]})}function ue(e){return k({name:`${e.name}_versions`,columns:{id:O.uuid({primaryKey:!0,defaultRandom:!0}),entityId:O.uuid({notNull:!0,pgName:`entity_id`}),version:O.integer({notNull:!0}),locale:O.varchar({length:10,notNull:!0,default:`_`}),data:O.jsonb({notNull:!0}),delta:O.jsonb(),status:O.varchar({length:20}),createdBy:O.varchar({length:255,pgName:`created_by`}),createdByName:O.varchar({length:255,pgName:`created_by_name`}),createdAt:O.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),isAutosave:O.boolean({notNull:!0,default:!1,pgName:`is_autosave`})},unique:[{on:[`entityId`,`version`,`locale`]}]})}function de(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,o=t===!1?void 0:{publishable:!0,...typeof t==`object`&&t.exclude?{exclude:t.exclude}:{}};return w({name:`@murumets-ee/content`,contributes:{"blocks.documentDraftStore":B(),"blocks.documentVersionStore":W(),"blocks.publishedPathResolver":se,...o?{"trash.autoEnable":o}:{},...e.reservedPathPrefixes?{"routing.reservedPathPrefixes":e.reservedPathPrefixes}:{}},shared:{settings:[r],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:[n,i],tables:{toolkitContentLocks:y,toolkitPaths:p,toolkitPathPatterns:ie,toolkitRedirects:h},routes:[...v(),..._()],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()}}],init:async t=>{a(e);let n=0,r=0,i=0;for(let[e,a]of t.entities){if(a.behaviors?.some(e=>e.name===`versionable`)){let t=`${e}_versions`;if(!A.has(t)){let n=ue(a);A.register(t,n.table),s(e,{versions:n})}n++}let t=a.behaviors?.some(e=>e.name===`publishable`),o=a.allFields,c=Object.values(o).some(e=>e.translatable);if(t&&c){let t=`${e}_locale_status`;if(!A.has(t)){let n=le(a);A.register(t,n.table),s(e,{localeStatus:n})}r++}if(t){let t=`${e}_drafts`;if(!A.has(t)){let n=ce(a);A.register(t,n.table),s(e,{drafts:n})}i++}}z(e.locales.map(e=>e.code)),ee();try{await re(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:i},`Content plugin initialized`)},onReady:async e=>{try{await oe(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{R as n,de as t};
2
+ //# sourceMappingURL=plugin-BxOHoKPR.mjs.map