@murumets-ee/content 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.d.mts +32 -0
- package/dist/admin.d.mts.map +1 -0
- package/dist/admin.mjs +1 -0
- package/dist/chrome-provider.d.mts +107 -0
- package/dist/chrome-provider.d.mts.map +1 -0
- package/dist/chrome-provider.mjs +2 -0
- package/dist/chrome-provider.mjs.map +1 -0
- package/dist/client.d.mts +2 -2
- package/dist/client.mjs +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/content-config-store-8omQF5p5.mjs +2 -0
- package/dist/content-config-store-8omQF5p5.mjs.map +1 -0
- package/dist/entities/index.d.mts +37 -11
- package/dist/entities/index.d.mts.map +1 -1
- package/dist/entities/index.mjs +1 -1
- package/dist/entities/index.mjs.map +1 -1
- package/dist/{index-D8nN7BFd.d.mts → index-Bsmju0UF.d.mts} +30 -3
- package/dist/index-Bsmju0UF.d.mts.map +1 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/locale-utils-DFEk4CE9.mjs +2 -0
- package/dist/{locale-utils-CuIBKeaz.mjs.map → locale-utils-DFEk4CE9.mjs.map} +1 -1
- package/dist/lock-service.d.mts +1 -1
- package/dist/plugin-BSxbiGc1.mjs +2 -0
- package/dist/plugin-BSxbiGc1.mjs.map +1 -0
- package/dist/plugin.d.mts +17 -6
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/query-client.mjs +1 -1
- package/dist/resolve-context-BVk6uObB.mjs +2 -0
- package/dist/resolve-context-BVk6uObB.mjs.map +1 -0
- package/dist/resolve.d.mts +596 -0
- package/dist/resolve.d.mts.map +1 -0
- package/dist/resolve.mjs +9 -0
- package/dist/resolve.mjs.map +1 -0
- package/dist/routes-Bc9DcaPk.mjs +2 -0
- package/dist/routes-Bc9DcaPk.mjs.map +1 -0
- package/dist/{routing-strategy-TPSzrapD.d.mts → routing-strategy-D3Qowm3D.d.mts} +40 -6
- package/dist/routing-strategy-D3Qowm3D.d.mts.map +1 -0
- package/dist/routing-strategy-dkt2yO25.mjs +2 -0
- package/dist/routing-strategy-dkt2yO25.mjs.map +1 -0
- package/dist/schema-BJxAwPrI.mjs +2 -0
- package/dist/schema-BJxAwPrI.mjs.map +1 -0
- package/dist/{types-CUw2Pf0Y.d.mts → types-B3K2p9t8.d.mts} +22 -1
- package/dist/types-B3K2p9t8.d.mts.map +1 -0
- package/package.json +21 -7
- package/dist/index-D8nN7BFd.d.mts.map +0 -1
- package/dist/locale-utils-CuIBKeaz.mjs +0 -2
- package/dist/plugin-IW9SM9iO.mjs +0 -2
- package/dist/plugin-IW9SM9iO.mjs.map +0 -1
- package/dist/routing-strategy-DGx6Rq6F.mjs +0 -2
- package/dist/routing-strategy-DGx6Rq6F.mjs.map +0 -1
- package/dist/routing-strategy-TPSzrapD.d.mts.map +0 -1
- package/dist/types-CUw2Pf0Y.d.mts.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { m as VersionableOptions, o as LocaleEntry, r as I18nConfig } from "./types-
|
|
1
|
+
import { m as VersionableOptions, o as LocaleEntry, r as I18nConfig } from "./types-B3K2p9t8.mjs";
|
|
2
2
|
import { Behavior, DateField, Entity, JsonField, NumberField, SelectField, TextField } from "@murumets-ee/entity";
|
|
3
3
|
import { ToolkitApp } from "@murumets-ee/core";
|
|
4
|
+
import { MessageEntry } from "@murumets-ee/i18n";
|
|
4
5
|
|
|
5
6
|
//#region src/behaviors/versionable.d.ts
|
|
6
7
|
type VersionableFields = {
|
|
@@ -171,6 +172,24 @@ declare class DraftNotFoundError extends Error {
|
|
|
171
172
|
readonly locale: string;
|
|
172
173
|
constructor(entityId: string, locale: string);
|
|
173
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Thrown by `restoreVersion` when the requested version snapshot does not
|
|
177
|
+
* exist for the entity+locale — either the version number is out of range,
|
|
178
|
+
* or the entity row itself is gone (a deleted row has no versions).
|
|
179
|
+
*
|
|
180
|
+
* Distinct from generic `Error` so admin-ui's API handler can map it to
|
|
181
|
+
* 404 Not Found, matching the route-level 404 the publishable branch
|
|
182
|
+
* already returns for the same condition. Before this it fell through to
|
|
183
|
+
* the generic 500 branch on non-publishable entities — the same
|
|
184
|
+
* "caller-state condition audited as a server fault" defect as
|
|
185
|
+
* `EntityNotFoundError` (`plan/admin-api-hardening` F008).
|
|
186
|
+
*/
|
|
187
|
+
declare class VersionNotFoundError extends Error {
|
|
188
|
+
readonly entityId: string;
|
|
189
|
+
readonly version: number;
|
|
190
|
+
readonly locale: string;
|
|
191
|
+
constructor(entityId: string, version: number, locale: string);
|
|
192
|
+
}
|
|
174
193
|
/**
|
|
175
194
|
* Thrown when a locale string passed at runtime is not configured in the
|
|
176
195
|
* content plugin. The plugin validates `defaultLocale` at init time, but
|
|
@@ -204,5 +223,13 @@ declare function enabledLocaleCodes(entries: readonly LocaleEntry[]): string[];
|
|
|
204
223
|
*/
|
|
205
224
|
declare function resolveI18nConfig(app: ToolkitApp, entries?: readonly LocaleEntry[]): Promise<I18nConfig>;
|
|
206
225
|
//#endregion
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
//#region src/message-registry.d.ts
|
|
227
|
+
/**
|
|
228
|
+
* The DB `message` override rows for `locale`, as flat `(namespace, key, value)` entries.
|
|
229
|
+
* Empty when the `message` entity isn't registered (no `messageEditor()` / pre-migration)
|
|
230
|
+
* or on any read error.
|
|
231
|
+
*/
|
|
232
|
+
declare function resolveMessageOverrides(app: ToolkitApp, locale: string): Promise<MessageEntry[]>;
|
|
233
|
+
//#endregion
|
|
234
|
+
export { versionable as A, OverrideDirective as C, withOverrides as D, hasWithOverrides as E, VersionableFields as O, workflowable as S, WithOverridesFields as T, WORKFLOW_TRANSITIONS as _, DraftNotFoundError as a, WorkflowableFields as b, VersionNotFoundError as c, DiffSegment as d, FieldDiff as f, WORKFLOW_STATES as g, entitySupports as h, resolveI18nConfig as i, getVersionableOptions as k, BlockDiffEntry as l, EntityCapability as m, enabledLocaleCodes as n, EntityCapabilityError as o, PropDiff as p, resolveEffectiveLocales as r, InvalidLocaleError as s, resolveMessageOverrides as t, BlockDiffStatus as u, WorkflowState as v, OverridesMap as w, isAllowedTransition as x, WorkflowTransitionError as y };
|
|
235
|
+
//# sourceMappingURL=index-Bsmju0UF.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-Bsmju0UF.d.mts","names":[],"sources":["../src/behaviors/versionable.ts","../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":";;;;;;KAmBY,iBAAA;EACV,QAAA,EAAU,WAAA;IAAgB,QAAA;EAAA;EAC1B,YAAA,EAAc,SAAS;AAAA;AAAA,iBAQT,WAAA,CAAY,OAAA,GAAU,kBAAA,GAAqB,QAAA,CAAS,iBAAA;;;;iBAqDpD,qBAAA,CAAsB,SAAA,GAAY,QAAA,KAAa,QAAA,CAAS,kBAAA;;;AAAkB;;;;ACrC5B;ADqC4B,KC7BrF,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;;;;;iBAoKP,aAAA,CAAA,GAAiB,QAAQ,CAAC,mBAAA;AAtLN;AAAA;;;;AAAA,iBAkOpB,gBAAA,CACd,SAAA,EAAW,aAAa;EAAG,IAAA;AAAA,IAAiB,MAAA;;;cCjPjC,eAAA;AAAA,KACD,aAAA,WAAwB,eAAe;;;;;;;AFmCuC;cE1B7E,oBAAA;EAAA;;;;;;;;;iBAWG,mBAAA,CAAoB,IAAA,EAAM,aAAA,EAAe,EAAA,EAAI,aAAa;;;;;cAU7D,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;;UAIU,SAAA;EACf,KAAA;EACA,IAAA;EAWA,IAAA;EACA,EAAA;EJJA;EIMA,QAAA,GAAW,WAAA;EJNY;EIQvB,UAAA,GAAa,cAAc;AAAA;;KAIjB,eAAA;;UAGK,cAAA;EACf,OAAA;EACA,SAAA;EACA,MAAA,EAAQ,eAAA;EACR,SAAA;EACA,OAAA;EJZyD;EIczD,SAAA,GAAY,QAAQ;AAAA;AJd+D;AAAA,UIkBpE,QAAA;EACf,KAAA;EACA,IAAA;EACA,EAAA;EJgCsE;EI9BtE,QAAA,GAAW,WAAW;AAAA;;;;;;;;;;AJjCxB;;;;;cKNa,qBAAA,SAA8B,KAAK;EAAA,SACrC,UAAA;EAAA,SACA,UAAA;cAGP,UAAA,UACA,UAAA,EAAY,qBAAA,gBACZ,OAAA;AAAA;ALCqB;AAQzB;;;;;;;;AARyB,cKqBZ,kBAAA,SAA2B,KAAK;EAAA,SAClC,QAAA;EAAA,SACA,MAAA;cAEG,QAAA,UAAkB,MAAA;AAAA;ALjBqD;AAqDrF;;;;;;;;;;;AArDqF,cK2CxE,oBAAA,SAA6B,KAAK;EAAA,SACpC,QAAA;EAAA,SACA,OAAA;EAAA,SACA,MAAA;cAEG,QAAA,UAAkB,OAAA,UAAiB,MAAA;AAAA;AJhCa;;;;;;;;;AAAA,cIkDjD,kBAAA,SAA2B,KAAK;EAAA,SAClC,MAAA;EAAA,SACA,OAAA;cAEG,MAAA,UAAgB,OAAA;AAAA;;;ALtE9B;;;;;AAAA,iBM0DsB,uBAAA,CAAwB,GAAA,EAAK,UAAA,GAAa,OAAA,CAAQ,WAAA;;iBAoBxD,kBAAA,CAAmB,OAA+B,WAAb,WAAW;;;;;;AN9EqB;AAqDrF;iBMoCsB,iBAAA,CACpB,GAAA,EAAK,UAAA,EACL,OAAA,YAAmB,WAAA,KAClB,OAAA,CAAQ,UAAA;;;AN5FX;;;;;AAAA,iBOkBsB,uBAAA,CACpB,GAAA,EAAK,UAAA,EACL,MAAA,WACC,OAAA,CAAQ,YAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as LocaleDirection, c as LockInfo, d as TranslationReport, f as TranslationStatus, i as LocaleConfig, l as RoutingStrategy, m as VersionableOptions, n as DraftEntry, o as LocaleEntry, p as VersionEntry, r as I18nConfig, t as ContentConfig, u as TranslationFieldStatus } from "./types-
|
|
2
|
-
import { C as
|
|
3
|
-
import { a as
|
|
4
|
-
export { type BlockDiffEntry, type BlockDiffStatus, type ContentConfig, type DiffSegment, type DraftEntry, DraftNotFoundError, type EntityCapability, EntityCapabilityError, type FieldDiff, type I18nConfig, InvalidLocaleError, Locale, type LocaleConfig, type LocaleDirection, type LocaleEntry, type LockInfo, type OverrideDirective, type OverridesMap, type PropDiff, ROUTING_STRATEGIES, type RoutingStrategy, type TranslationFieldStatus, type TranslationReport, type TranslationStatus, type VersionEntry, type VersionableOptions, WORKFLOW_STATES, WORKFLOW_TRANSITIONS, type WithOverridesFields, type WorkflowState, WorkflowTransitionError, type WorkflowableFields, enabledLocaleCodes, entitySupports, getVersionableOptions, hasWithOverrides, i18nSettings, isAllowedTransition, localePrefixForStrategy, prefixDefaultLocaleForStrategy, resolveEffectiveLocales, resolveI18nConfig, versionable, withOverrides, workflowable };
|
|
1
|
+
import { a as LocaleDirection, c as LockInfo, d as TranslationReport, f as TranslationStatus, i as LocaleConfig, l as RoutingStrategy, m as VersionableOptions, n as DraftEntry, o as LocaleEntry, p as VersionEntry, r as I18nConfig, t as ContentConfig, u as TranslationFieldStatus } from "./types-B3K2p9t8.mjs";
|
|
2
|
+
import { A as versionable, C as OverrideDirective, D as withOverrides, E as hasWithOverrides, S as workflowable, T as WithOverridesFields, _ as WORKFLOW_TRANSITIONS, a as DraftNotFoundError, b as WorkflowableFields, c as VersionNotFoundError, d as DiffSegment, f as FieldDiff, g as WORKFLOW_STATES, h as entitySupports, i as resolveI18nConfig, k as getVersionableOptions, l as BlockDiffEntry, m as EntityCapability, n as enabledLocaleCodes, o as EntityCapabilityError, p as PropDiff, r as resolveEffectiveLocales, s as InvalidLocaleError, t as resolveMessageOverrides, u as BlockDiffStatus, v as WorkflowState, w as OverridesMap, x as isAllowedTransition, y as WorkflowTransitionError } from "./index-Bsmju0UF.mjs";
|
|
3
|
+
import { a as ROUTING_STRATEGIES, i as Locale, n as prefixDefaultLocaleForStrategy, o as i18nSettings, r as Message, t as localePrefixForStrategy } from "./routing-strategy-D3Qowm3D.mjs";
|
|
4
|
+
export { type BlockDiffEntry, type BlockDiffStatus, type ContentConfig, type DiffSegment, type DraftEntry, DraftNotFoundError, type EntityCapability, EntityCapabilityError, type FieldDiff, type I18nConfig, InvalidLocaleError, Locale, type LocaleConfig, type LocaleDirection, type LocaleEntry, type LockInfo, Message, type OverrideDirective, type OverridesMap, type PropDiff, ROUTING_STRATEGIES, type RoutingStrategy, type TranslationFieldStatus, type TranslationReport, type TranslationStatus, type VersionEntry, VersionNotFoundError, type VersionableOptions, WORKFLOW_STATES, WORKFLOW_TRANSITIONS, type WithOverridesFields, type WorkflowState, WorkflowTransitionError, type WorkflowableFields, enabledLocaleCodes, entitySupports, getVersionableOptions, hasWithOverrides, i18nSettings, isAllowedTransition, localePrefixForStrategy, prefixDefaultLocaleForStrategy, resolveEffectiveLocales, resolveI18nConfig, resolveMessageOverrides, versionable, withOverrides, workflowable };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{n as e,t}from"./versionable-Br_G5VGR.mjs";import{a as n,i as r,n as i,r as a,t as o}from"./workflowable-BufrZXu9.mjs";import{a as s,c,l,
|
|
1
|
+
import{n as e,t}from"./versionable-Br_G5VGR.mjs";import{a as n,i as r,n as i,r as a,t as o}from"./workflowable-BufrZXu9.mjs";import{a as s,c,i as l,l as u,n as d,o as f,r as p,s as m,t as h,u as g}from"./routing-strategy-dkt2yO25.mjs";import{t as _}from"./content-config-store-8omQF5p5.mjs";import{field as v}from"@murumets-ee/entity";import{createSystemReachResolver as y,mergeConfigOverrides as b}from"@murumets-ee/core";import{QueryClient as x}from"@murumets-ee/entity/query";import{schemaRegistry as S}from"@murumets-ee/db";import{eq as C}from"drizzle-orm";const w=new Set([`hide`,`patch`,`replace`]),T=Symbol.for(`@murumets-ee/content:withOverrides`),E=new Set([`__proto__`,`constructor`,`prototype`]);function D(e,t,n=0){if(e!=null&&!(typeof e!=`object`||Array.isArray(e)))for(let r of Object.keys(e)){if(E.has(r))throw Error(`withOverrides: reserved key '${r}' at ${t} is not allowed.`);n<3&&D(e[r],`${t}.${r}`,n+1)}}function O(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(E.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`||!w.has(i))throw Error(`withOverrides: _overrides['${e}'].kind must be one of [hide, patch, replace]; got ${String(i)}.`);if(i===`patch`){let t=n;k(t.props,`_overrides['${e}'].props`),k(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'.`)}D(n,`_overrides['${e}']`),r[e]=n}return r}function k(e,t){if(e!==void 0&&(typeof e!=`object`||!e||Array.isArray(e)))throw Error(`withOverrides: ${t} must be an object.`)}function A(){return{name:`withOverrides`,fields:{_overrides:v.json({default:()=>({}),internal:!0})},hooks:{beforeCreate:async e=>(e._overrides===void 0?e._overrides={}:e._overrides=O(e._overrides),e),beforeUpdate:async(e,t)=>(t._overrides===void 0||(t._overrides=O(t._overrides)),t)},[T]:!0}}function j(e){return e?e.some(e=>!!e?.[T]):!1}function M(e,t){switch(t){case`drafts`:return N(e,`publishable`);case`versions`:return N(e,`versionable`);case`localeStatus`:return N(e,`publishable`)&&P(e);case`translations`:return P(e)}}function N(e,t){return e.behaviors?.some(e=>e.name===t)??!1}function P(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 F=y(`locale-registry`);function I(){return _().locales.map((e,t)=>({code:e.code,label:e.label,enabled:!0,direction:`ltr`,sortOrder:t}))}function L(e,t){return e.sortOrder-t.sortOrder||e.code.localeCompare(t.code)}async function R(e){return e.entities?.has(`locale`)?(await new x({entity:l,db:e.db.readOnly,logger:e.logger.child({registry:`locale`}),contextResolver:F}).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 z(e){let t;try{t=await R(e)}catch(t){return e.logger.warn({err:t},`locale registry read failed — falling back to content() defaults`),[...I()].sort(L)}return b({defaults:I(),overrides:t,idOfDefault:e=>e.code,idOfOverride:e=>e.code}).sort(L)}function B(e){return e.filter(e=>e.enabled).map(e=>e.code)}async function V(e,t){let n=B(t??await z(e)),r=_().defaultLocale,i={};try{let{createSettingsClient:t}=await import(`@murumets-ee/settings/runtime`);i=await t(f,{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,o=n.includes(a)?a:n.includes(r)?r:n[0]??r,c=i.routingStrategy;return{defaultLocale:o,routingStrategy:s.includes(typeof c==`string`?c:``)?c:`no-prefix-default`}}const H=y(`message-registry`),U=2e4;async function W(e,t){if(!e.entities?.has(`message`))return[];let n=S.get(`message`);if(!n)return[];let r=new x({entity:p,db:e.db.readOnly,logger:e.logger.child({registry:`message`}),contextResolver:H});try{let i=await r.findMany({where:C(n.locale,t),limit:U});return i.length>=U&&e.logger.warn({locale:t,cap:U},`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{m as DraftNotFoundError,c as EntityCapabilityError,u as InvalidLocaleError,l as Locale,p as Message,s as ROUTING_STRATEGIES,g as VersionNotFoundError,o as WORKFLOW_STATES,i as WORKFLOW_TRANSITIONS,a as WorkflowTransitionError,B as enabledLocaleCodes,M as entitySupports,t as getVersionableOptions,j as hasWithOverrides,f as i18nSettings,r as isAllowedTransition,h as localePrefixForStrategy,d as prefixDefaultLocaleForStrategy,z as resolveEffectiveLocales,V as resolveI18nConfig,W as resolveMessageOverrides,e as versionable,A as withOverrides,n as workflowable};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/behaviors/with-overrides.ts","../src/capabilities.ts","../src/locale-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 * (`composeMultiScopeDocument` in `@murumets-ee/blocks/server`) reads this\n * field and calls `applyOverrides()` AFTER the multi-scope tree has been\n * assembled.\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 `composeMultiScopeDocument`'s\n // `readOverridesMap` 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: pages overriding more than ~50 scoped blocks is a\n * smell (consider a `route_layout` instead).\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 * 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 { 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/** Elevated resolver for the app-wide registry read (the roles-read analog). */\nconst SYSTEM_CONTEXT = () => ({\n user: { id: 'system', groups: ['admin'] },\n checker: () => true,\n})\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"],"mappings":"4XAsFA,MAAM,EAAqC,IAAI,IAAI,CAAC,OAAQ,QAAS,SAAS,CAAC,EAWzE,EAAwB,OAAO,IAAI,oCAAoC,EAMvE,EAAiB,IAAI,IAAI,CAAC,YAAa,cAAe,WAAW,CAAC,EA4BxE,SAAS,EAAsB,EAAgB,EAAc,EAAQ,EAAS,CACxE,MAAU,MACV,SAAO,GAAU,UAAY,MAAM,QAAQ,CAAK,GACpD,IAAK,IAAM,KAAO,OAAO,KAAK,CAAgC,EAAG,CAC/D,GAAI,EAAe,IAAI,CAAG,EACxB,MAAU,MAAM,gCAAgC,EAAI,OAAO,EAAK,iBAAiB,EAE/E,EAAQ,GACV,EAAuB,EAAkC,GAAM,GAAG,EAAK,GAAG,IAAO,EAAQ,CAAC,CAE9F,CACF,CAeA,SAAS,EAAqB,EAA8B,CAC1D,GAAI,GAAU,KAA6B,MAAO,CAAC,EACnD,GAAI,OAAO,GAAU,UAAY,MAAM,QAAQ,CAAK,EAClD,MAAU,MAAM,uDAAuD,EAEzE,IAAM,EAAM,EACN,EAAO,OAAO,KAAK,CAAG,EAC5B,GAAI,EAAK,OAAS,IAChB,MAAU,MACR,iCAAiC,EAAK,OAAO,sBAC/C,EAEF,IAAM,EAA0B,CAAC,EACjC,IAAK,IAAM,KAAO,EAAM,CAGtB,GAAI,EAAe,IAAI,CAAG,EACxB,MAAU,MAAM,gCAAgC,EAAI,kBAAkB,EAExE,IAAM,EAAY,EAAI,GACtB,GAGE,OAAO,GAAc,WAFrB,GAGA,MAAM,QAAQ,CAAS,EAEvB,MAAU,MAAM,8BAA8B,EAAI,+BAA+B,EAEnF,IAAM,EAAQ,EAAiC,KAC/C,GAAI,OAAO,GAAS,UAAY,CAAC,EAAc,IAAI,CAAI,EACrD,MAAU,MACR,8BAA8B,EAAI,qDAAqD,OACrF,CACF,EAAE,EACJ,EAOF,GAAI,IAAS,QAAS,CACpB,IAAM,EAAI,EACV,EAA6B,EAAE,MAAO,eAAe,EAAI,SAAS,EAClE,EAA6B,EAAE,OAAQ,eAAe,EAAI,UAAU,CACtE,CACA,GAAI,IAAS,UAAW,CACtB,IAAM,EAAS,EAAkC,MACjD,GAGE,OAAO,GAAU,WAFjB,GAGA,MAAM,QAAQ,CAAK,GACnB,OAAQ,EAA2B,IAAO,SAE1C,MAAU,MACR,8BAA8B,EAAI,sEACpC,CAEJ,CAIA,EAAsB,EAAW,eAAe,EAAI,GAAG,EACvD,EAAU,GAAO,CACnB,CACA,OAAO,CACT,CAOA,SAAS,EAA6B,EAAgB,EAAqB,CACrE,OAAU,IAAA,KACQ,OAAO,GAAU,WAAnC,GAA+C,MAAM,QAAQ,CAAK,GACpE,MAAU,MAAM,kBAAkB,EAAM,oBAAoB,CAEhE,CAMA,SAAgB,GAAgE,CAoC9E,MAAO,CAlCL,KAAM,gBACN,OAAQ,CAMN,WAAY,EAAM,KAAK,CACrB,aAAgB,CAAC,GACjB,SAAU,EACZ,CAAC,CACH,EACA,MAAO,CACL,aAAc,KAAO,KAKf,EAAK,aAAe,IAAA,GACtB,EAAK,WAAa,CAAC,EAEnB,EAAK,WAAa,EAAqB,EAAK,UAAU,EAEjD,GAET,aAAc,MAAO,EAAK,KAEpB,EAAK,aAAe,IAAA,KACxB,EAAK,WAAa,EAAqB,EAAK,UAAU,GADZ,EAI9C,GACC,GAAwB,EAEb,CAChB,CAOA,SAAgB,EACd,EACS,CAET,OADK,EACE,EAAU,KAAM,GAAM,EAAQ,IAAI,EAAuB,EADzC,EAEzB,CCtQA,SAAgB,EAAe,EAAgB,EAAuC,CACpF,OAAQ,EAAR,CACE,IAAK,SACH,OAAO,EAAY,EAAQ,aAAa,EAC1C,IAAK,WACH,OAAO,EAAY,EAAQ,aAAa,EAC1C,IAAK,eACH,OAAO,EAAY,EAAQ,aAAa,GAAK,EAAqB,CAAM,EAC1E,IAAK,eACH,OAAO,EAAqB,CAAM,CACtC,CACF,CAEA,SAAS,EAAY,EAAgB,EAAuB,CAC1D,OAAO,EAAO,WAAW,KAAM,GAAM,EAAE,OAAS,CAAI,GAAK,EAC3D,CAEA,SAAS,EAAqB,EAAyB,CACrD,IAAM,EAAS,EAAO,UACtB,IAAK,IAAM,KAAO,OAAO,KAAK,CAAM,EAAG,CACrC,IAAM,EAAI,EAAO,GACjB,GAAI,GAAM,EAAiC,eAAiB,GAAM,MAAO,EAC3E,CACA,MAAO,EACT,CC5BA,MAAM,OAAwB,CAC5B,KAAM,CAAE,GAAI,SAAU,OAAQ,CAAC,OAAO,CAAE,EACxC,YAAe,EACjB,GAGA,SAAS,GAAiC,CACxC,OAAO,EAAiB,CAAC,CAAC,QAAQ,KAAK,EAAG,KAAO,CAC/C,KAAM,EAAE,KACR,MAAO,EAAE,MACT,QAAS,GACT,UAAW,MACX,UAAW,CACb,EAAE,CACJ,CAGA,SAAS,EAAY,EAAgB,EAAwB,CAC3D,OAAO,EAAE,UAAY,EAAE,WAAa,EAAE,KAAK,cAAc,EAAE,IAAI,CACjE,CAUA,eAAe,EAAe,EAAyC,CASrE,OARK,EAAI,UAAU,IAAI,QAAQ,GAQxB,MADY,IANA,EAAY,CAC7B,OAAQ,EACR,GAAI,EAAI,GAAG,SACX,OAAQ,EAAI,OAAO,MAAM,CAAE,SAAU,QAAS,CAAC,EAC/C,gBAAiB,CACnB,CACwB,CAAC,CAAC,SAAS,CAAE,MAAO,GAAgB,CAAC,EAAA,CACjD,IAAK,GAAM,CACrB,IAAM,EAAM,EACZ,MAAO,CACL,KAAM,OAAO,EAAI,IAAI,EACrB,MAAO,OAAO,EAAI,OAAU,UAAY,EAAI,MAAM,OAAS,EAAI,EAAI,MAAQ,OAAO,EAAI,IAAI,EAC1F,QAAS,EAAI,UAAY,GACzB,UAAW,EAAI,YAAc,MAAQ,MAAQ,MAC7C,UAAW,OAAO,EAAI,WAAc,SAAW,EAAI,UAAY,CACjE,CACF,CAAC,EAjBwC,CAAC,CAkB5C,CAOA,eAAsB,EAAwB,EAAyC,CACrF,IAAI,EACJ,GAAI,CACF,EAAO,MAAM,EAAe,CAAG,CACjC,OAAS,EAAK,CAEZ,OADA,EAAI,OAAO,KAAK,CAAE,KAAI,EAAG,kEAAkE,EACpF,CAAC,GAAG,EAAgB,CAAC,CAAC,CAAC,KAAK,CAAW,CAChD,CASA,OARe,EAA+C,CAC5D,SAAU,EAAgB,EAC1B,UAAW,EACX,YAAc,GAAM,EAAE,KACtB,aAAe,GAAM,EAAE,IAGzB,CACa,CAAC,CAAmB,KAAK,CAAW,CACnD,CAGA,SAAgB,EAAmB,EAA2C,CAC5E,OAAO,EAAQ,OAAQ,GAAM,EAAE,OAAO,CAAC,CAAC,IAAK,GAAM,EAAE,IAAI,CAC3D,CASA,eAAsB,EACpB,EACA,EACqB,CAErB,IAAM,EAAU,EADE,GAAY,MAAM,EAAwB,CAAG,CACnB,EACtC,EAAiB,EAAiB,CAAC,CAAC,cAEtC,EAAkC,CAAC,EACvC,GAAI,CAMF,GAAM,CAAE,wBAAyB,MAAM,OAAO,iCAE9C,EAAU,MADK,EAAqB,EAAc,CAAE,KAAI,CACnC,CAAC,CAAC,OAAO,GAAM,CAAC,CACvC,OAAS,EAAK,CACZ,EAAI,OAAO,KAAK,CAAE,KAAI,EAAG,gEAAgE,CAC3F,CAEA,IAAM,EACJ,OAAO,EAAO,eAAkB,UAAY,EAAO,cAAc,OAAS,EACtE,EAAO,cACP,EAEA,EAAgB,EAAQ,SAAS,CAAiB,EACpD,EACA,EAAQ,SAAS,CAAc,EAC7B,EACC,EAAQ,IAAM,EAEf,EAAc,EAAO,gBAO3B,MAAO,CAAE,gBAAe,gBANkB,EAAyC,SACjF,OAAO,GAAgB,SAAW,EAAc,EAClD,EACK,EACD,mBAEoC,CAC1C"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["SYSTEM_CONTEXT"],"sources":["../src/behaviors/with-overrides.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 * (`composeMultiScopeDocument` in `@murumets-ee/blocks/server`) reads this\n * field and calls `applyOverrides()` AFTER the multi-scope tree has been\n * assembled.\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 `composeMultiScopeDocument`'s\n // `readOverridesMap` 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: pages overriding more than ~50 scoped blocks is a\n * smell (consider a `route_layout` instead).\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 * 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{l as e}from"./routing-strategy-dkt2yO25.mjs";import{t}from"./content-config-store-8omQF5p5.mjs";import"./plugin-BSxbiGc1.mjs";import{getContext as n}from"@murumets-ee/core";function r(r){if(r===`_`)return;let i=n()?.availableLocales??t().locales.map(e=>e.code);for(let e of i)if(e===r)return;throw new e(r,[...i])}export{r as t};
|
|
2
|
+
//# sourceMappingURL=locale-utils-DFEk4CE9.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locale-utils-
|
|
1
|
+
{"version":3,"file":"locale-utils-DFEk4CE9.mjs","names":[],"sources":["../src/locale-utils.ts"],"sourcesContent":["/**\n * Locale validation helpers.\n *\n * Every ContentClient and LockService method that accepts a `locale`\n * parameter must run it through `validateLocale()` before passing it to\n * the database. Drizzle parameterizes queries so SQL injection isn't a\n * concern, but unvalidated locale strings:\n * - bypass the configured locale set (typo'd `'frr'` writes a row that\n * never resolves on read),\n * - silently truncate at the column's `varchar(10)` boundary (e.g.\n * `'pt-BR-traditional'` → `'pt-BR-trad'`, breaking later equality),\n * - pollute drafts/versions/locale_status tables with rows that no\n * UI ever queries.\n *\n * The `'_'` sentinel is allowed because it's the conventional value for\n * default-locale rows in versions/drafts (where every row needs a\n * non-null `locale` for unique-constraint composition).\n */\n\nimport { getContext } from '@murumets-ee/core'\nimport { InvalidLocaleError } from './errors.js'\nimport { getContentConfig } from './plugin.js'\n\n/**\n * The internal sentinel for \"default locale\" used in version/draft storage.\n * `@murumets-ee/entity/admin` exports the same constant (where\n * `updateForLocale` defines the cross-package contract); the `satisfies`\n * pins the two literals together at compile time — drift on either side is\n * a type error — without a runtime import (which would force every unit\n * test that mocks `entity/admin` to spread `importOriginal`).\n */\nexport const DEFAULT_LOCALE_SENTINEL = '_' satisfies typeof import(\n '@murumets-ee/entity/admin'\n)['DEFAULT_LOCALE_SENTINEL']\n\n/**\n * Throw `InvalidLocaleError` if `locale` is neither the `'_'` sentinel nor one of\n * the **effective enabled** content-locale codes for the current request.\n *\n * The allowed set is the request-scoped {@link RequestContext.availableLocales}\n * (plan/config phase 05 §7, SD006) — the read-time-merge of `content()` shipped\n * defaults ⊕ DB `locale` override rows, resolved once per request at context\n * establishment. So a locale added/enabled at runtime validates with **no rebuild**,\n * and a disabled one is rejected — without a globalThis cache (multi-process /\n * multi-tenant correct).\n *\n * **Fallback:** outside a request context (build-time, CLI, unit tests, or the rare\n * concurrent-render ALS-scope escape) `availableLocales` is absent, so the check\n * falls back to the static `content()` codes — same behavior as before SD006, and\n * still deny-by-default (an unknown locale rejects either way).\n */\nexport function validateLocale(locale: string): void {\n if (locale === DEFAULT_LOCALE_SENTINEL) return\n const allowed = getContext()?.availableLocales ?? getContentConfig().locales.map((l) => l.code)\n for (const code of allowed) {\n if (code === locale) return\n }\n throw new InvalidLocaleError(locale, [...allowed])\n}\n"],"mappings":"oLAmDA,SAAgB,EAAe,EAAsB,CACnD,GAAI,IAAA,IAAoC,OACxC,IAAM,EAAU,EAAW,CAAC,EAAE,kBAAoB,EAAiB,CAAC,CAAC,QAAQ,IAAK,GAAM,EAAE,IAAI,EAC9F,IAAK,IAAM,KAAQ,EACjB,GAAI,IAAS,EAAQ,OAEvB,MAAM,IAAI,EAAmB,EAAQ,CAAC,GAAG,CAAO,CAAC,CACnD"}
|
package/dist/lock-service.d.mts
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{i as e,l as t,o as n,r}from"./routing-strategy-dkt2yO25.mjs";import{n as i,t as a}from"./content-config-store-8omQF5p5.mjs";import{d as o,f as s,g as c,l,m as u,t as d,u as f}from"./schema-BJxAwPrI.mjs";import{n as p,t as m}from"./routes-Bc9DcaPk.mjs";import{n as h}from"./locks-5779ugSW.mjs";import{MAX_PATH_DEPTH as g,hasPathParent as _,resolveEffectiveSlugMode as v,resolvePattern as y}from"@murumets-ee/entity";import{definePlugin as b,getContext as x,runAsCli as S}from"@murumets-ee/core";import{column as C,defineTable as w,schemaRegistry as T}from"@murumets-ee/db";import{asc as E,eq as D,inArray as O,sql as k}from"drizzle-orm";import{z as A}from"zod";import{createHash as j}from"node:crypto";const M=Symbol.for(`@murumets-ee/content:content-tables-registry`);function N(){let e=globalThis,t=e[M];return t||(t=new Map,e[M]=t),t}function P(e,t){let n=N(),r=n.get(e)??{};n.set(e,{...r,...t})}function F(e){return N().get(e)??{}}const I=Symbol.for(`@murumets-ee/content:locale-status-writer`);async function L(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 R(e){globalThis[I]=(n,r)=>{let{localeStatus:i}=F(n.name);if(i)return async(n,a,o)=>{let s=x()?.availableLocales??e;if(!s.includes(a))throw new t(a,[...s]);await L(i.makeClient(r),n,a,o)}}}function z(){return{async writeDraft({documentType:e,documentId:t,locale:n,dataPatch:r,user:i,tx:a}){let{drafts:o}=F(e);if(!o)return!1;let s=o.makeClient(a),c={...(await s.findOne({entityId:t,locale:n}))?.data??{},...r},l=new Date;return await s.upsert({entityId:t,locale:n,data:c,createdBy:i.id,createdByName:i.name??null,createdAt:l,updatedAt:l},{target:[`entityId`,`locale`],set:{data:c,updatedAt:l}}),!0},async readDraft({documentType:e,documentId:t,locale:n,db:r}){let{drafts:i}=F(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 hasAnyDraft({documentType:e,documentId:t,db:n}){let{drafts:r}=F(e);return r?r.makeClient(n).exists({entityId:t}):!1},async hasAnyLocaleActivity({documentType:e,documentId:t,db:n}){let{localeStatus:r}=F(e);if(r&&await r.makeClient(n).exists({entityId:t}))return!0;let i=T.get(`${e}_translations`);if(i){let e=i.entityId;if(e!==void 0&&(await n.select({one:k`1`}).from(i).where(D(e,t)).limit(1)).length>0)return!0}return!1}}}const B=Symbol.for(`@murumets-ee/content:path-writers-factory`),V=Symbol.for(`@murumets-ee/content:cascade-trigger-factory`);function H(e){globalThis[V]=e}function U(){let e=globalThis[V];return typeof e==`function`?e:void 0}function W(e,t){let n=f.makeClient(e.db.readWrite);return async r=>{if(r.action===`delete`){await n.deleteMany({entityType:r.entityName,entityId:r.id});return}let i=a(),s=i.defaultLocale,c=i.reservedPathPrefixes??[],l=r.locale?[r.locale]:i.locales.map(e=>e.code),d=e.getClient(t),f=async n=>{if(!_(t))return;let i=U();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 l){let a=i===r.locale?r.row??null:await d.findById(r.id,{locale:i,defaultLocale:s});if(!a)continue;let l=await o({entity:t,row:a,locale:i,defaultLocale:s,pathsClient:n});(await u({pathsClient:n,entityType:r.entityName,entityId:r.id,locale:i,path:l,reservedPathPrefixes:c,logger:e.logger})).outcome===`renamed`&&await f(i)}}}function G(e,t){let n=f.makeClient(e.db.readWrite);return async r=>{let i=a(),o=i.defaultLocale,c=i.reservedPathPrefixes??[],l=r.locale??o,u=null;r.id!==void 0&&(u=await e.getClient(t).findById(r.id,{locale:l,defaultLocale:o}));let d=v(r.data.slugMode,u?.slugMode);await s({entity:t,data:r.data,currentRow:u,id:r.id,locale:l,defaultLocale:o,slugMode:d,reservedPathPrefixes:c,pathsClient:n})}}function K(){globalThis[B]=(e,t)=>({pathIndexWriter:W(e,t),pathConflictGuard:G(e,t)})}const q=A.object({entityType:A.string().min(1),parentId:A.string().uuid(),locale:A.string().min(1)}),J=A.object({entity:A.string().min(1).optional(),locale:A.string().min(1).optional(),dryRun:A.boolean()});async function Y(e,t,n={}){let{entityType:r,parentId:i,locale:s}=t,l=n.maxItems??2e3,d=n.concurrency??5,p={processed:0,regenerated:0,unchanged:0,skipped:0,failed:0,truncated:!1},m=e=>({phase:e,...p}),h=()=>({entityType:r,parentId:i,locale:s,cap:l,...p}),g=e.entities.get(r);if(!g)return e.logger.warn({entityType:r,parentId:i,locale:s},`paths-cascade: unknown entity type — nothing to regenerate`),n.onProgress?.(m(`done`)),h();if(!_(g))return e.logger.warn({entityType:r,parentId:i,locale:s},`paths-cascade: entity is not a pathParent() hierarchy — nothing to regenerate`),n.onProgress?.(m(`done`)),h();let v=a(),y=v.defaultLocale,b=v.reservedPathPrefixes??[],x=f.makeClient(e.db.readWrite),S=e.getClient(g),C=S.getTable().parent,w=async t=>{let n=t.id;try{switch((await u({pathsClient:x,entityType:r,entityId:n,locale:s,path:await o({entity:g,row:t,locale:s,defaultLocale:y,pathsClient:x}),reservedPathPrefixes:b,logger:e.logger})).outcome){case`created`:case`renamed`:p.regenerated+=1;break;case`unchanged`:p.unchanged+=1;break;default:p.skipped+=1;break}}catch(t){p.failed+=1,e.logger.warn({err:t,entityType:r,id:n,locale:s},`paths-cascade: failed to regenerate one descendant path — continuing (per-item isolation)`)}},T=[i];for(;T.length>0;){let e=l-p.processed;if(e<=0){p.truncated=!0;break}let t=await S.findMany({where:O(C,T),locale:s,defaultLocale:y,limit:e+1});if(t.length===0)break;let r=t;if(t.length>e&&(r=t.slice(0,e),p.truncated=!0),await c(r,d,w),p.processed+=r.length,n.onProgress?.(m(`walking`)),p.truncated)break;T=r.map(e=>e.id)}return p.truncated&&e.logger.warn({entityType:r,parentId:i,locale:s,processed:p.processed,cap:l},`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?.(m(`done`)),h()}async function X(e,t,n={}){let r=n.pageSize??200,i=n.concurrency??5,s=n.sampleCap??25,l=t.dryRun,d=a(),p=d.defaultLocale,m=d.reservedPathPrefixes??[],h=d.locales.map(e=>e.code),v=t.locale?h.filter(e=>e===t.locale):h,y=[...e.entities.values()].filter(e=>e.frontend?.path!==void 0&&(t.entity===void 0||e.name===t.entity)),b=t.entity!==void 0&&y.length===0,x=t.locale!==void 0&&v.length===0;b&&e.logger.warn({entity:t.entity,dryRun:l},`paths-rebuild: --entity matches no routable entity (one declaring frontend.path) — nothing to rebuild (likely a typo in the entity name)`),x&&e.logger.warn({locale:t.locale,configuredLocales:h,dryRun:l},`paths-rebuild: --locale matches no configured locale — nothing to rebuild (likely a typo in the locale code)`);let S=b||x?{entity:b,locale:x}:void 0,C=y.some(e=>_(e)),w=l&&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=f.makeClient(e.db.readWrite),D={scanned:0,changed:0,applied:0,failed:0},O=new Map,k={missing:0,conflicts:0,unchanged:0,skipped:0},A=(e,t)=>{let n=O.get(e);n!==t&&(n!==void 0&&n!==`resolved`&&--k[n],O.set(e,t),t!==`resolved`&&(k[t]+=1))},j=[],M=!1,N=e=>{j.length<s?j.push(e):M=!0},P=0,F=e=>({phase:e,dryRun:l,scope:{entity:t.entity??null,locale:t.locale??null},scanned:D.scanned,changed:D.changed,applied:D.applied,failed:D.failed,missing:k.missing,conflicts:k.conflicts,unchanged:k.unchanged,skipped:k.skipped,passes:P,sample:j,sampleTruncated:M,...w===void 0?{}:{hierarchyNote:w},...S===void 0?{}:{unmatchedScope:S}}),I=async(t,n,r,i)=>{let a=r.id,s=!1;try{let c=await u({pathsClient:T,entityType:n,entityId:a,locale:i,path:await o({entity:t,row:r,locale:i,defaultLocale:p,pathsClient:T}),reservedPathPrefixes:m,logger:e.logger,dryRun:l}),d=`${n}:${a}:${i}`;switch(c.outcome){case`unchanged`:A(d,`unchanged`);break;case`skipped-null`:case`skipped-reserved`:A(d,`skipped`);break;case`conflict`:A(d,`conflicts`),N({kind:`conflict`,entityType:n,entityId:a,locale:i,oldPath:c.oldPath,newPath:c.newPath,...c.conflictWith?{conflictWith:c.conflictWith.entityId}:{}});break;case`created`:l?A(d,`missing`):(D.applied+=1,s=!0,A(d,`resolved`)),N({kind:`missing`,entityType:n,entityId:a,locale:i,oldPath:null,newPath:c.newPath});break;case`renamed`:D.changed+=1,l||(D.applied+=1,s=!0),A(d,`resolved`),N({kind:`changed`,entityType:n,entityId:a,locale:i,oldPath:c.oldPath,newPath:c.newPath});break}}catch(t){D.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 s},L=async t=>{let a=0;for(let o of t){let t=e.getClient(o),s=o.name,l=t.getTable().id;for(let e of v){let u;for(;;){let d=await t.findMany({locale:e,defaultLocale:p,limit:r,orderBy:E(l),...u===void 0?{}:{cursor:{field:`id`,value:u,direction:`asc`}}});if(d.length===0)break;await c(d,i,async t=>{await I(o,s,t,e)&&(a+=1)}),D.scanned+=d.length;let f=d[d.length-1];if(f&&(u=f.id),n.onProgress?.(F(`scanning`)),d.length<r)break}}}return a},R=async t=>{let r=[];for(let[n,i]of O){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 c(r,i,async t=>{let n=await e.getClient(t.entity).findById(t.entityId,{locale:t.locale,defaultLocale:p});n!==null&&(D.scanned+=1,await I(t.entity,t.entityType,n,t.locale)&&(a+=1))}),n.onProgress?.(F(`scanning`)),a},z=y.filter(e=>_(e)),B=new Set(z.map(e=>e.name)),V=l||!C?1:g;for(let e=0;e<V;e++){P=e+1;let t=await L(e===0?y:z);if(e>0&&(t+=await R(B)),t===0)break}M&&e.logger.warn({scanned:D.scanned,changed:D.changed,missing:k.missing,conflicts:k.conflicts,sampleCap:s},`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 Z(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:q}),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:J});n(i,async t=>{await S(async()=>{let n=await Y(e,q.parse(t.payload),{onProgress:e=>t.updateProgress(e)});e.logger.info(n,`paths-cascade: descendant regeneration complete`)})}),n(a,async t=>{await S(async()=>{let n=await X(e,J.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`)})}),H(async(e,t,n,a)=>{await new r({app:e,logger:e.logger}).enqueue(i,{entityType:t.name,parentId:n,locale:a})})}const Q=w({name:`toolkit_path_patterns`,columns:{id:C.uuid({primaryKey:!0,defaultRandom:!0}),entityType:C.varchar({length:100,notNull:!0,pgName:`entity_type`}),locale:C.varchar({length:10,notNull:!0}),patternHash:C.varchar({length:64,notNull:!0,pgName:`pattern_hash`}),updatedAt:C.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityType`,`locale`]}]}),$=Q.table;function ee(e,t,n){let r=y(e,t,n);if(r===null)return null;let i=typeof r==`function`?r.toString():JSON.stringify(r);return j(`sha256`).update(i).digest(`hex`)}async function te(e){let t=a(),n=t.defaultLocale,r=t.locales.map(e=>e.code),i=Q.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=ee(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}}))}}}function ne(e){return w({name:`${e.name}_drafts`,columns:{id:C.uuid({primaryKey:!0,defaultRandom:!0}),entityId:C.uuid({notNull:!0,pgName:`entity_id`}),locale:C.varchar({length:10,notNull:!0,default:`_`}),data:C.jsonb({notNull:!0}),createdBy:C.varchar({length:255,notNull:!0,pgName:`created_by`}),createdByName:C.varchar({length:255,pgName:`created_by_name`}),createdAt:C.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),updatedAt:C.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityId`,`locale`]}]})}function re(e){return w({name:`${e.name}_locale_status`,columns:{id:C.uuid({primaryKey:!0,defaultRandom:!0}),entityId:C.uuid({notNull:!0,pgName:`entity_id`}),locale:C.varchar({length:10,notNull:!0}),status:C.varchar({length:20,notNull:!0,default:`draft`}),publishedAt:C.timestamp({withTimezone:!0,pgName:`published_at`})},unique:[{on:[`entityId`,`locale`]}]})}function ie(e){return w({name:`${e.name}_versions`,columns:{id:C.uuid({primaryKey:!0,defaultRandom:!0}),entityId:C.uuid({notNull:!0,pgName:`entity_id`}),version:C.integer({notNull:!0}),locale:C.varchar({length:10,notNull:!0,default:`_`}),data:C.jsonb({notNull:!0}),delta:C.jsonb(),status:C.varchar({length:20}),createdBy:C.varchar({length:255,pgName:`created_by`}),createdByName:C.varchar({length:255,pgName:`created_by_name`}),createdAt:C.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),isAutosave:C.boolean({notNull:!0,default:!1,pgName:`is_autosave`})},unique:[{on:[`entityId`,`version`,`locale`]}]})}function ae(t){if(!t.locales||t.locales.length===0)throw Error(`content() requires at least one locale`);if(!t.defaultLocale)throw Error(`content() requires a defaultLocale`);if(!t.locales.some(e=>e.code===t.defaultLocale))throw Error(`defaultLocale "${t.defaultLocale}" not found in locales: [${t.locales.map(e=>e.code).join(`, `)}]`);if(t.reservedPathPrefixes){for(let e of t.reservedPathPrefixes)if(typeof e!=`string`||!e.startsWith(`/`))throw Error(`content(): reservedPathPrefixes entries must be absolute paths starting with '/', got ${JSON.stringify(e)}`)}let a=t.autoTrash??!0,o=a===!1?void 0:{publishable:!0,...typeof a==`object`&&a.exclude?{exclude:a.exclude}:{}};return b({name:`@murumets-ee/content`,contributes:{"blocks.documentDraftStore":z(),...o?{"trash.autoEnable":o}:{},...t.reservedPathPrefixes?{"routing.reservedPathPrefixes":t.reservedPathPrefixes}:{}},shared:{settings:[n],features:[{resource:`redirect`,actions:[`manage`],defaultRoles:[`admin`],description:`Create, edit, and delete manual URL redirects (toolkit_redirects).`},{resource:`path`,actions:[`resolve`],defaultRoles:[`admin`,`editor`],description:`Resolve a site URL path to the CMS document that serves it.`}]},server:{entities:[e,r],tables:{toolkitContentLocks:h,toolkitPaths:l,toolkitPathPatterns:$,toolkitRedirects:d},routes:[...m(),...p()],init:async e=>{i(t);let n=0,r=0,a=0;for(let[t,i]of e.entities){if(i.behaviors?.some(e=>e.name===`versionable`)){let e=`${t}_versions`;if(!T.has(e)){let n=ie(i);T.register(e,n.table),P(t,{versions:n})}n++}let e=i.behaviors?.some(e=>e.name===`publishable`),o=i.allFields,s=Object.values(o).some(e=>e.translatable);if(e&&s){let e=`${t}_locale_status`;if(!T.has(e)){let n=re(i);T.register(e,n.table),P(t,{localeStatus:n})}r++}if(e){let e=`${t}_drafts`;if(!T.has(e)){let n=ne(i);T.register(e,n.table),P(t,{drafts:n})}a++}}R(t.locales.map(e=>e.code)),K();try{await Z(e)}catch(t){e.logger.warn({err:t},"content: failed to register routing path jobs — descendant cascade + `lumi paths:rebuild` will be unavailable (path writes still succeed)")}e.logger.info({locales:t.locales.map(e=>e.code),defaultLocale:t.defaultLocale,versionableEntities:n,localeStatusEntities:r,draftEntities:a},`Content plugin initialized`)},onReady:async e=>{try{await te(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{L as n,F as r,ae as t};
|
|
2
|
+
//# sourceMappingURL=plugin-BSxbiGc1.mjs.map
|