@murumets-ee/content 0.107.0 → 0.108.1

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 (46) hide show
  1. package/dist/admin.d.mts +23 -1
  2. package/dist/admin.d.mts.map +1 -1
  3. package/dist/admin.mjs +1 -1
  4. package/dist/chrome-provider.d.mts +1 -1
  5. package/dist/client.d.mts +2 -2
  6. package/dist/client.mjs +1 -1
  7. package/dist/context-D7eEItCY.mjs +2 -0
  8. package/dist/{context-DtHgyrgb.mjs.map → context-D7eEItCY.mjs.map} +1 -1
  9. package/dist/entities/index.d.mts +2 -2
  10. package/dist/{index-Bni-PiIz.d.mts → index-cT8Qk4gu.d.mts} +2 -2
  11. package/dist/{index-Bni-PiIz.d.mts.map → index-cT8Qk4gu.d.mts.map} +1 -1
  12. package/dist/index.d.mts +4 -4
  13. package/dist/lock-service.d.mts +1 -1
  14. package/dist/plugin-CjgqBSFy.mjs +2 -0
  15. package/dist/{plugin-CvPWz399.mjs.map → plugin-CjgqBSFy.mjs.map} +1 -1
  16. package/dist/plugin.d.mts +2 -2
  17. package/dist/plugin.mjs +1 -1
  18. package/dist/registry-CaPbY5lm.mjs +2 -0
  19. package/dist/registry-CaPbY5lm.mjs.map +1 -0
  20. package/dist/routes-siXmjKDA.mjs +2 -0
  21. package/dist/routes-siXmjKDA.mjs.map +1 -0
  22. package/dist/{routing-strategy-DVWB7qPp.d.mts → routing-strategy-CAG3tZP0.d.mts} +2 -2
  23. package/dist/{routing-strategy-DVWB7qPp.d.mts.map → routing-strategy-CAG3tZP0.d.mts.map} +1 -1
  24. package/dist/{site-chrome-BiV3zhw0.d.mts → site-chrome-DNCcePSR.d.mts} +2 -2
  25. package/dist/{site-chrome-BiV3zhw0.d.mts.map → site-chrome-DNCcePSR.d.mts.map} +1 -1
  26. package/dist/translation-provenance-B41lEDM-.mjs +2 -0
  27. package/dist/translation-provenance-B41lEDM-.mjs.map +1 -0
  28. package/dist/{translation-provenance--dHtcmfg.d.mts → translation-provenance-BZ0MlbFR.d.mts} +2 -2
  29. package/dist/{translation-provenance--dHtcmfg.d.mts.map → translation-provenance-BZ0MlbFR.d.mts.map} +1 -1
  30. package/dist/translation.d.mts +144 -2
  31. package/dist/translation.d.mts.map +1 -0
  32. package/dist/translation.mjs +2 -1
  33. package/dist/translation.mjs.map +1 -0
  34. package/dist/{types-wMTglM4j.d.mts → types-BOk9tg_9.d.mts} +2 -2
  35. package/dist/{types-wMTglM4j.d.mts.map → types-BOk9tg_9.d.mts.map} +1 -1
  36. package/dist/{versionable-D2JjselN.d.mts → versionable-CRvV87nZ.d.mts} +2 -2
  37. package/dist/{versionable-D2JjselN.d.mts.map → versionable-CRvV87nZ.d.mts.map} +1 -1
  38. package/dist/{versions-B0_9TtZ0.mjs → versions-w2ui-Vzb.mjs} +2 -2
  39. package/dist/{versions-B0_9TtZ0.mjs.map → versions-w2ui-Vzb.mjs.map} +1 -1
  40. package/package.json +10 -10
  41. package/dist/context-DtHgyrgb.mjs +0 -2
  42. package/dist/plugin-CvPWz399.mjs +0 -2
  43. package/dist/routes-D-NWP0hK.mjs +0 -2
  44. package/dist/routes-D-NWP0hK.mjs.map +0 -1
  45. package/dist/translation-provenance-eqhsGAyi.mjs +0 -2
  46. package/dist/translation-provenance-eqhsGAyi.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translation.mjs","names":[],"sources":["../src/translatable-sources.ts"],"sourcesContent":["/**\n * `'content.translatableSources'` — text OUTSIDE the entity translation tables\n * that a whole-site translation still has to cover (agent-access D019).\n *\n * A menu keeps its per-locale labels inside one JSON tree, so the translation\n * engine's walk over translatable entity fields never sees them. Content owns\n * this slot so that the engine (`@murumets-ee/translation`) and the plugin that\n * owns such text (`@murumets-ee/menus`) need no dependency on each other: the\n * owner DECLARES a {@link TranslatableSource} on its plugin's `contributes`,\n * and the engine reads them with {@link getTranslatableSources}.\n *\n * No `server-only` and no database import: this module is types plus one\n * registry read, so a plugin entry that jiti loads may import from it.\n */\n\nimport { getContributions } from '@murumets-ee/core'\n\n/** One piece of source-locale text a {@link TranslatableSource} wants translated. */\nexport interface TranslatableSourceText {\n /** Stable within one item — the job hands it back to `write`. */\n key: string\n text: string\n maxLength?: number\n}\n\n/** One translatable thing of a kind — a menu, say — as `list` reports it. */\nexport interface TranslatableSourceItem {\n id: string\n label: string\n}\n\nexport interface TranslatableSourceRead {\n /** What `texts` was read from. `write` refuses (`'changed'`) when the item no longer matches it. */\n revision: string\n /** The source-locale texts that need a translation for this target, per `mode`. Empty = nothing to do. */\n texts: TranslatableSourceText[]\n}\n\n/**\n * Text outside the entity translation tables that a whole-site translation\n * covers (agent-access D019).\n *\n * ⚠️ **Every method runs INSIDE the caller's request context.** `getApp()` /\n * `app.getClient(entity)` see the ambient user and permission checker, and that\n * is the only authority a source has: it does its reads and writes through\n * `AdminClient` / `ContentClient` and NEVER elevates — no\n * `createSystemAdminClient`, no `runAsCli`, no hand-built context. The engine\n * checks {@link TranslatableSource.permission} with the starter's own checker\n * before it lists anything; the entity firewall under these methods is the\n * second gate, not a formality.\n *\n * Each call touches ONE item. `list` is the only enumeration, and it is a\n * bounded page.\n */\nexport interface TranslatableSource {\n /** Unique: `'menu'`. Lower-case `[a-z][a-z0-9_]*`. */\n kind: string\n /** English fallback beside {@link labelKey} (CLAUDE.md \"Interface copy\") — never rendered when the key resolves. */\n label: string\n /** next-intl key for {@link label}, in the contributing plugin's own namespace. */\n labelKey: string\n /** What the caller must hold to translate this kind. The job checks it with the starter's own checker before listing. */\n permission: { resource: string; action: string }\n /** `true`: `write` lands in a draft and nothing is live until someone publishes. */\n draft: boolean\n /**\n * A bounded keyset page, ordered by id: the items after `after`, at most\n * `limit` of them. `limit` is never above 200, and a source must honour it\n * EXACTLY up to that: the job asks for one item more than it reads, and\n * takes a page of FEWER rows than it asked for to mean \"there are no more\n * rows\". A source that caps its page lower than the `limit` it was given\n * silently ends the scan there — the rest is never planned, and nothing\n * says so.\n *\n * Under scope `all` a source's items are taken by EVERY run: a source\n * records nothing that says \"this is already a current machine\n * translation\" (`read` with mode `'all'` returns every text that has a\n * source). Only scope `missing` comes to an end for a source.\n */\n list(input: { after?: string; limit: number }): Promise<TranslatableSourceItem[]>\n /**\n * `null` = no such item (or not readable by this caller). mode `'missing'`:\n * only texts the target does not have yet; `'all'`: every text that has a\n * source.\n */\n read(input: {\n id: string\n sourceLocale: string\n targetLocale: string\n mode: 'missing' | 'all'\n }): Promise<TranslatableSourceRead | null>\n write(input: {\n id: string\n targetLocale: string\n revision: string\n /** key → translated text, for keys `read` returned. A key not in `texts` is left alone. */\n texts: Readonly<Record<string, string>>\n user: { id: string; name?: string }\n }): Promise<'written' | 'changed' | 'not_found'>\n}\n\ndeclare module '@murumets-ee/core' {\n interface PluginContributionSlots {\n /**\n * Translatable text that lives outside the entity translation tables, one\n * list per contributing plugin. Read it with `getTranslatableSources()`\n * from `@murumets-ee/content/translation`, which flattens the lists.\n */\n 'content.translatableSources': readonly TranslatableSource[]\n /**\n * Where a person starts a whole-site translation run — the admin page, and\n * the permission it needs. Contributed by `@murumets-ee/translation`; read\n * with `getSiteTranslationEntry()` by the editor, which links to it.\n */\n 'content.siteTranslation': SiteTranslationEntry\n }\n}\n\n/**\n * Every contributed {@link TranslatableSource}, as one flat list.\n *\n * One entry per `kind`: when two plugins contribute the same kind the LATER\n * plugin (in `plugins:` order) wins and keeps the earlier one's position, so\n * an app can replace a source without the list reordering. `[]` when nothing\n * contributes — an app without menus simply has no extra text to translate.\n */\nexport function getTranslatableSources(): TranslatableSource[] {\n const byKind = new Map<string, TranslatableSource>()\n for (const contribution of getContributions('content.translatableSources')) {\n for (const source of contribution) byKind.set(source.kind, source)\n }\n return [...byKind.values()]\n}\n\n/** The whole-site translation page, as the content editor links to it. */\nexport interface SiteTranslationEntry {\n /** An admin path without the locale prefix, e.g. `/admin/translate-site`. */\n href: string\n permission: { resource: string; action: string }\n}\n\n/** The contributed whole-site translation page, or `undefined` when no plugin offers one. */\nexport function getSiteTranslationEntry(): SiteTranslationEntry | undefined {\n return getContributions('content.siteTranslation').at(-1)\n}\n"],"mappings":"uLA8HA,SAAgB,GAA+C,CAC7D,IAAM,EAAS,IAAI,IACnB,IAAK,IAAM,KAAgB,EAAiB,6BAA6B,EACvE,IAAK,IAAM,KAAU,EAAc,EAAO,IAAI,EAAO,KAAM,CAAM,EAEnE,MAAO,CAAC,GAAG,EAAO,OAAO,CAAC,CAC5B,CAUA,SAAgB,GAA4D,CAC1E,OAAO,EAAiB,yBAAyB,CAAC,CAAC,GAAG,EAAE,CAC1D"}
@@ -1,4 +1,4 @@
1
- import { n as TranslationFreshness } from "./translation-provenance--dHtcmfg.mjs";
1
+ import { n as TranslationFreshness } from "./translation-provenance-BZ0MlbFR.mjs";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  interface LocaleConfig {
@@ -147,4 +147,4 @@ interface LockInfo {
147
147
  }
148
148
  //#endregion
149
149
  export { LocaleDirection as a, LockInfo as c, TranslationReport as d, TranslationStatus as f, LocaleConfig as i, RoutingStrategy as l, VersionableOptions as m, DraftEntry as n, LocaleEntry as o, VersionEntry as p, I18nConfig as r, LocalePublishStatus as s, ContentConfig as t, TranslationFieldStatus as u };
150
- //# sourceMappingURL=types-wMTglM4j.d.mts.map
150
+ //# sourceMappingURL=types-BOk9tg_9.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-wMTglM4j.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;UAUiB,YAAA;EACf,IAAA;EACA,KAAK;AAAA;AAIP;AAAA,KAAY,eAAA;;;AAAe;AAQ3B;;;KAAY,eAAA;AAAe;AAU3B;;;;;;;AAV2B,UAUV,WAAA;EACf,IAAA;EACA,KAAA;EACA,OAAA;EACA,SAAA,EAAW,eAAe;EAC1B,SAAA;AAAA;;;;;;;UASe,UAAA;EACf,aAAA;EACA,eAAA,EAAiB,eAAe;AAAA;AAAA,UAGjB,aAAA;EACf,OAAA,EAAS,YAAY;EACrB,aAAA;EAAA;;;;;AAmC+B;AAOjC;;;;AAIU;AAOV;;;;;;;;;EAhCE,oBAAA;EA0Ce;;;;;;;;;;;;;EA5Bf,SAAA;IAAwB,OAAA;EAAA;AAAA;AAAA,UAOT,kBAAA;EAsBf;EApBA,SAAA;EAoBU;EAlBV,QAAQ;AAAA;AAAA,UAOO,YAAA,eAA2B,MAAA,oBAA0B,MAAA;EACpE,EAAA;EACA,QAAA;EACA,OAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,KAAA,EAAO,OAAA,CAAQ,KAAA;EACf,MAAA;EACA,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,UAAA;AAAA;AAAA,UAae,sBAAA;EACf,QAAA;EACA,KAAA;EACA,UAAA;EACA,OAAA;EAmBA;;;AAEiB;AAOnB;EAtBE,SAAA,GAAY,oBAAoB;AAAA;AAAA,UAGjB,iBAAA;EAAA,CACd,MAAA,WAAiB,sBAAsB;AAAA;AAAA,UAGzB,iBAAA;EACf,KAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;EAqByB;EAnBzB,QAAA;EAmBwC;EAjBxC,iBAAA;AAAA;AAAA,UAOe,mBAAA;EACf,MAAA;EACA,MAAA;EACA,WAAA,EAAa,IAAI;AAAA;AAAA,UAOF,UAAA,eAAyB,MAAA,oBAA0B,MAAA;EAClE,EAAA;EACA,QAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,SAAA,EAAW,IAAA;AAAA;AAAA,UAOI,QAAA;EACf,QAAA;EACA,YAAA;EACA,QAAA,EAAU,IAAA;EACV,SAAA,EAAW,IAAI;AAAA"}
1
+ {"version":3,"file":"types-BOk9tg_9.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;UAUiB,YAAA;EACf,IAAA;EACA,KAAK;AAAA;AAIP;AAAA,KAAY,eAAA;;;AAAe;AAQ3B;;;KAAY,eAAA;AAAe;AAU3B;;;;;;;AAV2B,UAUV,WAAA;EACf,IAAA;EACA,KAAA;EACA,OAAA;EACA,SAAA,EAAW,eAAe;EAC1B,SAAA;AAAA;;;;;;;UASe,UAAA;EACf,aAAA;EACA,eAAA,EAAiB,eAAe;AAAA;AAAA,UAGjB,aAAA;EACf,OAAA,EAAS,YAAY;EACrB,aAAA;EAAA;;;;;AAmC+B;AAOjC;;;;AAIU;AAOV;;;;;;;;;EAhCE,oBAAA;EA0Ce;;;;;;;;;;;;;EA5Bf,SAAA;IAAwB,OAAA;EAAA;AAAA;AAAA,UAOT,kBAAA;EAsBf;EApBA,SAAA;EAoBU;EAlBV,QAAQ;AAAA;AAAA,UAOO,YAAA,eAA2B,MAAA,oBAA0B,MAAA;EACpE,EAAA;EACA,QAAA;EACA,OAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,KAAA,EAAO,OAAA,CAAQ,KAAA;EACf,MAAA;EACA,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,UAAA;AAAA;AAAA,UAae,sBAAA;EACf,QAAA;EACA,KAAA;EACA,UAAA;EACA,OAAA;EAmBA;;;AAEiB;AAOnB;EAtBE,SAAA,GAAY,oBAAoB;AAAA;AAAA,UAGjB,iBAAA;EAAA,CACd,MAAA,WAAiB,sBAAsB;AAAA;AAAA,UAGzB,iBAAA;EACf,KAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;EAqByB;EAnBzB,QAAA;EAmBwC;EAjBxC,iBAAA;AAAA;AAAA,UAOe,mBAAA;EACf,MAAA;EACA,MAAA;EACA,WAAA,EAAa,IAAI;AAAA;AAAA,UAOF,UAAA,eAAyB,MAAA,oBAA0B,MAAA;EAClE,EAAA;EACA,QAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,SAAA,EAAW,IAAA;AAAA;AAAA,UAOI,QAAA;EACf,QAAA;EACA,YAAA;EACA,QAAA,EAAU,IAAA;EACV,SAAA,EAAW,IAAI;AAAA"}
@@ -1,4 +1,4 @@
1
- import { m as VersionableOptions } from "./types-wMTglM4j.mjs";
1
+ import { m as VersionableOptions } from "./types-BOk9tg_9.mjs";
2
2
  import { Behavior, DateField, NumberField } from "@murumets-ee/entity";
3
3
 
4
4
  //#region src/behaviors/versionable.d.ts
@@ -15,4 +15,4 @@ declare function versionable(options?: VersionableOptions): Behavior<Versionable
15
15
  declare function getVersionableOptions(behaviors?: Behavior[]): Required<VersionableOptions> | null;
16
16
  //#endregion
17
17
  export { getVersionableOptions as n, versionable as r, VersionableFields as t };
18
- //# sourceMappingURL=versionable-D2JjselN.d.mts.map
18
+ //# sourceMappingURL=versionable-CRvV87nZ.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"versionable-D2JjselN.d.mts","names":[],"sources":["../src/behaviors/versionable.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"}
1
+ {"version":3,"file":"versionable-CRvV87nZ.d.mts","names":[],"sources":["../src/behaviors/versionable.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"}
@@ -1,2 +1,2 @@
1
- import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{i as t,n}from"./errors-NRNILhhm.mjs";import{t as r}from"./content-config-store-8omQF5p5.mjs";import{n as i}from"./plugin-CvPWz399.mjs";import{t as a}from"./locale-utils-B0mFoV47.mjs";import{a as o,i as s}from"./context-DtHgyrgb.mjs";import{schemaRegistry as c}from"@murumets-ee/db";import{eq as l}from"drizzle-orm";import"server-only";function u(e){return e.match(/\S+|\s+/g)??[]}function d(e,t){if(e===t)return e?[{type:`equal`,value:e}]:[];if(!e)return[{type:`add`,value:t}];if(!t)return[{type:`remove`,value:e}];let n=u(e),r=u(t),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=1;e<=i;e++){let t=o[e],i=o[e-1];if(!(!t||!i))for(let o=1;o<=a;o++)n[e-1]===r[o-1]?t[o]=(i[o-1]??0)+1:t[o]=Math.max(i[o]??0,t[o-1]??0)}let s=[],c=i,l=a,d=[];for(;c>0||l>0;){let e=c>0?n[c-1]:void 0,t=l>0?r[l-1]:void 0;if(c>0&&l>0&&e!==void 0&&e===t)d.push({type:`equal`,value:e}),c--,l--;else if(l>0&&t!==void 0&&(c===0||(o[c]?.[l-1]??0)>=(o[c-1]?.[l]??0)))d.push({type:`add`,value:t}),l--;else if(e!==void 0)d.push({type:`remove`,value:e}),c--;else break}d.reverse();for(let e of d){let t=s[s.length-1];t&&t.type===e.type?t.value+=e.value:s.push({...e})}return s}function f(e){return e.replace(/<[^>]*>/g,` `).replace(/\s+/g,` `).trim()}function p(e){return e==null?``:e instanceof Date?e.toISOString():typeof e==`object`?JSON.stringify(e):String(e)}function m(e,t,n,r){if(JSON.stringify(t)===JSON.stringify(n))return null;switch(r?.type??g(t,n)){case`text`:case`slug`:return{field:e,type:`text`,from:t,to:n,segments:d(p(t),p(n))};case`richtext`:return{field:e,type:`richtext`,from:t,to:n,segments:d(f(p(t)),f(p(n)))};case`number`:return{field:e,type:`number`,from:t,to:n};case`boolean`:return{field:e,type:`boolean`,from:t,to:n};case`date`:return{field:e,type:`date`,from:t,to:n};case`select`:return{field:e,type:`select`,from:t,to:n};case`media`:return{field:e,type:`media`,from:t,to:n};case`json`:return{field:e,type:`json`,from:t,to:n};case`blockTree`:return{field:e,type:`blockTree`,from:t,to:n};default:return{field:e,type:`unknown`,from:t,to:n}}}function h(e,t,n){let r=new Set([...Object.keys(e),...Object.keys(t)]),i=[];for(let a of r){if(a===`id`||a===`createdAt`||a===`updatedAt`)continue;let r=m(a,e[a],t[a],n?.[a]);r&&i.push(r)}return i}function g(e,t){let n=e??t;if(typeof n==`number`)return`number`;if(typeof n==`boolean`)return`boolean`;if(n instanceof Date)return`date`;if(typeof n==`string`)return/^\d{4}-\d{2}-\d{2}/.test(n)?`date`:`text`;if(Array.isArray(n)){let e=n[0];return n.length>0&&typeof e==`object`&&e&&`type`in e&&`id`in e?`blockTree`:`json`}return typeof n==`object`?`json`:`unknown`}function _(e,t){if(typeof e==`string`||typeof t==`string`)return d(p(e),p(t))}function v(e){if(typeof e!=`object`||!e)return null;let t=e;if(typeof t.id!=`string`||typeof t.type!=`string`)return null;let n={...t.props??{}};return t.traits!==void 0&&(n.traits=t.traits),t.children!==void 0&&(n.children=t.children),{id:t.id,type:t.type,props:n}}function y(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let i of n){let n=e[i],a=t[i];JSON.stringify(n)!==JSON.stringify(a)&&r.push({field:i,from:n,to:a,segments:_(n,a)})}return r}function b(e){let t=new Map,n=0;for(let r of e){let e=v(r);e&&(t.set(e.id,{block:e,index:n}),n++)}return t}function x(e,t){let n=b(e),r=b(t),i=[],a=new Set;for(let[e,{block:t,index:o}]of r){a.add(e);let r=n.get(e);if(!r){i.push({blockId:e,blockType:t.type,status:`added`,toIndex:o});continue}let s=r.block,c=r.index,l=y(s.props,t.props),u=c!==o;l.length===0&&!u?i.push({blockId:e,blockType:t.type,status:`unchanged`,fromIndex:c,toIndex:o}):l.length>0?i.push({blockId:e,blockType:t.type,status:`changed`,fromIndex:c,toIndex:o,propDiffs:l}):i.push({blockId:e,blockType:t.type,status:`moved`,fromIndex:c,toIndex:o})}for(let[e,{block:t,index:r}]of n)a.has(e)||i.push({blockId:e,blockType:t.type,status:`removed`,fromIndex:r});return i}function S(e){return e===`_`||e===r().defaultLocale}function C(e){let{defaultLocale:t}=r();return S(e)?{defaultLocale:t}:{locale:e,defaultLocale:t}}async function w(e,t,n,o,c){if(c!==void 0&&e.db!==c)throw Error("applyLocaleRoutedUpdate: `tx` must be the context's own db handle");let l=await s(e);return e.admin.updateForLocale(t,n,o,{defaultLocale:r().defaultLocale,...c!==void 0&&{tx:c},...l!==null&&{localeStatusWriter:(e,t,n)=>(a(t),i(l,e,t,n))}})}const T=new Set([`id`,`createdAt`,`createdBy`,`updatedAt`,`updatedBy`]);function E(e,t){let n=e.allFields,r={};for(let e of Object.keys(t)){if(T.has(e)||e.startsWith(`_`)||!Object.hasOwn(n,e))continue;let i=n[e];i&&i.internal===!0||(r[e]=t[e])}return r}function D(e,t){return JSON.stringify(e)===JSON.stringify(t)}function O(e,t){let n={};for(let r of Object.keys(t))r===`id`||r.startsWith(`_`)||D(e[r],t[r])||(n[r]=t[r]);return n}var k=e({compareVersions:()=>M,createVersionSnapshot:()=>I,getVersion:()=>j,getVersions:()=>A,pruneVersions:()=>P,restoreVersion:()=>N,saveVersionSnapshot:()=>F,writeVersionColumn:()=>L});async function A(e,t,n){n?.locale!==void 0&&a(n.locale);let r=await o(e);return r?await r.findMany({where:{entityId:t,locale:n?.locale??`_`},orderBy:[{column:`version`,dir:`desc`}],...n?.limit===void 0?{}:{limit:n.limit},...n?.offset===void 0?{}:{offset:n.offset}}):[]}async function j(e,t,n,r=`_`){a(r);let i=await o(e);return i?await i.findOne({entityId:t,version:n,locale:r})??null:null}async function M(e,t,n,r,i=`_`){a(i);let[o,s]=await Promise.all([j(e,t,n,i),j(e,t,r,i)]);if(!o||!s)return null;let c=e.entity.allFields,l=h(o.data,s.data,c);for(let e of l)e.type===`blockTree`&&(e.blockDiffs=x(Array.isArray(e.from)?e.from:[],Array.isArray(e.to)?e.to:[]));return{diffs:l,fromVersion:o,toVersion:s}}async function N(e,r,i,s=`_`){if(a(s),!await o(e))throw new n(e.entity.name,`versions`);let c=await j(e,r,i,s);if(!c)throw new t(r,i,s);let l=c.data;return w(e,r,E(e.entity,l),s)}async function P(e,t,n=`_`){if(a(n),!e.versionableOptions)return;let r=await o(e);if(!r)return;let i=e.versionableOptions.maxPerDoc,s=await r.findMany({where:{entityId:t,locale:n},orderBy:[{column:`version`,dir:`desc`}],limit:i});if(s.length<i)return;let c=s.map(e=>e.id);await r.deleteMany({entityId:t,locale:n,id:{notIn:c}})}async function F(e,t,n,r,i,s=`_`,c){a(s);let l=await o(e);if(!l)return;let[u]=await l.aggregate({select:{maxVersion:{fn:`max`,column:`version`}},where:{entityId:t,locale:s}}),d=Number(u?.maxVersion??0)+1,f=n.status??null;if(i){let e=await l.findOne({entityId:t,isAutosave:!0,version:d,locale:s});if(e)return await l.update({id:e.id},{data:n,delta:r,status:f,createdAt:new Date}),d}return await l.insert({entityId:t,version:d,locale:s,data:n,delta:r,status:f,createdBy:c?.id??null,createdByName:c?.name??null,isAutosave:i}),d}async function I(e,t,n,r,i,o){if(i!==void 0&&a(i),!e.versionableOptions)return;let s=i??`_`,c=await F(e,t,n,r?O(r,n):null,!1,s,o);c!==void 0&&s===`_`&&await L(e,t,c),await P(e,t,s)}async function L(e,t,n){let r=c.get(e.entity.name);r&&await e.db.update(r).set({_version:n}).where(l(r.id,t))}export{P as a,k as c,E as d,w as f,h,A as i,L as l,x as m,I as n,N as o,C as p,j as r,F as s,M as t,O as u};
2
- //# sourceMappingURL=versions-B0_9TtZ0.mjs.map
1
+ import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{i as t,n}from"./errors-NRNILhhm.mjs";import{t as r}from"./content-config-store-8omQF5p5.mjs";import{n as i}from"./plugin-CjgqBSFy.mjs";import{t as a}from"./locale-utils-B0mFoV47.mjs";import{a as o,i as s}from"./context-D7eEItCY.mjs";import{schemaRegistry as c}from"@murumets-ee/db";import{eq as l}from"drizzle-orm";import"server-only";function u(e){return e.match(/\S+|\s+/g)??[]}function d(e,t){if(e===t)return e?[{type:`equal`,value:e}]:[];if(!e)return[{type:`add`,value:t}];if(!t)return[{type:`remove`,value:e}];let n=u(e),r=u(t),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=1;e<=i;e++){let t=o[e],i=o[e-1];if(!(!t||!i))for(let o=1;o<=a;o++)n[e-1]===r[o-1]?t[o]=(i[o-1]??0)+1:t[o]=Math.max(i[o]??0,t[o-1]??0)}let s=[],c=i,l=a,d=[];for(;c>0||l>0;){let e=c>0?n[c-1]:void 0,t=l>0?r[l-1]:void 0;if(c>0&&l>0&&e!==void 0&&e===t)d.push({type:`equal`,value:e}),c--,l--;else if(l>0&&t!==void 0&&(c===0||(o[c]?.[l-1]??0)>=(o[c-1]?.[l]??0)))d.push({type:`add`,value:t}),l--;else if(e!==void 0)d.push({type:`remove`,value:e}),c--;else break}d.reverse();for(let e of d){let t=s[s.length-1];t&&t.type===e.type?t.value+=e.value:s.push({...e})}return s}function f(e){return e.replace(/<[^>]*>/g,` `).replace(/\s+/g,` `).trim()}function p(e){return e==null?``:e instanceof Date?e.toISOString():typeof e==`object`?JSON.stringify(e):String(e)}function m(e,t,n,r){if(JSON.stringify(t)===JSON.stringify(n))return null;switch(r?.type??g(t,n)){case`text`:case`slug`:return{field:e,type:`text`,from:t,to:n,segments:d(p(t),p(n))};case`richtext`:return{field:e,type:`richtext`,from:t,to:n,segments:d(f(p(t)),f(p(n)))};case`number`:return{field:e,type:`number`,from:t,to:n};case`boolean`:return{field:e,type:`boolean`,from:t,to:n};case`date`:return{field:e,type:`date`,from:t,to:n};case`select`:return{field:e,type:`select`,from:t,to:n};case`media`:return{field:e,type:`media`,from:t,to:n};case`json`:return{field:e,type:`json`,from:t,to:n};case`blockTree`:return{field:e,type:`blockTree`,from:t,to:n};default:return{field:e,type:`unknown`,from:t,to:n}}}function h(e,t,n){let r=new Set([...Object.keys(e),...Object.keys(t)]),i=[];for(let a of r){if(a===`id`||a===`createdAt`||a===`updatedAt`)continue;let r=m(a,e[a],t[a],n?.[a]);r&&i.push(r)}return i}function g(e,t){let n=e??t;if(typeof n==`number`)return`number`;if(typeof n==`boolean`)return`boolean`;if(n instanceof Date)return`date`;if(typeof n==`string`)return/^\d{4}-\d{2}-\d{2}/.test(n)?`date`:`text`;if(Array.isArray(n)){let e=n[0];return n.length>0&&typeof e==`object`&&e&&`type`in e&&`id`in e?`blockTree`:`json`}return typeof n==`object`?`json`:`unknown`}function _(e,t){if(typeof e==`string`||typeof t==`string`)return d(p(e),p(t))}function v(e){if(typeof e!=`object`||!e)return null;let t=e;if(typeof t.id!=`string`||typeof t.type!=`string`)return null;let n={...t.props??{}};return t.traits!==void 0&&(n.traits=t.traits),t.children!==void 0&&(n.children=t.children),{id:t.id,type:t.type,props:n}}function y(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let i of n){let n=e[i],a=t[i];JSON.stringify(n)!==JSON.stringify(a)&&r.push({field:i,from:n,to:a,segments:_(n,a)})}return r}function b(e){let t=new Map,n=0;for(let r of e){let e=v(r);e&&(t.set(e.id,{block:e,index:n}),n++)}return t}function x(e,t){let n=b(e),r=b(t),i=[],a=new Set;for(let[e,{block:t,index:o}]of r){a.add(e);let r=n.get(e);if(!r){i.push({blockId:e,blockType:t.type,status:`added`,toIndex:o});continue}let s=r.block,c=r.index,l=y(s.props,t.props),u=c!==o;l.length===0&&!u?i.push({blockId:e,blockType:t.type,status:`unchanged`,fromIndex:c,toIndex:o}):l.length>0?i.push({blockId:e,blockType:t.type,status:`changed`,fromIndex:c,toIndex:o,propDiffs:l}):i.push({blockId:e,blockType:t.type,status:`moved`,fromIndex:c,toIndex:o})}for(let[e,{block:t,index:r}]of n)a.has(e)||i.push({blockId:e,blockType:t.type,status:`removed`,fromIndex:r});return i}function S(e){return e===`_`||e===r().defaultLocale}function C(e){let{defaultLocale:t}=r();return S(e)?{defaultLocale:t}:{locale:e,defaultLocale:t}}async function w(e,t,n,o,c){if(c!==void 0&&e.db!==c)throw Error("applyLocaleRoutedUpdate: `tx` must be the context's own db handle");let l=await s(e);return e.admin.updateForLocale(t,n,o,{defaultLocale:r().defaultLocale,...c!==void 0&&{tx:c},...l!==null&&{localeStatusWriter:(e,t,n)=>(a(t),i(l,e,t,n))}})}const T=new Set([`id`,`createdAt`,`createdBy`,`updatedAt`,`updatedBy`]);function E(e,t){let n=e.allFields,r={};for(let e of Object.keys(t)){if(T.has(e)||e.startsWith(`_`)||!Object.hasOwn(n,e))continue;let i=n[e];i&&i.internal===!0||(r[e]=t[e])}return r}function D(e,t){return JSON.stringify(e)===JSON.stringify(t)}function O(e,t){let n={};for(let r of Object.keys(t))r===`id`||r.startsWith(`_`)||D(e[r],t[r])||(n[r]=t[r]);return n}var k=e({compareVersions:()=>M,createVersionSnapshot:()=>I,getVersion:()=>j,getVersions:()=>A,pruneVersions:()=>P,restoreVersion:()=>N,saveVersionSnapshot:()=>F,writeVersionColumn:()=>L});async function A(e,t,n){n?.locale!==void 0&&a(n.locale);let r=await o(e);return r?await r.findMany({where:{entityId:t,locale:n?.locale??`_`},orderBy:[{column:`version`,dir:`desc`}],...n?.limit===void 0?{}:{limit:n.limit},...n?.offset===void 0?{}:{offset:n.offset}}):[]}async function j(e,t,n,r=`_`){a(r);let i=await o(e);return i?await i.findOne({entityId:t,version:n,locale:r})??null:null}async function M(e,t,n,r,i=`_`){a(i);let[o,s]=await Promise.all([j(e,t,n,i),j(e,t,r,i)]);if(!o||!s)return null;let c=e.entity.allFields,l=h(o.data,s.data,c);for(let e of l)e.type===`blockTree`&&(e.blockDiffs=x(Array.isArray(e.from)?e.from:[],Array.isArray(e.to)?e.to:[]));return{diffs:l,fromVersion:o,toVersion:s}}async function N(e,r,i,s=`_`){if(a(s),!await o(e))throw new n(e.entity.name,`versions`);let c=await j(e,r,i,s);if(!c)throw new t(r,i,s);let l=c.data;return w(e,r,E(e.entity,l),s)}async function P(e,t,n=`_`){if(a(n),!e.versionableOptions)return;let r=await o(e);if(!r)return;let i=e.versionableOptions.maxPerDoc,s=await r.findMany({where:{entityId:t,locale:n},orderBy:[{column:`version`,dir:`desc`}],limit:i});if(s.length<i)return;let c=s.map(e=>e.id);await r.deleteMany({entityId:t,locale:n,id:{notIn:c}})}async function F(e,t,n,r,i,s=`_`,c){a(s);let l=await o(e);if(!l)return;let[u]=await l.aggregate({select:{maxVersion:{fn:`max`,column:`version`}},where:{entityId:t,locale:s}}),d=Number(u?.maxVersion??0)+1,f=n.status??null;if(i){let e=await l.findOne({entityId:t,isAutosave:!0,version:d,locale:s});if(e)return await l.update({id:e.id},{data:n,delta:r,status:f,createdAt:new Date}),d}return await l.insert({entityId:t,version:d,locale:s,data:n,delta:r,status:f,createdBy:c?.id??null,createdByName:c?.name??null,isAutosave:i}),d}async function I(e,t,n,r,i,o){if(i!==void 0&&a(i),!e.versionableOptions)return;let s=i??`_`,c=await F(e,t,n,r?O(r,n):null,!1,s,o);c!==void 0&&s===`_`&&await L(e,t,c),await P(e,t,s)}async function L(e,t,n){let r=c.get(e.entity.name);r&&await e.db.update(r).set({_version:n}).where(l(r.id,t))}export{P as a,k as c,E as d,w as f,h,A as i,L as l,x as m,I as n,N as o,C as p,j as r,F as s,M as t,O as u};
2
+ //# sourceMappingURL=versions-w2ui-Vzb.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"versions-B0_9TtZ0.mjs","names":[],"sources":["../src/diff/text-diff.ts","../src/diff/field-diff.ts","../src/diff/block-diff.ts","../src/internal/apply-locale-routed-update.ts","../src/internal/projection.ts","../src/versions.ts"],"sourcesContent":["/**\n * Word-level text diff using a simplified Myers algorithm.\n *\n * Splits text into words (preserving whitespace), computes the longest common\n * subsequence, and returns an array of diff segments.\n */\n\nimport type { DiffSegment } from './types.js'\n\n/**\n * Split text into tokens — words and whitespace runs — so diffs are\n * human-readable (\"Changed *this* word\") rather than character-level.\n */\nfunction tokenize(text: string): string[] {\n return text.match(/\\S+|\\s+/g) ?? []\n}\n\n/**\n * Compute word-level diff between two strings.\n *\n * Uses a simple LCS (dynamic programming) approach which is fine for\n * the text sizes we deal with (entity field values, not source code).\n */\nexport function diffText(from: string, to: string): DiffSegment[] {\n if (from === to) {\n return from ? [{ type: 'equal', value: from }] : []\n }\n if (!from) {\n return [{ type: 'add', value: to }]\n }\n if (!to) {\n return [{ type: 'remove', value: from }]\n }\n\n const fromTokens = tokenize(from)\n const toTokens = tokenize(to)\n\n // Build LCS table\n const m = fromTokens.length\n const n = toTokens.length\n const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0))\n\n for (let i = 1; i <= m; i++) {\n const dpI = dp[i]\n const dpIm1 = dp[i - 1]\n if (!dpI || !dpIm1) continue\n for (let j = 1; j <= n; j++) {\n if (fromTokens[i - 1] === toTokens[j - 1]) {\n dpI[j] = (dpIm1[j - 1] ?? 0) + 1\n } else {\n dpI[j] = Math.max(dpIm1[j] ?? 0, dpI[j - 1] ?? 0)\n }\n }\n }\n\n // Backtrack to build diff segments\n const segments: DiffSegment[] = []\n let i = m\n let j = n\n\n // Collect in reverse, then flip\n const reversed: DiffSegment[] = []\n\n while (i > 0 || j > 0) {\n const fromTok = i > 0 ? fromTokens[i - 1] : undefined\n const toTok = j > 0 ? toTokens[j - 1] : undefined\n if (i > 0 && j > 0 && fromTok !== undefined && fromTok === toTok) {\n reversed.push({ type: 'equal', value: fromTok })\n i--\n j--\n } else if (j > 0 && toTok !== undefined && (i === 0 || (dp[i]?.[j - 1] ?? 0) >= (dp[i - 1]?.[j] ?? 0))) {\n reversed.push({ type: 'add', value: toTok })\n j--\n } else if (fromTok !== undefined) {\n reversed.push({ type: 'remove', value: fromTok })\n i--\n } else {\n // Defensive: i and j both 0, but loop condition implied otherwise.\n break\n }\n }\n\n reversed.reverse()\n\n // Merge consecutive segments of the same type\n for (const seg of reversed) {\n const last = segments[segments.length - 1]\n if (last && last.type === seg.type) {\n last.value += seg.value\n } else {\n segments.push({ ...seg })\n }\n }\n\n return segments\n}\n","/**\n * Field-level diff — dispatches to the appropriate diff strategy per field type.\n */\n\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport { diffText } from './text-diff.js'\nimport type { DiffSegment, FieldDiff } from './types.js'\n\n/** Strip HTML tags for diffing richtext content as plain text. */\nfunction stripHtml(html: string): string {\n return html\n .replace(/<[^>]*>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** Stringify a value for display (dates, objects, etc.). */\nfunction stringify(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'object') return JSON.stringify(value)\n return String(value)\n}\n\n/**\n * Compute diff for a single field, using the field's type to determine\n * the best diff strategy.\n */\nexport function diffField(\n fieldName: string,\n from: unknown,\n to: unknown,\n fieldConfig?: FieldConfig,\n): FieldDiff | null {\n // No change\n if (JSON.stringify(from) === JSON.stringify(to)) return null\n\n const type = fieldConfig?.type ?? inferType(from, to)\n\n switch (type) {\n case 'text':\n case 'slug': {\n const segments = diffText(stringify(from), stringify(to))\n return { field: fieldName, type: 'text', from, to, segments }\n }\n\n case 'richtext': {\n // Diff on stripped HTML so users see word changes, not tag noise\n const segments = diffText(stripHtml(stringify(from)), stripHtml(stringify(to)))\n return { field: fieldName, type: 'richtext', from, to, segments }\n }\n\n case 'number':\n return { field: fieldName, type: 'number', from, to }\n\n case 'boolean':\n return { field: fieldName, type: 'boolean', from, to }\n\n case 'date':\n return { field: fieldName, type: 'date', from, to }\n\n case 'select':\n return { field: fieldName, type: 'select', from, to }\n\n case 'media':\n return { field: fieldName, type: 'media', from, to }\n\n case 'json':\n return { field: fieldName, type: 'json', from, to }\n\n case 'blockTree':\n // Block tree — block-diff.ts computes the structural diff; just mark the\n // field type so compareVersions/compareDraftWithCurrent populate\n // `blockDiffs`.\n return { field: fieldName, type: 'blockTree', from, to }\n\n default:\n return { field: fieldName, type: 'unknown', from, to }\n }\n}\n\n/**\n * Diff all fields between two version snapshots.\n * Pass fieldConfigs to get type-aware diffs; without them, types are inferred.\n */\nexport function diffFields(\n from: Record<string, unknown>,\n to: Record<string, unknown>,\n fieldConfigs?: Record<string, FieldConfig>,\n): FieldDiff[] {\n const allKeys = new Set([...Object.keys(from), ...Object.keys(to)])\n const diffs: FieldDiff[] = []\n\n for (const key of allKeys) {\n // Skip internal/auto-generated fields\n if (key === 'id' || key === 'createdAt' || key === 'updatedAt') continue\n\n const diff = diffField(key, from[key], to[key], fieldConfigs?.[key])\n if (diff) diffs.push(diff)\n }\n\n return diffs\n}\n\n/** Best-effort type inference when no FieldConfig is available. */\nfunction inferType(from: unknown, to: unknown): string {\n const val = from ?? to\n if (typeof val === 'number') return 'number'\n if (typeof val === 'boolean') return 'boolean'\n if (val instanceof Date) return 'date'\n if (typeof val === 'string') {\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(val)) return 'date'\n return 'text'\n }\n if (Array.isArray(val)) {\n const first = val[0]\n if (val.length > 0 && typeof first === 'object' && first !== null) {\n // Block tree: instances discriminated by `type` + `id`.\n if ('type' in first && 'id' in first) return 'blockTree'\n }\n return 'json'\n }\n if (typeof val === 'object') return 'json'\n return 'unknown'\n}\n\n/** Diff two string values and return segments (used by block-diff for string props). */\nexport function diffStringValues(from: unknown, to: unknown): DiffSegment[] | undefined {\n if (typeof from === 'string' || typeof to === 'string') {\n return diffText(stringify(from), stringify(to))\n }\n return undefined\n}\n","/**\n * Structural diff for block-tree arrays (`field.blockTree()` values).\n *\n * Operates on new-format `BlockInstance`s (`{ id, type, props, traits,\n * children }`) — the only block shape the toolkit stores. Blocks are\n * identified by their `id`. The algorithm detects:\n * - Added blocks (new id in target)\n * - Removed blocks (missing id in target)\n * - Moved blocks (same id, different position)\n * - Changed blocks (same id, different prop/trait/child values)\n * - Unchanged blocks\n */\n\nimport { diffStringValues } from './field-diff.js'\nimport type { BlockDiffEntry, PropDiff } from './types.js'\n\n/** Comparable view of a block instance. */\ninterface NormalizedBlock {\n id: string\n type: string\n /** Comparable content map: `props` + folded-in `traits` / `children`. */\n props: Record<string, unknown>\n}\n\n/**\n * Normalize a `BlockInstance` to its comparable `{ id, type, props }` view,\n * or null when the value isn't a recognizable block. `traits` and `children`\n * fold into the comparable map so trait edits and nested-tree changes surface\n * as prop diffs (`diffProps` skips JSON-equal values, so empty/unchanged ones\n * add no noise).\n */\nfunction normalizeBlock(val: unknown): NormalizedBlock | null {\n if (typeof val !== 'object' || val === null) return null\n const rec = val as Record<string, unknown>\n\n if (typeof rec.id !== 'string' || typeof rec.type !== 'string') return null\n\n const props: Record<string, unknown> = { ...((rec.props as Record<string, unknown>) ?? {}) }\n if (rec.traits !== undefined) props.traits = rec.traits\n if (rec.children !== undefined) props.children = rec.children\n return { id: rec.id, type: rec.type, props }\n}\n\n/**\n * Diff two prop objects within a single block, returning per-prop diffs.\n */\nfunction diffProps(\n fromProps: Record<string, unknown>,\n toProps: Record<string, unknown>,\n): PropDiff[] {\n const allKeys = new Set([...Object.keys(fromProps), ...Object.keys(toProps)])\n const diffs: PropDiff[] = []\n\n for (const key of allKeys) {\n const fromVal = fromProps[key]\n const toVal = toProps[key]\n\n if (JSON.stringify(fromVal) !== JSON.stringify(toVal)) {\n diffs.push({\n field: key,\n from: fromVal,\n to: toVal,\n segments: diffStringValues(fromVal, toVal),\n })\n }\n }\n\n return diffs\n}\n\n/**\n * Index a block array by id, assigning each block its position AMONG\n * recognizable blocks (not its raw array index). Values that fail\n * normalization are skipped without consuming a position, so a stray\n * non-block entry can't skew move detection for the real blocks around it.\n */\nfunction indexBlocks(blocks: unknown[]): Map<string, { block: NormalizedBlock; index: number }> {\n const map = new Map<string, { block: NormalizedBlock; index: number }>()\n let position = 0\n for (const raw of blocks) {\n const block = normalizeBlock(raw)\n if (block) {\n map.set(block.id, { block, index: position })\n position++\n }\n }\n return map\n}\n\n/**\n * Compute structural diff between two block arrays.\n *\n * Each block is identified by its id. Position changes, additions, removals,\n * and prop-level changes are all detected.\n */\nexport function diffBlocks(fromBlocks: unknown[], toBlocks: unknown[]): BlockDiffEntry[] {\n const fromMap = indexBlocks(fromBlocks)\n const toMap = indexBlocks(toBlocks)\n\n const entries: BlockDiffEntry[] = []\n const processedIds = new Set<string>()\n\n // Walk the \"to\" array to preserve target order\n for (const [id, { block: toBlock, index: toIndex }] of toMap) {\n processedIds.add(id)\n const fromEntry = fromMap.get(id)\n\n if (!fromEntry) {\n // Added\n entries.push({\n blockId: id,\n blockType: toBlock.type,\n status: 'added',\n toIndex,\n })\n continue\n }\n\n const fromBlock = fromEntry.block\n const fromIndex = fromEntry.index\n const propDiffs = diffProps(fromBlock.props, toBlock.props)\n const moved = fromIndex !== toIndex\n\n if (propDiffs.length === 0 && !moved) {\n entries.push({\n blockId: id,\n blockType: toBlock.type,\n status: 'unchanged',\n fromIndex,\n toIndex,\n })\n } else if (propDiffs.length > 0) {\n entries.push({\n blockId: id,\n blockType: toBlock.type,\n status: 'changed',\n fromIndex,\n toIndex,\n propDiffs,\n })\n } else {\n entries.push({\n blockId: id,\n blockType: toBlock.type,\n status: 'moved',\n fromIndex,\n toIndex,\n })\n }\n }\n\n // Removed blocks (in \"from\" but not in \"to\")\n for (const [id, { block, index }] of fromMap) {\n if (!processedIds.has(id)) {\n entries.push({\n blockId: id,\n blockType: block.type,\n status: 'removed',\n fromIndex: index,\n })\n }\n }\n\n return entries\n}\n","/**\n * Locale-routed entity write shared by `publishDraft` and `restoreVersion`.\n *\n * Since SD021, the routing itself lives in ONE place —\n * `AdminClient.updateForLocale` (`@murumets-ee/entity`): default locale →\n * base row; non-default → translatable fields to `saveTranslation`,\n * `status` to the per-locale status store, non-translatable remainder to\n * the base row. This module is the thin content-side call: it supplies the\n * plugin's `defaultLocale` and a per-call locale-status writer bound to\n * THIS context's tables, so routing is deterministic regardless of how\n * `ctx.admin` was constructed.\n *\n * Lives in its own internal module because `drafts.ts` imports\n * `versions.ts` (for `createVersionSnapshot`) — exporting this from\n * either of them would close an import cycle.\n */\n\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport type { InferEntityDTO, InferUpdateInput } from '@murumets-ee/entity/admin'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n// The LEAF definition, not the `../plugin.js` re-export — see `locale-utils.ts`'s\n// identical comment. `plugin.ts` imports this module (indirectly, through\n// `document-version-store.ts`'s contribution), so the barrel would cycle.\nimport { getContentConfig } from '../content-config-store.js'\nimport { upsertLocaleStatusRow } from '../locale-status-slot.js'\nimport { DEFAULT_LOCALE_SENTINEL, validateLocale } from '../locale-utils.js'\nimport { type ContentContext, getLocaleStatusClient } from './context.js'\n\n/** Is this snapshot-key locale the default locale (base-row) one? */\nexport function isDefaultLocale(locale: string): boolean {\n return locale === DEFAULT_LOCALE_SENTINEL || locale === getContentConfig().defaultLocale\n}\n\n/**\n * `findById` options that read the same locale view the write targets —\n * default locale reads the base row, non-default reads the\n * translation-merged row.\n */\nexport function localeFindOptions(\n locale: string,\n): { defaultLocale: string } | { locale: string; defaultLocale: string } {\n const { defaultLocale } = getContentConfig()\n return isDefaultLocale(locale) ? { defaultLocale } : { locale, defaultLocale }\n}\n\n/**\n * Apply projected entity-field data to the given locale via the SD021\n * choke point. `updateData` MUST already be projected through\n * `projectToEntityFields` — this function only delegates, it does not\n * whitelist.\n *\n * `tx` joins the row write to a caller's transaction. The locale-status writer\n * needs no tx of its own: it is bound to `ctx`, and a caller that passes `tx`\n * builds `ctx` over that same transaction (`publishDraft`), so the status row\n * commits or rolls back with the base row.\n */\nexport async function applyLocaleRoutedUpdate<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n updateData: Record<string, unknown>,\n locale: string,\n tx?: PostgresJsDatabase,\n): Promise<InferEntityDTO<F>> {\n // Bind the status writer to THIS context's locale-status table when the\n // entity has one; otherwise let updateForLocale fall back to the base\n // status column (non-translatable publishables have only the default\n // locale).\n //\n // The writer is the RAW upsert, not `setLocaleStatus`: since the publish gate\n // moved INTO `setLocaleStatus` (which now delegates to this same\n // `updateForLocale`), passing it here would re-enter the method one extra\n // level deep. `updateForLocale` has already asserted the caller's publish\n // authority from `updateData` before it reaches any writer, so the raw upsert\n // is gated by the call below — it is not a second ungated door.\n // The status writer below is bound to `ctx.db`, so a `tx` with a context NOT\n // built over it would write the row inside the transaction and the status\n // outside it — a partial publish nothing reports.\n if (tx !== undefined && ctx.db !== tx) {\n throw new Error(\"applyLocaleRoutedUpdate: `tx` must be the context's own db handle\")\n }\n const localeStatusClient = await getLocaleStatusClient(ctx)\n return ctx.admin.updateForLocale(entityId, updateData as InferUpdateInput<F>, locale, {\n defaultLocale: getContentConfig().defaultLocale,\n ...(tx !== undefined && { tx }),\n ...(localeStatusClient !== null && {\n localeStatusWriter: (id: string, loc: string, status: 'draft' | 'published') => {\n // Same whitelist `setLocaleStatus` applied on this path before — an\n // unconfigured locale must not mint a status row no reader will merge.\n validateLocale(loc)\n return upsertLocaleStatusRow(localeStatusClient, id, loc, status)\n },\n }),\n })\n}\n","/**\n * Field projection + delta helpers shared between drafts and versions.\n *\n * `projectToEntityFields` is the security-critical whitelist filter\n * applied before replaying stored data through `admin.update()` —\n * see audit-security-content.md M#4 (drafts/restore must not pass\n * through forbidden fields like `status: 'published'` or\n * `_workflowStatus: 'approved'`).\n *\n * `valuesEqual` and `calculateDelta` are pure utilities used by both\n * the version-snapshot delta calculator and the block-diff prop\n * comparison — collapsing 3× repeated `JSON.stringify(a) !== JSON.stringify(b)`\n * sites identified in audit-quality-content.md DRY candidates.\n */\n\nimport type { Entity, FieldConfig } from '@murumets-ee/entity'\n\n/**\n * Identity / audit / timestamp fields that exist as real columns on every\n * entity (added by `defineEntity` / `auditable()` / `timestamped()`) and\n * MUST NOT be relayed through `admin.update()` from a draft or version\n * payload. They're either server-set (`createdAt`, `createdBy`,\n * `updatedAt`, `updatedBy`) or immutable identity (`id`).\n *\n * Without this explicit deny-list, `id` would survive `projectToEntityFields`\n * because `defineEntity` injects it into `entity.allFields` and it's not\n * marked `internal: true`. AdminClient.update's Zod schema strips it\n * downstream as a final defense, but security-critical filtering should\n * live in one place — not split across the projection AND the validator.\n */\nconst PROJECTION_DENYLIST = new Set(['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'])\n\n/**\n * Project a draft / version payload through the entity's declared\n * field map before forwarding to `admin.update()`. Used by\n * `publishDraft()` and `restoreVersion()` — both replay stored data\n * that originated from a write, but neither should be allowed to set\n * fields the entity didn't declare.\n *\n * Drops:\n * - the audit/identity column set above (`id`, `createdAt`,\n * `createdBy`, `updatedAt`, `updatedBy`)\n * - any key starting with `_` (behavior internals managed by hooks)\n * - any field declared `internal: true` (writable only via\n * AdminClient.updateInternal — never via public CRUD or replay)\n * - any key not in `entity.allFields` (whitelist for app-defined fields)\n */\nexport function projectToEntityFields(\n entity: Entity,\n data: Record<string, unknown>,\n): Record<string, unknown> {\n const allowed = entity.allFields as Record<string, FieldConfig>\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(data)) {\n if (PROJECTION_DENYLIST.has(key)) continue\n if (key.startsWith('_')) continue\n if (!Object.hasOwn(allowed, key)) continue\n const f = allowed[key]\n if (f && (f as { internal?: boolean }).internal === true) continue\n out[key] = data[key]\n }\n return out\n}\n\n/**\n * Structural equality via canonical JSON serialization. Coarse, with\n * three known quirks worth flagging at every call site:\n *\n * 1. **Key order matters in pathological cases.** Two objects with\n * identical keys+values but different insertion order serialize\n * differently. Safe for entity field values where keys come from\n * the same field map on both sides; not safe for arbitrary user\n * JSONB blobs.\n * 2. **Dates compare equal to their ISO string.** `new Date('2026-01-01')`\n * and `'2026-01-01T00:00:00.000Z'` both stringify to\n * `\"2026-01-01T00:00:00.000Z\"`. After a round-trip through JSONB\n * storage, a previously Date-typed field will come back as a\n * string and compare equal to its origin Date — exactly what we\n * want for delta calculation.\n * 3. **`undefined` is dropped.** `{ x: undefined }` and `{}` both\n * serialize to `{}`. If you need to distinguish \"field present\n * but null\" from \"field missing\", do not use this helper.\n */\nexport function valuesEqual(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b)\n}\n\n/**\n * Field-level delta between previous and current entity snapshots.\n * Skips `id` and any underscore-prefixed behavior-internal field.\n * Only changed keys are returned. Used by version-snapshot persistence\n * to store a delta alongside the full snapshot for compact \"what\n * changed since last version\" reads.\n */\nexport function calculateDelta(\n previous: Record<string, unknown>,\n current: Record<string, unknown>,\n): Record<string, unknown> {\n const delta: Record<string, unknown> = {}\n for (const key of Object.keys(current)) {\n if (key === 'id' || key.startsWith('_')) continue\n if (!valuesEqual(previous[key], current[key])) {\n delta[key] = current[key]\n }\n }\n return delta\n}\n","/**\n * Version-history operations on a content entity.\n *\n * All functions take a {@link ContentContext} as first argument and\n * are wired into ContentClient via thin delegation in `client.ts`.\n *\n * Capability: requires the entity to declare `versionable()`. Read\n * methods return falsy when the entity doesn't support versions\n * (defensive, supports mixed-entity loops in the dashboard); write\n * methods throw `EntityCapabilityError` mapped to HTTP 400 by admin-ui.\n */\n\nimport 'server-only'\n\nimport { schemaRegistry } from '@murumets-ee/db'\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport type { InferEntityDTO } from '@murumets-ee/entity/admin'\nimport { eq } from 'drizzle-orm'\nimport { diffBlocks } from './diff/block-diff.js'\nimport { diffFields } from './diff/field-diff.js'\nimport type { FieldDiff } from './diff/types.js'\nimport { EntityCapabilityError, VersionNotFoundError } from './errors.js'\nimport { applyLocaleRoutedUpdate } from './internal/apply-locale-routed-update.js'\nimport { type ContentContext, getVersionsClient } from './internal/context.js'\nimport { calculateDelta, projectToEntityFields } from './internal/projection.js'\nimport { DEFAULT_LOCALE_SENTINEL, validateLocale } from './locale-utils.js'\nimport type { VersionEntry } from './types.js'\n\n/**\n * List versions for an entity (newest first), filtered by locale.\n * Uses '_' as the locale value for default-locale snapshots.\n */\nexport async function getVersions<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n options?: { limit?: number; offset?: number; locale?: string },\n): Promise<VersionEntry<InferEntityDTO<F>>[]> {\n if (options?.locale !== undefined) validateLocale(options.locale)\n const client = await getVersionsClient(ctx)\n if (!client) return []\n\n const rows = await client.findMany({\n where: { entityId, locale: options?.locale ?? DEFAULT_LOCALE_SENTINEL },\n orderBy: [{ column: 'version', dir: 'desc' }],\n ...(options?.limit !== undefined ? { limit: options.limit } : {}),\n ...(options?.offset !== undefined ? { offset: options.offset } : {}),\n })\n return rows as VersionEntry<InferEntityDTO<F>>[]\n}\n\n/**\n * Get a specific version snapshot.\n */\nexport async function getVersion<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n version: number,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<VersionEntry<InferEntityDTO<F>> | null> {\n validateLocale(locale)\n const client = await getVersionsClient(ctx)\n if (!client) return null\n const row = await client.findOne({ entityId, version, locale })\n return (row ?? null) as VersionEntry<InferEntityDTO<F>> | null\n}\n\n/**\n * Compare two versions and return field-level diff.\n *\n * Returns structured diffs with word-level segments for text fields\n * and structural block diffs for blocks fields.\n */\nexport async function compareVersions<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n v1: number,\n v2: number,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<{\n diffs: FieldDiff[]\n fromVersion: VersionEntry<InferEntityDTO<F>>\n toVersion: VersionEntry<InferEntityDTO<F>>\n} | null> {\n validateLocale(locale)\n const [version1, version2] = await Promise.all([\n getVersion(ctx, entityId, v1, locale),\n getVersion(ctx, entityId, v2, locale),\n ])\n if (!version1 || !version2) return null\n\n const fieldConfigs = ctx.entity.allFields as Record<string, FieldConfig> | undefined\n const diffs = diffFields(version1.data, version2.data, fieldConfigs)\n for (const diff of diffs) {\n if (diff.type === 'blockTree') {\n // Normalize a null/undefined side to [] so add/clear of the whole field\n // (null ↔ blockTree[]) still produces a structural diff, not an opaque one.\n diff.blockDiffs = diffBlocks(\n Array.isArray(diff.from) ? diff.from : [],\n Array.isArray(diff.to) ? diff.to : [],\n )\n }\n }\n return { diffs, fromVersion: version1, toVersion: version2 }\n}\n\n/**\n * Restore an entity to a previous version. Replays the snapshot data\n * through the locale-routed writer, projected through `entity.allFields`\n * so forbidden fields (workflow internals, behavior-managed underscore\n * fields, fields the entity declared `internal: true`) cannot leak.\n *\n * Snapshots are locale-keyed; the restore reads the snapshot for the\n * given locale and writes it back to the same locale's home —\n * translations for non-default locales, the base row for the default.\n */\nexport async function restoreVersion<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n version: number,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<InferEntityDTO<F>> {\n validateLocale(locale)\n if (!(await getVersionsClient(ctx))) {\n throw new EntityCapabilityError(ctx.entity.name, 'versions')\n }\n const versionData = await getVersion(ctx, entityId, version, locale)\n if (!versionData) {\n throw new VersionNotFoundError(entityId, version, locale)\n }\n const data = versionData.data as Record<string, unknown>\n const updateData = projectToEntityFields(ctx.entity, data)\n return applyLocaleRoutedUpdate(ctx, entityId, updateData, locale)\n}\n\n/**\n * Enforce maxPerDoc retention by deleting oldest versions for a locale.\n */\nexport async function pruneVersions(\n ctx: ContentContext,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<void> {\n validateLocale(locale)\n if (!ctx.versionableOptions) return\n const client = await getVersionsClient(ctx)\n if (!client) return\n\n const maxPerDoc = ctx.versionableOptions.maxPerDoc\n\n // Get IDs of versions to keep (newest N for this locale). The findMany\n // is capped at `limit: maxPerDoc`, so `keepVersions.length` is\n // `min(totalRows, maxPerDoc)` — never larger than the limit.\n const keepVersions = await client.findMany({\n where: { entityId, locale },\n orderBy: [{ column: 'version', dir: 'desc' }],\n limit: maxPerDoc,\n })\n\n // Strictly under the limit ⇒ totalRows < maxPerDoc ⇒ nothing to prune.\n // At the limit (length == maxPerDoc) we cannot distinguish \"exactly\n // maxPerDoc\" from \"more than maxPerDoc\" without another count query,\n // so we always run the DELETE — Postgres correctly removes 0-or-more\n // older rows. Better one always-cheap DELETE per prune than a guard\n // round trip on every call.\n if (keepVersions.length < maxPerDoc) return\n\n const keepIds = keepVersions.map((v) => v.id as string)\n\n // Delete everything for this entity+locale NOT in the keep list.\n await client.deleteMany({\n entityId,\n locale,\n id: { notIn: keepIds },\n })\n}\n\n/**\n * Persist a new version snapshot. Internal helper used by\n * `createVersionSnapshot` and ContentClient.update / .create.\n *\n * Attribution: ONLY the explicit `user` parameter is trusted. The\n * fallback chain that previously read `data.updatedBy` /\n * `data.createdBy` was an audit-log integrity gap (see audit\n * MEDIUM #1) — version history must never absorb a forged actor from\n * the request payload.\n */\nexport async function saveVersionSnapshot(\n ctx: ContentContext,\n entityId: string,\n data: Record<string, unknown>,\n delta: Record<string, unknown> | null,\n isAutosave: boolean,\n locale = DEFAULT_LOCALE_SENTINEL,\n user?: { id: string; name?: string },\n): Promise<number | undefined> {\n // All current callers pass a pre-validated locale (or the sentinel).\n // Defensive check at the boundary so future callers — or anyone who\n // exposes this helper publicly — still get the validation.\n validateLocale(locale)\n const client = await getVersionsClient(ctx)\n if (!client) return\n\n // Get the next version number for this locale (max existing + 1).\n const [maxRow] = await client.aggregate<{ maxVersion: number | null }>({\n select: { maxVersion: { fn: 'max', column: 'version' } },\n where: { entityId, locale },\n })\n const version = Number(maxRow?.maxVersion ?? 0) + 1\n const status = (data.status as string) ?? null\n\n // Autosave: update the existing autosave row at the same version\n // number, preserving \"autosaves land on the latest slot\" semantics.\n if (isAutosave) {\n const existing = await client.findOne({ entityId, isAutosave: true, version, locale })\n if (existing) {\n await client.update(\n { id: existing.id as string },\n { data, delta, status, createdAt: new Date() },\n )\n return version\n }\n }\n\n await client.insert({\n entityId,\n version,\n locale,\n data,\n delta,\n status,\n createdBy: user?.id ?? null,\n createdByName: user?.name ?? null,\n isAutosave,\n })\n return version\n}\n\n/**\n * Create a version snapshot for an entity after an external update.\n * Use this when the update was performed by AdminClient directly\n * (e.g. the admin API handler) rather than through ContentClient.update().\n *\n * @param locale - The locale being edited. Use undefined or '_' for the default locale.\n * @param user - The user who made the change (id + name for display).\n */\nexport async function createVersionSnapshot(\n ctx: ContentContext,\n entityId: string,\n currentData: Record<string, unknown>,\n previousData?: Record<string, unknown> | null,\n locale?: string,\n user?: { id: string; name?: string },\n): Promise<void> {\n if (locale !== undefined) validateLocale(locale)\n if (!ctx.versionableOptions) return\n const snapshotLocale = locale ?? DEFAULT_LOCALE_SENTINEL\n const delta = previousData ? calculateDelta(previousData, currentData) : null\n const version = await saveVersionSnapshot(\n ctx,\n entityId,\n currentData,\n delta,\n false,\n snapshotLocale,\n user,\n )\n\n // Update _version on the entity to match (default locale only).\n if (version !== undefined && snapshotLocale === DEFAULT_LOCALE_SENTINEL) {\n await writeVersionColumn(ctx, entityId, version)\n }\n\n await pruneVersions(ctx, entityId, snapshotLocale)\n}\n\n/**\n * Write the `_version` column on the entity row.\n *\n * KNOWN EXCEPTION: this bypasses AdminClient and goes straight to the\n * entity pgTable via `schemaRegistry` + direct Drizzle. Using\n * `admin.update()` would retrigger the `versionable.beforeUpdate` hook\n * and loop indefinitely. The only ContentClient path that still writes\n * to an entity table directly.\n */\nexport async function writeVersionColumn(\n ctx: ContentContext,\n entityId: string,\n version: number,\n): Promise<void> {\n const table = schemaRegistry.get(ctx.entity.name)\n if (!table) return\n await ctx.db.update(table).set({ _version: version }).where(eq(table.id, entityId))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"versions-w2ui-Vzb.mjs","names":[],"sources":["../src/diff/text-diff.ts","../src/diff/field-diff.ts","../src/diff/block-diff.ts","../src/internal/apply-locale-routed-update.ts","../src/internal/projection.ts","../src/versions.ts"],"sourcesContent":["/**\n * Word-level text diff using a simplified Myers algorithm.\n *\n * Splits text into words (preserving whitespace), computes the longest common\n * subsequence, and returns an array of diff segments.\n */\n\nimport type { DiffSegment } from './types.js'\n\n/**\n * Split text into tokens — words and whitespace runs — so diffs are\n * human-readable (\"Changed *this* word\") rather than character-level.\n */\nfunction tokenize(text: string): string[] {\n return text.match(/\\S+|\\s+/g) ?? []\n}\n\n/**\n * Compute word-level diff between two strings.\n *\n * Uses a simple LCS (dynamic programming) approach which is fine for\n * the text sizes we deal with (entity field values, not source code).\n */\nexport function diffText(from: string, to: string): DiffSegment[] {\n if (from === to) {\n return from ? [{ type: 'equal', value: from }] : []\n }\n if (!from) {\n return [{ type: 'add', value: to }]\n }\n if (!to) {\n return [{ type: 'remove', value: from }]\n }\n\n const fromTokens = tokenize(from)\n const toTokens = tokenize(to)\n\n // Build LCS table\n const m = fromTokens.length\n const n = toTokens.length\n const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0))\n\n for (let i = 1; i <= m; i++) {\n const dpI = dp[i]\n const dpIm1 = dp[i - 1]\n if (!dpI || !dpIm1) continue\n for (let j = 1; j <= n; j++) {\n if (fromTokens[i - 1] === toTokens[j - 1]) {\n dpI[j] = (dpIm1[j - 1] ?? 0) + 1\n } else {\n dpI[j] = Math.max(dpIm1[j] ?? 0, dpI[j - 1] ?? 0)\n }\n }\n }\n\n // Backtrack to build diff segments\n const segments: DiffSegment[] = []\n let i = m\n let j = n\n\n // Collect in reverse, then flip\n const reversed: DiffSegment[] = []\n\n while (i > 0 || j > 0) {\n const fromTok = i > 0 ? fromTokens[i - 1] : undefined\n const toTok = j > 0 ? toTokens[j - 1] : undefined\n if (i > 0 && j > 0 && fromTok !== undefined && fromTok === toTok) {\n reversed.push({ type: 'equal', value: fromTok })\n i--\n j--\n } else if (j > 0 && toTok !== undefined && (i === 0 || (dp[i]?.[j - 1] ?? 0) >= (dp[i - 1]?.[j] ?? 0))) {\n reversed.push({ type: 'add', value: toTok })\n j--\n } else if (fromTok !== undefined) {\n reversed.push({ type: 'remove', value: fromTok })\n i--\n } else {\n // Defensive: i and j both 0, but loop condition implied otherwise.\n break\n }\n }\n\n reversed.reverse()\n\n // Merge consecutive segments of the same type\n for (const seg of reversed) {\n const last = segments[segments.length - 1]\n if (last && last.type === seg.type) {\n last.value += seg.value\n } else {\n segments.push({ ...seg })\n }\n }\n\n return segments\n}\n","/**\n * Field-level diff — dispatches to the appropriate diff strategy per field type.\n */\n\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport { diffText } from './text-diff.js'\nimport type { DiffSegment, FieldDiff } from './types.js'\n\n/** Strip HTML tags for diffing richtext content as plain text. */\nfunction stripHtml(html: string): string {\n return html\n .replace(/<[^>]*>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** Stringify a value for display (dates, objects, etc.). */\nfunction stringify(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'object') return JSON.stringify(value)\n return String(value)\n}\n\n/**\n * Compute diff for a single field, using the field's type to determine\n * the best diff strategy.\n */\nexport function diffField(\n fieldName: string,\n from: unknown,\n to: unknown,\n fieldConfig?: FieldConfig,\n): FieldDiff | null {\n // No change\n if (JSON.stringify(from) === JSON.stringify(to)) return null\n\n const type = fieldConfig?.type ?? inferType(from, to)\n\n switch (type) {\n case 'text':\n case 'slug': {\n const segments = diffText(stringify(from), stringify(to))\n return { field: fieldName, type: 'text', from, to, segments }\n }\n\n case 'richtext': {\n // Diff on stripped HTML so users see word changes, not tag noise\n const segments = diffText(stripHtml(stringify(from)), stripHtml(stringify(to)))\n return { field: fieldName, type: 'richtext', from, to, segments }\n }\n\n case 'number':\n return { field: fieldName, type: 'number', from, to }\n\n case 'boolean':\n return { field: fieldName, type: 'boolean', from, to }\n\n case 'date':\n return { field: fieldName, type: 'date', from, to }\n\n case 'select':\n return { field: fieldName, type: 'select', from, to }\n\n case 'media':\n return { field: fieldName, type: 'media', from, to }\n\n case 'json':\n return { field: fieldName, type: 'json', from, to }\n\n case 'blockTree':\n // Block tree — block-diff.ts computes the structural diff; just mark the\n // field type so compareVersions/compareDraftWithCurrent populate\n // `blockDiffs`.\n return { field: fieldName, type: 'blockTree', from, to }\n\n default:\n return { field: fieldName, type: 'unknown', from, to }\n }\n}\n\n/**\n * Diff all fields between two version snapshots.\n * Pass fieldConfigs to get type-aware diffs; without them, types are inferred.\n */\nexport function diffFields(\n from: Record<string, unknown>,\n to: Record<string, unknown>,\n fieldConfigs?: Record<string, FieldConfig>,\n): FieldDiff[] {\n const allKeys = new Set([...Object.keys(from), ...Object.keys(to)])\n const diffs: FieldDiff[] = []\n\n for (const key of allKeys) {\n // Skip internal/auto-generated fields\n if (key === 'id' || key === 'createdAt' || key === 'updatedAt') continue\n\n const diff = diffField(key, from[key], to[key], fieldConfigs?.[key])\n if (diff) diffs.push(diff)\n }\n\n return diffs\n}\n\n/** Best-effort type inference when no FieldConfig is available. */\nfunction inferType(from: unknown, to: unknown): string {\n const val = from ?? to\n if (typeof val === 'number') return 'number'\n if (typeof val === 'boolean') return 'boolean'\n if (val instanceof Date) return 'date'\n if (typeof val === 'string') {\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(val)) return 'date'\n return 'text'\n }\n if (Array.isArray(val)) {\n const first = val[0]\n if (val.length > 0 && typeof first === 'object' && first !== null) {\n // Block tree: instances discriminated by `type` + `id`.\n if ('type' in first && 'id' in first) return 'blockTree'\n }\n return 'json'\n }\n if (typeof val === 'object') return 'json'\n return 'unknown'\n}\n\n/** Diff two string values and return segments (used by block-diff for string props). */\nexport function diffStringValues(from: unknown, to: unknown): DiffSegment[] | undefined {\n if (typeof from === 'string' || typeof to === 'string') {\n return diffText(stringify(from), stringify(to))\n }\n return undefined\n}\n","/**\n * Structural diff for block-tree arrays (`field.blockTree()` values).\n *\n * Operates on new-format `BlockInstance`s (`{ id, type, props, traits,\n * children }`) — the only block shape the toolkit stores. Blocks are\n * identified by their `id`. The algorithm detects:\n * - Added blocks (new id in target)\n * - Removed blocks (missing id in target)\n * - Moved blocks (same id, different position)\n * - Changed blocks (same id, different prop/trait/child values)\n * - Unchanged blocks\n */\n\nimport { diffStringValues } from './field-diff.js'\nimport type { BlockDiffEntry, PropDiff } from './types.js'\n\n/** Comparable view of a block instance. */\ninterface NormalizedBlock {\n id: string\n type: string\n /** Comparable content map: `props` + folded-in `traits` / `children`. */\n props: Record<string, unknown>\n}\n\n/**\n * Normalize a `BlockInstance` to its comparable `{ id, type, props }` view,\n * or null when the value isn't a recognizable block. `traits` and `children`\n * fold into the comparable map so trait edits and nested-tree changes surface\n * as prop diffs (`diffProps` skips JSON-equal values, so empty/unchanged ones\n * add no noise).\n */\nfunction normalizeBlock(val: unknown): NormalizedBlock | null {\n if (typeof val !== 'object' || val === null) return null\n const rec = val as Record<string, unknown>\n\n if (typeof rec.id !== 'string' || typeof rec.type !== 'string') return null\n\n const props: Record<string, unknown> = { ...((rec.props as Record<string, unknown>) ?? {}) }\n if (rec.traits !== undefined) props.traits = rec.traits\n if (rec.children !== undefined) props.children = rec.children\n return { id: rec.id, type: rec.type, props }\n}\n\n/**\n * Diff two prop objects within a single block, returning per-prop diffs.\n */\nfunction diffProps(\n fromProps: Record<string, unknown>,\n toProps: Record<string, unknown>,\n): PropDiff[] {\n const allKeys = new Set([...Object.keys(fromProps), ...Object.keys(toProps)])\n const diffs: PropDiff[] = []\n\n for (const key of allKeys) {\n const fromVal = fromProps[key]\n const toVal = toProps[key]\n\n if (JSON.stringify(fromVal) !== JSON.stringify(toVal)) {\n diffs.push({\n field: key,\n from: fromVal,\n to: toVal,\n segments: diffStringValues(fromVal, toVal),\n })\n }\n }\n\n return diffs\n}\n\n/**\n * Index a block array by id, assigning each block its position AMONG\n * recognizable blocks (not its raw array index). Values that fail\n * normalization are skipped without consuming a position, so a stray\n * non-block entry can't skew move detection for the real blocks around it.\n */\nfunction indexBlocks(blocks: unknown[]): Map<string, { block: NormalizedBlock; index: number }> {\n const map = new Map<string, { block: NormalizedBlock; index: number }>()\n let position = 0\n for (const raw of blocks) {\n const block = normalizeBlock(raw)\n if (block) {\n map.set(block.id, { block, index: position })\n position++\n }\n }\n return map\n}\n\n/**\n * Compute structural diff between two block arrays.\n *\n * Each block is identified by its id. Position changes, additions, removals,\n * and prop-level changes are all detected.\n */\nexport function diffBlocks(fromBlocks: unknown[], toBlocks: unknown[]): BlockDiffEntry[] {\n const fromMap = indexBlocks(fromBlocks)\n const toMap = indexBlocks(toBlocks)\n\n const entries: BlockDiffEntry[] = []\n const processedIds = new Set<string>()\n\n // Walk the \"to\" array to preserve target order\n for (const [id, { block: toBlock, index: toIndex }] of toMap) {\n processedIds.add(id)\n const fromEntry = fromMap.get(id)\n\n if (!fromEntry) {\n // Added\n entries.push({\n blockId: id,\n blockType: toBlock.type,\n status: 'added',\n toIndex,\n })\n continue\n }\n\n const fromBlock = fromEntry.block\n const fromIndex = fromEntry.index\n const propDiffs = diffProps(fromBlock.props, toBlock.props)\n const moved = fromIndex !== toIndex\n\n if (propDiffs.length === 0 && !moved) {\n entries.push({\n blockId: id,\n blockType: toBlock.type,\n status: 'unchanged',\n fromIndex,\n toIndex,\n })\n } else if (propDiffs.length > 0) {\n entries.push({\n blockId: id,\n blockType: toBlock.type,\n status: 'changed',\n fromIndex,\n toIndex,\n propDiffs,\n })\n } else {\n entries.push({\n blockId: id,\n blockType: toBlock.type,\n status: 'moved',\n fromIndex,\n toIndex,\n })\n }\n }\n\n // Removed blocks (in \"from\" but not in \"to\")\n for (const [id, { block, index }] of fromMap) {\n if (!processedIds.has(id)) {\n entries.push({\n blockId: id,\n blockType: block.type,\n status: 'removed',\n fromIndex: index,\n })\n }\n }\n\n return entries\n}\n","/**\n * Locale-routed entity write shared by `publishDraft` and `restoreVersion`.\n *\n * Since SD021, the routing itself lives in ONE place —\n * `AdminClient.updateForLocale` (`@murumets-ee/entity`): default locale →\n * base row; non-default → translatable fields to `saveTranslation`,\n * `status` to the per-locale status store, non-translatable remainder to\n * the base row. This module is the thin content-side call: it supplies the\n * plugin's `defaultLocale` and a per-call locale-status writer bound to\n * THIS context's tables, so routing is deterministic regardless of how\n * `ctx.admin` was constructed.\n *\n * Lives in its own internal module because `drafts.ts` imports\n * `versions.ts` (for `createVersionSnapshot`) — exporting this from\n * either of them would close an import cycle.\n */\n\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport type { InferEntityDTO, InferUpdateInput } from '@murumets-ee/entity/admin'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n// The LEAF definition, not the `../plugin.js` re-export — see `locale-utils.ts`'s\n// identical comment. `plugin.ts` imports this module (indirectly, through\n// `document-version-store.ts`'s contribution), so the barrel would cycle.\nimport { getContentConfig } from '../content-config-store.js'\nimport { upsertLocaleStatusRow } from '../locale-status-slot.js'\nimport { DEFAULT_LOCALE_SENTINEL, validateLocale } from '../locale-utils.js'\nimport { type ContentContext, getLocaleStatusClient } from './context.js'\n\n/** Is this snapshot-key locale the default locale (base-row) one? */\nexport function isDefaultLocale(locale: string): boolean {\n return locale === DEFAULT_LOCALE_SENTINEL || locale === getContentConfig().defaultLocale\n}\n\n/**\n * `findById` options that read the same locale view the write targets —\n * default locale reads the base row, non-default reads the\n * translation-merged row.\n */\nexport function localeFindOptions(\n locale: string,\n): { defaultLocale: string } | { locale: string; defaultLocale: string } {\n const { defaultLocale } = getContentConfig()\n return isDefaultLocale(locale) ? { defaultLocale } : { locale, defaultLocale }\n}\n\n/**\n * Apply projected entity-field data to the given locale via the SD021\n * choke point. `updateData` MUST already be projected through\n * `projectToEntityFields` — this function only delegates, it does not\n * whitelist.\n *\n * `tx` joins the row write to a caller's transaction. The locale-status writer\n * needs no tx of its own: it is bound to `ctx`, and a caller that passes `tx`\n * builds `ctx` over that same transaction (`publishDraft`), so the status row\n * commits or rolls back with the base row.\n */\nexport async function applyLocaleRoutedUpdate<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n updateData: Record<string, unknown>,\n locale: string,\n tx?: PostgresJsDatabase,\n): Promise<InferEntityDTO<F>> {\n // Bind the status writer to THIS context's locale-status table when the\n // entity has one; otherwise let updateForLocale fall back to the base\n // status column (non-translatable publishables have only the default\n // locale).\n //\n // The writer is the RAW upsert, not `setLocaleStatus`: since the publish gate\n // moved INTO `setLocaleStatus` (which now delegates to this same\n // `updateForLocale`), passing it here would re-enter the method one extra\n // level deep. `updateForLocale` has already asserted the caller's publish\n // authority from `updateData` before it reaches any writer, so the raw upsert\n // is gated by the call below — it is not a second ungated door.\n // The status writer below is bound to `ctx.db`, so a `tx` with a context NOT\n // built over it would write the row inside the transaction and the status\n // outside it — a partial publish nothing reports.\n if (tx !== undefined && ctx.db !== tx) {\n throw new Error(\"applyLocaleRoutedUpdate: `tx` must be the context's own db handle\")\n }\n const localeStatusClient = await getLocaleStatusClient(ctx)\n return ctx.admin.updateForLocale(entityId, updateData as InferUpdateInput<F>, locale, {\n defaultLocale: getContentConfig().defaultLocale,\n ...(tx !== undefined && { tx }),\n ...(localeStatusClient !== null && {\n localeStatusWriter: (id: string, loc: string, status: 'draft' | 'published') => {\n // Same whitelist `setLocaleStatus` applied on this path before — an\n // unconfigured locale must not mint a status row no reader will merge.\n validateLocale(loc)\n return upsertLocaleStatusRow(localeStatusClient, id, loc, status)\n },\n }),\n })\n}\n","/**\n * Field projection + delta helpers shared between drafts and versions.\n *\n * `projectToEntityFields` is the security-critical whitelist filter\n * applied before replaying stored data through `admin.update()` —\n * see audit-security-content.md M#4 (drafts/restore must not pass\n * through forbidden fields like `status: 'published'` or\n * `_workflowStatus: 'approved'`).\n *\n * `valuesEqual` and `calculateDelta` are pure utilities used by both\n * the version-snapshot delta calculator and the block-diff prop\n * comparison — collapsing 3× repeated `JSON.stringify(a) !== JSON.stringify(b)`\n * sites identified in audit-quality-content.md DRY candidates.\n */\n\nimport type { Entity, FieldConfig } from '@murumets-ee/entity'\n\n/**\n * Identity / audit / timestamp fields that exist as real columns on every\n * entity (added by `defineEntity` / `auditable()` / `timestamped()`) and\n * MUST NOT be relayed through `admin.update()` from a draft or version\n * payload. They're either server-set (`createdAt`, `createdBy`,\n * `updatedAt`, `updatedBy`) or immutable identity (`id`).\n *\n * Without this explicit deny-list, `id` would survive `projectToEntityFields`\n * because `defineEntity` injects it into `entity.allFields` and it's not\n * marked `internal: true`. AdminClient.update's Zod schema strips it\n * downstream as a final defense, but security-critical filtering should\n * live in one place — not split across the projection AND the validator.\n */\nconst PROJECTION_DENYLIST = new Set(['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'])\n\n/**\n * Project a draft / version payload through the entity's declared\n * field map before forwarding to `admin.update()`. Used by\n * `publishDraft()` and `restoreVersion()` — both replay stored data\n * that originated from a write, but neither should be allowed to set\n * fields the entity didn't declare.\n *\n * Drops:\n * - the audit/identity column set above (`id`, `createdAt`,\n * `createdBy`, `updatedAt`, `updatedBy`)\n * - any key starting with `_` (behavior internals managed by hooks)\n * - any field declared `internal: true` (writable only via\n * AdminClient.updateInternal — never via public CRUD or replay)\n * - any key not in `entity.allFields` (whitelist for app-defined fields)\n */\nexport function projectToEntityFields(\n entity: Entity,\n data: Record<string, unknown>,\n): Record<string, unknown> {\n const allowed = entity.allFields as Record<string, FieldConfig>\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(data)) {\n if (PROJECTION_DENYLIST.has(key)) continue\n if (key.startsWith('_')) continue\n if (!Object.hasOwn(allowed, key)) continue\n const f = allowed[key]\n if (f && (f as { internal?: boolean }).internal === true) continue\n out[key] = data[key]\n }\n return out\n}\n\n/**\n * Structural equality via canonical JSON serialization. Coarse, with\n * three known quirks worth flagging at every call site:\n *\n * 1. **Key order matters in pathological cases.** Two objects with\n * identical keys+values but different insertion order serialize\n * differently. Safe for entity field values where keys come from\n * the same field map on both sides; not safe for arbitrary user\n * JSONB blobs.\n * 2. **Dates compare equal to their ISO string.** `new Date('2026-01-01')`\n * and `'2026-01-01T00:00:00.000Z'` both stringify to\n * `\"2026-01-01T00:00:00.000Z\"`. After a round-trip through JSONB\n * storage, a previously Date-typed field will come back as a\n * string and compare equal to its origin Date — exactly what we\n * want for delta calculation.\n * 3. **`undefined` is dropped.** `{ x: undefined }` and `{}` both\n * serialize to `{}`. If you need to distinguish \"field present\n * but null\" from \"field missing\", do not use this helper.\n */\nexport function valuesEqual(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b)\n}\n\n/**\n * Field-level delta between previous and current entity snapshots.\n * Skips `id` and any underscore-prefixed behavior-internal field.\n * Only changed keys are returned. Used by version-snapshot persistence\n * to store a delta alongside the full snapshot for compact \"what\n * changed since last version\" reads.\n */\nexport function calculateDelta(\n previous: Record<string, unknown>,\n current: Record<string, unknown>,\n): Record<string, unknown> {\n const delta: Record<string, unknown> = {}\n for (const key of Object.keys(current)) {\n if (key === 'id' || key.startsWith('_')) continue\n if (!valuesEqual(previous[key], current[key])) {\n delta[key] = current[key]\n }\n }\n return delta\n}\n","/**\n * Version-history operations on a content entity.\n *\n * All functions take a {@link ContentContext} as first argument and\n * are wired into ContentClient via thin delegation in `client.ts`.\n *\n * Capability: requires the entity to declare `versionable()`. Read\n * methods return falsy when the entity doesn't support versions\n * (defensive, supports mixed-entity loops in the dashboard); write\n * methods throw `EntityCapabilityError` mapped to HTTP 400 by admin-ui.\n */\n\nimport 'server-only'\n\nimport { schemaRegistry } from '@murumets-ee/db'\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport type { InferEntityDTO } from '@murumets-ee/entity/admin'\nimport { eq } from 'drizzle-orm'\nimport { diffBlocks } from './diff/block-diff.js'\nimport { diffFields } from './diff/field-diff.js'\nimport type { FieldDiff } from './diff/types.js'\nimport { EntityCapabilityError, VersionNotFoundError } from './errors.js'\nimport { applyLocaleRoutedUpdate } from './internal/apply-locale-routed-update.js'\nimport { type ContentContext, getVersionsClient } from './internal/context.js'\nimport { calculateDelta, projectToEntityFields } from './internal/projection.js'\nimport { DEFAULT_LOCALE_SENTINEL, validateLocale } from './locale-utils.js'\nimport type { VersionEntry } from './types.js'\n\n/**\n * List versions for an entity (newest first), filtered by locale.\n * Uses '_' as the locale value for default-locale snapshots.\n */\nexport async function getVersions<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n options?: { limit?: number; offset?: number; locale?: string },\n): Promise<VersionEntry<InferEntityDTO<F>>[]> {\n if (options?.locale !== undefined) validateLocale(options.locale)\n const client = await getVersionsClient(ctx)\n if (!client) return []\n\n const rows = await client.findMany({\n where: { entityId, locale: options?.locale ?? DEFAULT_LOCALE_SENTINEL },\n orderBy: [{ column: 'version', dir: 'desc' }],\n ...(options?.limit !== undefined ? { limit: options.limit } : {}),\n ...(options?.offset !== undefined ? { offset: options.offset } : {}),\n })\n return rows as VersionEntry<InferEntityDTO<F>>[]\n}\n\n/**\n * Get a specific version snapshot.\n */\nexport async function getVersion<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n version: number,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<VersionEntry<InferEntityDTO<F>> | null> {\n validateLocale(locale)\n const client = await getVersionsClient(ctx)\n if (!client) return null\n const row = await client.findOne({ entityId, version, locale })\n return (row ?? null) as VersionEntry<InferEntityDTO<F>> | null\n}\n\n/**\n * Compare two versions and return field-level diff.\n *\n * Returns structured diffs with word-level segments for text fields\n * and structural block diffs for blocks fields.\n */\nexport async function compareVersions<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n v1: number,\n v2: number,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<{\n diffs: FieldDiff[]\n fromVersion: VersionEntry<InferEntityDTO<F>>\n toVersion: VersionEntry<InferEntityDTO<F>>\n} | null> {\n validateLocale(locale)\n const [version1, version2] = await Promise.all([\n getVersion(ctx, entityId, v1, locale),\n getVersion(ctx, entityId, v2, locale),\n ])\n if (!version1 || !version2) return null\n\n const fieldConfigs = ctx.entity.allFields as Record<string, FieldConfig> | undefined\n const diffs = diffFields(version1.data, version2.data, fieldConfigs)\n for (const diff of diffs) {\n if (diff.type === 'blockTree') {\n // Normalize a null/undefined side to [] so add/clear of the whole field\n // (null ↔ blockTree[]) still produces a structural diff, not an opaque one.\n diff.blockDiffs = diffBlocks(\n Array.isArray(diff.from) ? diff.from : [],\n Array.isArray(diff.to) ? diff.to : [],\n )\n }\n }\n return { diffs, fromVersion: version1, toVersion: version2 }\n}\n\n/**\n * Restore an entity to a previous version. Replays the snapshot data\n * through the locale-routed writer, projected through `entity.allFields`\n * so forbidden fields (workflow internals, behavior-managed underscore\n * fields, fields the entity declared `internal: true`) cannot leak.\n *\n * Snapshots are locale-keyed; the restore reads the snapshot for the\n * given locale and writes it back to the same locale's home —\n * translations for non-default locales, the base row for the default.\n */\nexport async function restoreVersion<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n version: number,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<InferEntityDTO<F>> {\n validateLocale(locale)\n if (!(await getVersionsClient(ctx))) {\n throw new EntityCapabilityError(ctx.entity.name, 'versions')\n }\n const versionData = await getVersion(ctx, entityId, version, locale)\n if (!versionData) {\n throw new VersionNotFoundError(entityId, version, locale)\n }\n const data = versionData.data as Record<string, unknown>\n const updateData = projectToEntityFields(ctx.entity, data)\n return applyLocaleRoutedUpdate(ctx, entityId, updateData, locale)\n}\n\n/**\n * Enforce maxPerDoc retention by deleting oldest versions for a locale.\n */\nexport async function pruneVersions(\n ctx: ContentContext,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<void> {\n validateLocale(locale)\n if (!ctx.versionableOptions) return\n const client = await getVersionsClient(ctx)\n if (!client) return\n\n const maxPerDoc = ctx.versionableOptions.maxPerDoc\n\n // Get IDs of versions to keep (newest N for this locale). The findMany\n // is capped at `limit: maxPerDoc`, so `keepVersions.length` is\n // `min(totalRows, maxPerDoc)` — never larger than the limit.\n const keepVersions = await client.findMany({\n where: { entityId, locale },\n orderBy: [{ column: 'version', dir: 'desc' }],\n limit: maxPerDoc,\n })\n\n // Strictly under the limit ⇒ totalRows < maxPerDoc ⇒ nothing to prune.\n // At the limit (length == maxPerDoc) we cannot distinguish \"exactly\n // maxPerDoc\" from \"more than maxPerDoc\" without another count query,\n // so we always run the DELETE — Postgres correctly removes 0-or-more\n // older rows. Better one always-cheap DELETE per prune than a guard\n // round trip on every call.\n if (keepVersions.length < maxPerDoc) return\n\n const keepIds = keepVersions.map((v) => v.id as string)\n\n // Delete everything for this entity+locale NOT in the keep list.\n await client.deleteMany({\n entityId,\n locale,\n id: { notIn: keepIds },\n })\n}\n\n/**\n * Persist a new version snapshot. Internal helper used by\n * `createVersionSnapshot` and ContentClient.update / .create.\n *\n * Attribution: ONLY the explicit `user` parameter is trusted. The\n * fallback chain that previously read `data.updatedBy` /\n * `data.createdBy` was an audit-log integrity gap (see audit\n * MEDIUM #1) — version history must never absorb a forged actor from\n * the request payload.\n */\nexport async function saveVersionSnapshot(\n ctx: ContentContext,\n entityId: string,\n data: Record<string, unknown>,\n delta: Record<string, unknown> | null,\n isAutosave: boolean,\n locale = DEFAULT_LOCALE_SENTINEL,\n user?: { id: string; name?: string },\n): Promise<number | undefined> {\n // All current callers pass a pre-validated locale (or the sentinel).\n // Defensive check at the boundary so future callers — or anyone who\n // exposes this helper publicly — still get the validation.\n validateLocale(locale)\n const client = await getVersionsClient(ctx)\n if (!client) return\n\n // Get the next version number for this locale (max existing + 1).\n const [maxRow] = await client.aggregate<{ maxVersion: number | null }>({\n select: { maxVersion: { fn: 'max', column: 'version' } },\n where: { entityId, locale },\n })\n const version = Number(maxRow?.maxVersion ?? 0) + 1\n const status = (data.status as string) ?? null\n\n // Autosave: update the existing autosave row at the same version\n // number, preserving \"autosaves land on the latest slot\" semantics.\n if (isAutosave) {\n const existing = await client.findOne({ entityId, isAutosave: true, version, locale })\n if (existing) {\n await client.update(\n { id: existing.id as string },\n { data, delta, status, createdAt: new Date() },\n )\n return version\n }\n }\n\n await client.insert({\n entityId,\n version,\n locale,\n data,\n delta,\n status,\n createdBy: user?.id ?? null,\n createdByName: user?.name ?? null,\n isAutosave,\n })\n return version\n}\n\n/**\n * Create a version snapshot for an entity after an external update.\n * Use this when the update was performed by AdminClient directly\n * (e.g. the admin API handler) rather than through ContentClient.update().\n *\n * @param locale - The locale being edited. Use undefined or '_' for the default locale.\n * @param user - The user who made the change (id + name for display).\n */\nexport async function createVersionSnapshot(\n ctx: ContentContext,\n entityId: string,\n currentData: Record<string, unknown>,\n previousData?: Record<string, unknown> | null,\n locale?: string,\n user?: { id: string; name?: string },\n): Promise<void> {\n if (locale !== undefined) validateLocale(locale)\n if (!ctx.versionableOptions) return\n const snapshotLocale = locale ?? DEFAULT_LOCALE_SENTINEL\n const delta = previousData ? calculateDelta(previousData, currentData) : null\n const version = await saveVersionSnapshot(\n ctx,\n entityId,\n currentData,\n delta,\n false,\n snapshotLocale,\n user,\n )\n\n // Update _version on the entity to match (default locale only).\n if (version !== undefined && snapshotLocale === DEFAULT_LOCALE_SENTINEL) {\n await writeVersionColumn(ctx, entityId, version)\n }\n\n await pruneVersions(ctx, entityId, snapshotLocale)\n}\n\n/**\n * Write the `_version` column on the entity row.\n *\n * KNOWN EXCEPTION: this bypasses AdminClient and goes straight to the\n * entity pgTable via `schemaRegistry` + direct Drizzle. Using\n * `admin.update()` would retrigger the `versionable.beforeUpdate` hook\n * and loop indefinitely. The only ContentClient path that still writes\n * to an entity table directly.\n */\nexport async function writeVersionColumn(\n ctx: ContentContext,\n entityId: string,\n version: number,\n): Promise<void> {\n const table = schemaRegistry.get(ctx.entity.name)\n if (!table) return\n await ctx.db.update(table).set({ _version: version }).where(eq(table.id, entityId))\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@murumets-ee/content",
3
- "version": "0.107.0",
3
+ "version": "0.108.1",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,21 +64,21 @@
64
64
  "drizzle-orm": "^0.45.2",
65
65
  "server-only": "^0.0.1",
66
66
  "zod": "^3.24.1",
67
- "@murumets-ee/blocks": "0.107.0",
68
- "@murumets-ee/core": "0.107.0",
69
- "@murumets-ee/db": "0.107.0",
70
- "@murumets-ee/entity": "0.107.0",
71
- "@murumets-ee/i18n": "0.107.0",
72
- "@murumets-ee/logging": "0.107.0",
73
- "@murumets-ee/queue": "0.107.0",
74
- "@murumets-ee/settings": "0.107.0"
67
+ "@murumets-ee/blocks": "0.108.1",
68
+ "@murumets-ee/core": "0.108.1",
69
+ "@murumets-ee/db": "0.108.1",
70
+ "@murumets-ee/entity": "0.108.1",
71
+ "@murumets-ee/i18n": "0.108.1",
72
+ "@murumets-ee/logging": "0.108.1",
73
+ "@murumets-ee/queue": "0.108.1",
74
+ "@murumets-ee/settings": "0.108.1"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@types/node": "^20.19.42",
78
78
  "tsdown": "^0.22.2",
79
79
  "typescript": "^5.7.3",
80
80
  "vitest": "^2.1.8",
81
- "@murumets-ee/content-api": "0.107.0"
81
+ "@murumets-ee/content-api": "0.108.1"
82
82
  },
83
83
  "typeCoverage": {
84
84
  "atLeast": 99.48
@@ -1,2 +0,0 @@
1
- import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{h as t}from"./translation-provenance-eqhsGAyi.mjs";import{requireSiteFor as n}from"@murumets-ee/core";import"server-only";var r=e({createContentContext:()=>i,getDraftsClient:()=>a,getLocaleStatusClient:()=>s,getVersionsClient:()=>o});function i(e,t){return{entity:e.entity,admin:e.admin,db:e.admin.getDb(),versionableOptions:t,logger:e.logger}}async function a(e){let{drafts:r}=t(e.entity.name);return r?r.makeClient(e.db,await n(e.entity)):null}async function o(e){let{versions:r}=t(e.entity.name);return r?r.makeClient(e.db,await n(e.entity)):null}async function s(e){let{localeStatus:r}=t(e.entity.name);return r?r.makeClient(e.db,await n(e.entity)):null}export{o as a,s as i,i as n,a as r,r as t};
2
- //# sourceMappingURL=context-DtHgyrgb.mjs.map
@@ -1,2 +0,0 @@
1
- import{t as e}from"./versionable-Br_G5VGR.mjs";import{r as t}from"./errors-NRNILhhm.mjs";import{r as n,t as r}from"./locale-entity-CoSq3C77.mjs";import{n as i,t as a}from"./content-config-store-8omQF5p5.mjs";import{r as o}from"./routing-strategy-D6B4ngT9.mjs";import{g as s,h as c,m as l,n as u,p as d}from"./translation-provenance-eqhsGAyi.mjs";import"./locale-utils-B0mFoV47.mjs";import{n as f}from"./document-site-B8tmEqLw.mjs";import{n as p}from"./chrome-entity-names-zYJzP-_U.mjs";import{r as m}from"./detail-template-names-CD1Y9n1U.mjs";import{c as h,i as g,n as _,o as v,r as y,t as b}from"./schema-BYrYzsmO.mjs";import{l as x,t as S,u as C}from"./schema-LlK6EkOJ.mjs";import{r as w,t as T}from"./routes-D-NWP0hK.mjs";import{n as E}from"./locks-5779ugSW.mjs";import{MAX_PATH_DEPTH as ee,buildPartitionCondition as D,hasPathParent as O,isSiteBound as k,resolveEffectiveSlugMode as A,resolvePattern as j}from"@murumets-ee/entity";import{definePlugin as M,getApp as N,getContext as P,getCurrentSite as F,requireRequestSite as I,resolveImplicitSite as L,runAsCli as R,runInEverySite as z,runInSite as B}from"@murumets-ee/core";import{column as V,defineTable as te,schemaRegistry as H}from"@murumets-ee/db";import{and as U,asc as W,eq as G,getTableColumns as ne,inArray as re,sql as ie}from"drizzle-orm";import{createHash as ae}from"node:crypto";import{z as K}from"zod";const oe=Symbol.for(`@murumets-ee/content:locale-status-writer`);async function q(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 se(e){globalThis[oe]=(n,r)=>{let{localeStatus:i}=c(n.name);if(i)return async(n,a,o,s)=>{let c=P()?.availableLocales??e;if(!c.includes(a))throw new t(a,[...c]);await q(i.makeClient(r,s),n,a,o)}}}function ce(){return{async writeDraft({documentType:e,documentId:t,locale:n,dataPatch:r,user:i,tx:a}){let{drafts:o}=c(e);if(!o)return!1;let s=o.makeClient(a,await f(e)),l=new Date,d=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`]});return u({entityName:e,entityId:t,locale:n,userId:i.id,draftData:d.data??r}),!0},async readDraft({documentType:e,documentId:t,locale:n,db:r}){let{drafts:i}=c(e);if(!i)return null;let a=await i.makeClient(r,await f(e)).findOne({entityId:t,locale:n});return a?{data:a.data??{},createdBy:a.createdBy??null,createdByName:a.createdByName??null,updatedAt:a.updatedAt??null}:null},async readLocaleStatus({documentType:e,documentId:t,locale:n,db:r}){let{localeStatus:i}=c(e);if(!i)return null;let a=await i.makeClient(r,await f(e)).findOne({entityId:t,locale:n});return a?a.status===`published`?`published`:`draft`:null},async hasAnyDraft({documentType:e,documentId:t,db:n}){let{drafts:r}=c(e);return r?r.makeClient(n,await f(e)).exists({entityId:t}):!1},async hasAnyLocaleActivity({documentType:e,documentId:t,db:n}){let{localeStatus:r}=c(e),i=await f(e);if(r&&await r.makeClient(n,i).exists({entityId:t}))return!0;let a=H.get(`${e}_translations`);if(a){let r=a.entityId;if(r!==void 0){let o=i===void 0?G(r,t):U(G(r,t),D(a,{site:i},e));if((await n.select({one:ie`1`}).from(a).where(o).limit(1)).length>0)return!0}}return!1}}}function le(e){let{defaultLocale:t}=a();return e===`_`||e===t?{defaultLocale:t}:{locale:e,defaultLocale:t}}function ue(e){let{defaultLocale:t}=a();return e===`_`||e===t?`_`:e}async function de(e,t,n,r){let{drafts:i}=c(e);if(!i)return;let a=await i.makeClient(r,await f(e)).findOne({entityId:t,locale:n});return a?a.data??void 0:void 0}function fe(){return{async snapshotCurrentState({documentType:t,documentId:n,locale:r,user:i}){let a=N(),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=ue(r),d=await c.findById(n,le(u));if(!d)return!1;let f=await de(t,n,u,l),p=f?{...d,...f}:{...d},[{createContentContext:m},{createVersionSnapshot:h}]=await Promise.all([import(`./context-DtHgyrgb.mjs`).then(e=>e.t),import(`./versions-B0_9TtZ0.mjs`).then(e=>e.c)]);return await h(m({admin:c,entity:o},s),n,p,d,u,{id:i.id,...i.name==null?{}:{name:i.name}}),!0}}}async function pe(e,t,n){let r=ne(b),i=r[y],a=r.entityType,o=r.entityId;if(!i||!a||!o)throw Error(`toolkit_paths is missing '${y}'/entity columns`);let s=await e.selectDistinct({site:i}).from(b).where(U(G(a,t),G(o,n))).limit(2);if(s.length>1)return null;let c=s[0]?.site;return typeof c==`string`?c:void 0}const me=Symbol.for(`@murumets-ee/content:path-writers-factory`),J=Symbol.for(`@murumets-ee/content:cascade-trigger-factory`);function he(e){globalThis[J]=e}function ge(){let e=globalThis[J];return typeof e==`function`?e:void 0}function _e(e,t){return async r=>{let i=await ve(e,t,r);if(i===null)return;let a=_.makeClient(e.db.readWrite,i),o=F();if(o!==void 0&&o!==i&&!(r.action===`delete`||r.action===`release`)){e.logger.warn({entity:r.entityName,id:r.id,site:i,requestSite:o},"toolkit_paths: a cascade reached a document on another site — its path is left for `lumi paths:rebuild` on that site rather than recomputed from this request");return}return o===void 0&&r.viaCascade===!0&&k(t)?B(i,()=>n(a,r)):n(a,r)};async function n(n,r){if(r.action===`delete`){await n.deleteMany({entityType:r.entityName,entityId:r.id});return}if(r.action===`release`){await n.deleteMany({entityType:r.entityName,entityId:r.id,isCanonical:!0});return}let i=a(),o=i.defaultLocale,s=i.reservedPathPrefixes??[],c=r.locale?[r.locale]:i.locales.map(e=>e.code),l=r.viaCascade===!0?e.getSystemClient(t):e.getClient(t),u=async n=>{if(!O(t))return;let i=ge();if(i)try{await i(e,t,r.id,n)}catch(t){e.logger.warn({err:t,entity:r.entityName,id:r.id,locale:n},`toolkit_paths: descendant-cascade trigger threw — the entity write already committed, so tolerating and continuing (paths are rebuildable, D005)`)}};for(let i of c){let a=i===r.locale?r.row??null:await l.findById(r.id,{locale:i,defaultLocale:o});if(!a){await v({pathsClient:n,entityType:r.entityName,entityId:r.id,locale:i,path:null,skipReason:`row-unreadable`,reservedPathPrefixes:s,logger:e.logger});continue}let c=await x({entity:t,row:a,locale:i,defaultLocale:o,pathsClient:n}),d=await v({pathsClient:n,entityType:r.entityName,entityId:r.id,locale:i,path:c.path,...c.skipReason!==void 0&&{skipReason:c.skipReason},reservedPathPrefixes:s,logger:e.logger});(d.outcome===`renamed`||r.action===`restore`&&d.outcome===`created`)&&await u(i)}}}async function ve(e,t,n){if(n.viaCascade===!0&&k(t)){let t=await pe(e.db.readWrite,n.entityName,n.id);if(t===null)return e.logger.warn({entity:n.entityName,id:n.id},"toolkit_paths: a document has path rows on SEVERAL sites — refusing to guess which one this cascade maintains; `lumi paths:rebuild` per site repairs it"),null;if(t!==void 0)return t;let r=F();if(r!==void 0)return r;let i=await L(e).catch(()=>void 0);return i===void 0?(e.logger.warn({entity:n.entityName,id:n.id,action:n.action},"toolkit_paths: cascade child has no path rows and the request names no site — nothing to maintain here; `lumi paths:rebuild` recomputes it per site"),null):i}return I(`toolkit_paths`,e)}function ye(e,t){return async n=>{let r=_.makeClient(e.db.readWrite,await I(`toolkit_paths`,e)),i=a(),o=i.defaultLocale,s=i.reservedPathPrefixes??[],c=n.locale??o,l=null;n.id!==void 0&&(l=await e.getClient(t).findById(n.id,{locale:c,defaultLocale:o}));let u=A(n.data.slugMode,l?.slugMode);await C({entity:t,data:n.data,currentRow:l,id:n.id,locale:c,defaultLocale:o,slugMode:u,reservedPathPrefixes:s,pathsClient:r})}}function be(){globalThis[me]=(e,t)=>({pathIndexWriter:_e(e,t),pathConflictGuard:ye(e,t)})}const Y=K.object({entityType:K.string().min(1),parentId:K.string().uuid(),locale:K.string().min(1)}),X=K.object({entity:K.string().min(1).optional(),locale:K.string().min(1).optional(),dryRun:K.boolean()}),Z=K.object({dryRun:K.boolean()});async function xe(e,t,n={}){let{entityType:r,parentId:i,locale:o}=t,s=n.maxItems??2e3,c=n.concurrency??5,l={processed:0,regenerated:0,unchanged:0,skipped:0,failed:0,truncated:!1},u=e=>({phase:e,...l}),d=()=>({entityType:r,parentId:i,locale:o,cap:s,...l}),f=e.entities.get(r);if(!f)return e.logger.warn({entityType:r,parentId:i,locale:o},`paths-cascade: unknown entity type — nothing to regenerate`),n.onProgress?.(u(`done`)),d();if(!O(f))return e.logger.warn({entityType:r,parentId:i,locale:o},`paths-cascade: entity is not a pathParent() hierarchy — nothing to regenerate`),n.onProgress?.(u(`done`)),d();let p=a(),m=p.defaultLocale,g=p.reservedPathPrefixes??[],y=_.makeClient(e.db.readWrite,await I(`toolkit_paths`,e)),b=e.getClient(f),S=b.getTable().parent,C=async t=>{let n=t.id;try{let i=await x({entity:f,row:t,locale:o,defaultLocale:m,pathsClient:y});switch((await v({pathsClient:y,entityType:r,entityId:n,locale:o,path:i.path,...i.skipReason!==void 0&&{skipReason:i.skipReason},reservedPathPrefixes:g,logger:e.logger})).outcome){case`created`:case`renamed`:l.regenerated+=1;break;case`unchanged`:l.unchanged+=1;break;default:l.skipped+=1;break}}catch(t){l.failed+=1,e.logger.warn({err:t,entityType:r,id:n,locale:o},`paths-cascade: failed to regenerate one descendant path — continuing (per-item isolation)`)}},w=[i];for(;w.length>0;){let e=s-l.processed;if(e<=0){l.truncated=!0;break}let t=await b.findMany({where:re(S,w),locale:o,defaultLocale:m,limit:e+1});if(t.length===0)break;let r=t;if(t.length>e&&(r=t.slice(0,e),l.truncated=!0),await h(r,c,C),l.processed+=r.length,n.onProgress?.(u(`walking`)),l.truncated)break;w=r.map(e=>e.id)}return l.truncated&&e.logger.warn({entityType:r,parentId:i,locale:o,processed:l.processed,cap:s},`paths-cascade: descendant subtree exceeded the cap — deeper/remaining descendants were NOT regenerated. Run \`lumi paths:rebuild --entity ${r}\` for guaranteed full coverage.`),n.onProgress?.(u(`done`)),d()}async function Se(e,t,n={}){let r=n.pageSize??200,i=n.concurrency??5,o=n.sampleCap??25,s=t.dryRun,c=a(),l=c.defaultLocale,u=c.reservedPathPrefixes??[],d=c.locales.map(e=>e.code),f=t.locale?d.filter(e=>e===t.locale):d,p=[...e.entities.values()].filter(e=>!!e.frontend?.path&&(t.entity===void 0||e.name===t.entity)),m=t.entity!==void 0&&p.length===0,g=t.locale!==void 0&&f.length===0;m&&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)`),g&&e.logger.warn({locale:t.locale,configuredLocales:d,dryRun:s},`paths-rebuild: --locale matches no configured locale — nothing to rebuild (likely a typo in the locale code)`);let y=m||g?{entity:m,locale:g}:void 0,b=p.some(e=>O(e)),S=s&&b?`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,C=_.makeClient(e.db.readWrite,await I(`toolkit_paths`,e)),w={scanned:0,changed:0,applied:0,failed:0},T=new Map,E={missing:0,conflicts:0,unchanged:0,skipped:0},D=(e,t)=>{let n=T.get(e);n!==t&&(n!==void 0&&n!==`resolved`&&--E[n],T.set(e,t),t!==`resolved`&&(E[t]+=1))},k=[],A=!1,j=e=>{k.length<o?k.push(e):A=!0},M=0,N=e=>({phase:e,dryRun:s,scope:{entity:t.entity??null,locale:t.locale??null},scanned:w.scanned,changed:w.changed,applied:w.applied,failed:w.failed,missing:E.missing,conflicts:E.conflicts,unchanged:E.unchanged,skipped:E.skipped,passes:M,sample:k,sampleTruncated:A,...S===void 0?{}:{hierarchyNote:S},...y===void 0?{}:{unmatchedScope:y}}),P=async(t,n,r,i)=>{let a=r.id,o=!1;try{let c=await x({entity:t,row:r,locale:i,defaultLocale:l,pathsClient:C}),d=await v({pathsClient:C,entityType:n,entityId:a,locale:i,path:c.path,...c.skipReason!==void 0&&{skipReason:c.skipReason},suppressNoteworthy:[`parent-not-indexed`],reservedPathPrefixes:u,logger:e.logger,dryRun:s}),f=`${n}:${a}:${i}`;switch(d.outcome){case`unchanged`:D(f,`unchanged`);break;case`skipped-null`:case`skipped-reserved`:D(f,`skipped`);break;case`conflict`:D(f,`conflicts`),j({kind:`conflict`,entityType:n,entityId:a,locale:i,oldPath:d.oldPath,newPath:d.newPath,...d.conflictWith?{conflictWith:d.conflictWith.entityId}:{}});break;case`created`:s?D(f,`missing`):(w.applied+=1,o=!0,D(f,`resolved`)),j({kind:`missing`,entityType:n,entityId:a,locale:i,oldPath:null,newPath:d.newPath});break;case`renamed`:w.changed+=1,s||(w.applied+=1,o=!0),D(f,`resolved`),j({kind:`changed`,entityType:n,entityId:a,locale:i,oldPath:d.oldPath,newPath:d.newPath});break}}catch(t){w.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},F=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 f){let u;for(;;){let d=await t.findMany({locale:e,defaultLocale:l,limit:r,orderBy:W(c),...u===void 0?{}:{cursor:{field:`id`,value:u,direction:`asc`}}});if(d.length===0)break;await h(d,i,async t=>{await P(o,s,t,e)&&(a+=1)}),w.scanned+=d.length;let f=d[d.length-1];if(f&&(u=f.id),n.onProgress?.(N(`scanning`)),d.length<r)break}}}return a},L=async t=>{let r=[];for(let[n,i]of T){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 h(r,i,async t=>{let n=await e.getClient(t.entity).findById(t.entityId,{locale:t.locale,defaultLocale:l});n!==null&&(w.scanned+=1,await P(t.entity,t.entityType,n,t.locale)&&(a+=1))}),n.onProgress?.(N(`scanning`)),a},R=p.filter(e=>O(e)),z=new Set(R.map(e=>e.name)),B=s||!b?1:ee;for(let e=0;e<B;e++){M=e+1;let t=await F(e===0?p:R);if(e>0&&(t+=await L(z)),t===0)break}A&&e.logger.warn({scanned:w.scanned,changed:w.changed,missing:E.missing,conflicts:E.conflicts,sampleCap:o},`paths-rebuild: more diff entries than the progress sample can hold — see the aggregate counts; the stored sample is truncated`);let V=N(`done`);n.onProgress?.(V);let{phase:te,...H}=V;return H}async function Ce(e,t,n={}){let r=n.maxRows??1e4,i=n.batchSize??500,a={dryRun:t.dryRun,orphans:[],found:0,deleted:0,truncated:!1,refused:!1},o=new Set([...e.entities.values()].filter(e=>!!e.frontend?.path).map(e=>e.name));if(o.size===0)return e.logger.warn({dryRun:t.dryRun},`paths-prune: no entity declares frontend.path — refusing to treat every row as an orphan`),a.refused=!0,a;let s=_.makeClient(e.db.readWrite,await I(`toolkit_paths`,e)),c=(await s.distinct(`entityType`)).filter(e=>!o.has(e));for(let e of c){let t=await s.count({entityType:e});t>0&&a.orphans.push({entityType:e,rows:t}),a.found+=t}if(t.dryRun)return a;for(let{entityType:e}of a.orphans)for(;a.deleted<r;){let t=await s.findMany({where:{entityType:e},limit:Math.min(i,r-a.deleted)});if(t.length===0)break;let n=await s.deleteMany({id:{in:t.map(e=>e.id)}});a.deleted+=n.length}return a.truncated=a.deleted<a.found&&a.deleted>=r,a.truncated&&e.logger.warn({deleted:a.deleted,found:a.found,maxRows:r},`paths-prune: row cap reached — run again to continue`),a}async function we(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:Y}),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:X});n(t({name:`content:paths-prune`,description:`Delete toolkit_paths rows of entity types that are no longer routable (lost frontend.path or unregistered). dryRun reports them without deleting. On-demand (lumi paths:rebuild --orphans).`,schema:Z}),async t=>{let n={dryRun:Z.parse(t.payload).dryRun,orphans:[],found:0,deleted:0,truncated:!1,refused:!1};await R(()=>z(async()=>{let r=await Ce(e,Z.parse(t.payload));for(let e of r.orphans){let t=n.orphans.find(t=>t.entityType===e.entityType);t?t.rows+=e.rows:n.orphans.push({...e})}n.found+=r.found,n.deleted+=r.deleted,n.truncated||=r.truncated,n.refused||=r.refused,t.updateProgress(n),e.logger.info(r,`paths-prune: sweep complete`)},{app:e,label:`paths-prune`}))}),n(i,async t=>{await R(async()=>{let n=await xe(e,Y.parse(t.payload),{onProgress:e=>t.updateProgress(e)});e.logger.info(n,`paths-cascade: descendant regeneration complete`)})}),n(a,async t=>{await R(()=>z(async()=>{let n=await Se(e,X.parse(t.payload),{onProgress:e=>t.updateProgress({...e,site:F()??null})});e.logger.info({dryRun:n.dryRun,scanned:n.scanned,changed:n.changed,missing:n.missing,conflicts:n.conflicts,applied:n.applied,failed:n.failed,passes:n.passes,...n.hierarchyNote===void 0?{}:{hierarchyNote:n.hierarchyNote}},`paths-rebuild: sweep complete`)},{app:e,label:`paths-rebuild`}))}),he(async(e,t,n,a)=>{await new r({app:e,logger:e.logger}).enqueue(i,{entityType:t.name,parentId:n,locale:a})})}const Q=te({name:`toolkit_path_patterns`,columns:{id:V.uuid({primaryKey:!0,defaultRandom:!0}),entityType:V.varchar({length:100,notNull:!0,pgName:`entity_type`}),locale:V.varchar({length:10,notNull:!0}),patternHash:V.varchar({length:64,notNull:!0,pgName:`pattern_hash`}),updatedAt:V.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityType`,`locale`]}]}),Te=Q.table;function Ee(e,t,n){let r=j(e,t,n);if(r===null)return null;let i=typeof r==`function`?r.toString():JSON.stringify(r);return ae(`sha256`).update(i).digest(`hex`)}async function De(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}}))}}}const Oe=async(e,t,n)=>{if(t.length===0)return new Map;let[{buildEnumerateDeps:r},{getCanonicalPaths:i}]=await Promise.all([import(`./resolve-context-DklN53BH.mjs`).then(e=>e.u),import(`./enumerate-CYHJKQz6.mjs`).then(e=>e.n)]),a=r(N()),o=await a.filterPublished(e,t,n);return o.size===0?new Map:i(e,[...o],n,a)},ke=async(e,t)=>{if(e.length===0)return new Set;let{buildEnumerateDeps:n}=await import(`./resolve-context-DklN53BH.mjs`).then(e=>e.u),r=n(N()),i=[...new Set(e)],a=i.slice(0,48);i.length>a.length&&N().logger.warn({paths:i.length,max:48},`content: publishedPathsAmong was asked for more paths than it reads; the rest count as unpublished`);let o=await r.pathsClient.findMany({where:{locale:t,isCanonical:!0,path:{in:a}},limit:a.length}),s=new Map;for(let e of o){let t=s.get(e.entityType);t===void 0&&(t=new Map,s.set(e.entityType,t)),t.set(e.entityId,e.path)}let c=new Set;for(let[e,n]of s){let i=await r.filterPublished(e,[...n.keys()],t);for(let[e,t]of n)i.has(e)&&c.add(t)}return c},Ae=async e=>{let[{buildBreadcrumbDeps:t},{resolveBreadcrumbTrail:n}]=await Promise.all([import(`./resolve-context-DklN53BH.mjs`).then(e=>e.u),import(`./breadcrumbs-BqSuMNac.mjs`).then(e=>e.t)]);return(await n(e,t(N())))?.steps};function $(e){return g(e,{name:`${e.name}_drafts`,columns:{id:V.uuid({primaryKey:!0,defaultRandom:!0}),entityId:V.uuid({notNull:!0,pgName:`entity_id`}),locale:V.varchar({length:10,notNull:!0,default:`_`}),data:V.jsonb({notNull:!0}),createdBy:V.varchar({length:255,notNull:!0,pgName:`created_by`}),createdByName:V.varchar({length:255,pgName:`created_by_name`}),createdAt:V.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),updatedAt:V.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityId`,`locale`]}]})}function je(e){return g(e,{name:`${e.name}_locale_status`,columns:{id:V.uuid({primaryKey:!0,defaultRandom:!0}),entityId:V.uuid({notNull:!0,pgName:`entity_id`}),locale:V.varchar({length:10,notNull:!0}),status:V.varchar({length:20,notNull:!0,default:`draft`}),publishedAt:V.timestamp({withTimezone:!0,pgName:`published_at`})},unique:[{on:[`entityId`,`locale`]}]})}function Me(e){return g(e,{name:`${e.name}_versions`,columns:{id:V.uuid({primaryKey:!0,defaultRandom:!0}),entityId:V.uuid({notNull:!0,pgName:`entity_id`}),version:V.integer({notNull:!0}),locale:V.varchar({length:10,notNull:!0,default:`_`}),data:V.jsonb({notNull:!0}),delta:V.jsonb(),status:V.varchar({length:20}),createdBy:V.varchar({length:255,pgName:`created_by`}),createdByName:V.varchar({length:255,pgName:`created_by_name`}),createdAt:V.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),isAutosave:V.boolean({notNull:!0,default:!1,pgName:`is_autosave`})},unique:[{on:[`entityId`,`version`,`locale`]}]})}function Ne(e){if(!e.locales||e.locales.length===0)throw Error(`content() requires at least one locale`);if(!e.defaultLocale)throw Error(`content() requires a defaultLocale`);if(!e.locales.some(t=>t.code===e.defaultLocale))throw Error(`defaultLocale "${e.defaultLocale}" not found in locales: [${e.locales.map(e=>e.code).join(`, `)}]`);if(e.reservedPathPrefixes){for(let t of e.reservedPathPrefixes)if(typeof t!=`string`||!t.startsWith(`/`))throw Error(`content(): reservedPathPrefixes entries must be absolute paths starting with '/', got ${JSON.stringify(t)}`)}let t=e.autoTrash??!0,a=t===!1?void 0:{publishable:!0,...typeof t==`object`&&t.exclude?{exclude:t.exclude}:{}};return M({name:`@murumets-ee/content`,contributes:{"blocks.documentDraftStore":ce(),"blocks.documentVersionStore":fe(),"blocks.publishedPathResolver":Oe,"entity.writeListeners":d,"blocks.publishedPathsAmong":ke,"blocks.breadcrumbTrail":Ae,"sites.baseLocale":async()=>(await import(`./locale-registry-COsJrVZl.mjs`).then(e=>e.n)).resolveInstallBaseLocale(),...a?{"trash.autoEnable":a}:{},...e.reservedPathPrefixes?{"routing.reservedPathPrefixes":e.reservedPathPrefixes}:{}},shared:{settings:[n],features:[{resource:`redirect`,actions:[`manage`],defaultRoles:[`admin`],description:`Create, edit, and delete manual URL redirects (toolkit_redirects).`},{resource:`path`,actions:[`resolve`],defaultRoles:[`admin`,`editor`],description:`Resolve a site URL path to the CMS document that serves it.`}]},server:{entities:[r,o],tables:{toolkitContentLocks:E,toolkitPaths:b,toolkitPathPatterns:Te,toolkitRedirects:S,toolkitTranslationProvenance:l},routes:[...T(),...w()],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:p,bootstrap:!0,run:async()=>{let{seedSiteChrome:e}=await import(`@murumets-ee/content/seed`);return await e()}},{name:`content:detail-templates`,description:`Create the DRAFT detail template for each entity drawn by one (block-library D009), so its page layout can be edited on the canvas. Until a template is published its rows keep the plain fallback page. Idempotent — an existing template is left alone.`,available:m,bootstrap:!0,run:async()=>{let{seedDetailTemplates:e}=await import(`@murumets-ee/content/seed`);return await e()}}],init:async t=>{i(e);let n=0,r=0,a=0;for(let[e,i]of t.entities){if(i.behaviors?.some(e=>e.name===`versionable`)){let t=`${e}_versions`;if(!H.has(t)){let n=Me(i);H.register(t,n.table),s(e,{versions:n})}n++}let t=i.behaviors?.some(e=>e.name===`publishable`),o=i.allFields,c=Object.values(o).some(e=>e.translatable);if(t&&c){let t=`${e}_locale_status`;if(!H.has(t)){let n=je(i);H.register(t,n.table),s(e,{localeStatus:n})}r++}if(t){let t=`${e}_drafts`;if(!H.has(t)){let n=$(i);H.register(t,n.table),s(e,{drafts:n})}a++}}se(e.locales.map(e=>e.code)),be();try{await we(t)}catch(e){t.logger.warn({err:e},"content: failed to register routing path jobs — descendant cascade + `lumi paths:rebuild` will be unavailable (path writes still succeed)")}t.logger.info({locales:e.locales.map(e=>e.code),defaultLocale:e.defaultLocale,versionableEntities:n,localeStatusEntities:r,draftEntities:a},`Content plugin initialized`)},onReady:async e=>{try{await De(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{q as n,Ne as t};
2
- //# sourceMappingURL=plugin-CvPWz399.mjs.map
@@ -1,2 +0,0 @@
1
- import{t as e}from"./content-config-store-8omQF5p5.mjs";import{n as t,s as n}from"./schema-BYrYzsmO.mjs";import{d as r,n as i,s as a}from"./schema-LlK6EkOJ.mjs";import{matchReservedPrefix as o}from"@murumets-ee/entity";import{combineAdminRoutes as s,defineAdminRoute as c,getContext as l,requireRequestSite as u,safeAudit as d}from"@murumets-ee/core";import{and as f,eq as p}from"drizzle-orm";import{z as m}from"zod";import{entityRefs as h}from"@murumets-ee/entity/refs";const g=`path:resolve`,ee=[`admin`,`editor`];function _(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}const v={kind:`none`};function y(e){return e===null||e.length===0?{error:`query param 'path' is required`}:e.length>2048?{error:`query param 'path' is too long`}:e.startsWith(`/`)?e.startsWith(`//`)?{error:`query param 'path' must not be protocol-relative`}:e.includes(`?`)||e.includes(`#`)?{error:`query param 'path' must not include a query or fragment`}:{path:e}:{error:`query param 'path' must start with '/'`}}function b(t,n){let r=e(),i=t??n??r.defaultLocale;return(l()?.availableLocales??r.locales.map(e=>e.code)).includes(i)?{locale:i}:{error:`query param 'locale' is not a content locale`}}const x=async(e,t)=>{let n=new URL(e.url).searchParams,r=y(n.get(`path`));if(`error`in r)return _({error:r.error},400);let i=b(n.get(`locale`),t.locale??t.defaultLocale);if(`error`in i)return _({error:i.error},400);let{locale:o}=i,{buildResolveDeps:s}=await import(`./resolve-context-DklN53BH.mjs`).then(e=>e.u),c=s(t.app),l=(e,n)=>t.checkPermission(e,`view`)?{kind:`document`,entityType:e,id:n}:v,u=await a(r.path,o,c);if(u.kind===`document`)return _(l(u.entityType,u.id));if(u.kind===`none`)return _(v);if(!(u.to.startsWith(`/`)&&!u.to.startsWith(`//`)))return _({kind:`redirect`,to:u.to});let d=await a(u.to,o,c);return _(d.kind===`document`?l(d.entityType,d.id):v)};function S(){return s([c({prefix:`paths`,path:`resolve`,method:`GET`,permission:g,defaultRoles:ee,description:`Resolve a site URL path to the CMS document that serves it (editor canvas link navigation).`,tokens:`accept`,handler:x})])}var C=class extends Error{fromPath;prefix;constructor(e,t){super(`Redirect source "${e}" falls under the reserved prefix "${t}" and cannot be used — that path range is owned by the framework.`),this.name=`RedirectReservedPrefixError`,this.fromPath=e,this.prefix=t}},w=class extends Error{fromPath;locale;conflictingLocale;constructor(e,t,n){super(`Redirect source "${e}" already serves content in locale "${n}" (it is a live canonical URL). Choose a different source path, or unpublish/rename the content first.`),this.name=`RedirectServesContentError`,this.fromPath=e,this.locale=t,this.conflictingLocale=n}},T=class extends Error{entity;constructor(e){super(`Redirect target references entity type "${e}", which is not a registered entity. Check the entity name.`),this.name=`RedirectUnknownEntityError`,this.entity=e}},E=class extends Error{entity;id;constructor(e,t){super(`Redirect target ${e} "${t}" does not exist. Redirect to a document that exists, or use a URL/path target instead.`),this.name=`RedirectTargetNotFoundError`,this.entity=e,this.id=t}},D=class extends Error{fromPath;locale;constructor(e,t){super(`Redirect source "${e}"${t===null?``:` (locale "${t}")`} resolves back to itself — a redirect cannot point at its own source path. Choose a different destination.`),this.name=`RedirectSelfReferenceError`,this.fromPath=e,this.locale=t}};const O=`toolkit_redirects`;async function k(e,t,n){n.kind===`entity`&&await e.insert(h).values({sourceEntity:O,sourceId:t,sourceField:`target`,targetEntity:n.entity,targetId:n.id,relationKind:`link`}).onConflictDoNothing()}async function A(e,t){await e.delete(h).where(f(p(h.sourceEntity,O),p(h.sourceId,t)))}const j=m.discriminatedUnion(`kind`,[m.object({kind:m.literal(`entity`),entity:m.string().min(1),id:m.string().uuid()}),m.object({kind:m.literal(`url`),url:m.string().max(2048).url()}),m.object({kind:m.literal(`path`),path:m.string().min(1).max(2048).startsWith(`/`)})]);function M(e){return r(e)}function N(e){let t=M(e.fromPath),n=o(t,e.reservedPathPrefixes);if(n!==void 0)throw new C(t,n)}async function te(e){let t=M(e.fromPath);if(e.locale!==null){let n=await e.pathsClient.findOne({path:t,isCanonical:!0,locale:e.locale});if(n)throw new w(t,e.locale,n.locale);return}let n=await e.pathsClient.findOne({path:t,isCanonical:!0,locale:{in:[...e.configuredLocales]}});if(n)throw new w(t,null,n.locale)}async function ne(e){if(e.target.kind!==`path`)return e.target;let t=M(e.target.path),n=e.excludeId===void 0?{}:{id:{ne:e.excludeId}};if(e.locale!==null){let r=await e.redirectsClient.findOne({fromPath:t,locale:e.locale,...n});if(r)return r.target}let r=await e.redirectsClient.findOne({fromPath:t,locale:null,...n});return r?r.target:{kind:`path`,path:t}}async function P(e){if(e.target.kind!==`entity`)return;let{entity:t,id:n}=e.target,r=e.resolveClient(t);if(r===void 0)throw new T(t);if(await r.findById(n)==null)throw new E(t,n)}function F(){return u(`toolkit_redirects`)}const I=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,L=/^[a-zA-Z0-9:_.-]{1,100}$/,R=`redirect:manage`,z=[`admin`];function B(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function V(e,t){return B({error:e},t)}async function H(e,t){let n;try{n=await e.json()}catch{return{response:V(`Invalid JSON body`,400)}}let r=t.safeParse(n);if(!r.success){let e=r.error.issues[0];return{response:V(e?`${e.path.join(`.`)||`body`}: ${e.message}`:`Invalid body`,400)}}return{data:r.data}}function U(e,t){if(e===null||!/^\d+$/.test(e))return t;let n=Number(e);return Number.isSafeInteger(n)?n:t}function W(e){return e.name===void 0?{userId:e.id}:{userId:e.id,userName:e.name}}async function G(){let{getApp:e}=await import(`@murumets-ee/core`);return e()}async function K(){return(await G()).db.readWrite}async function q(){let e=await G();return t=>{let n=e.entities.get(t);return n?e.getClient(n):void 0}}function J(e){return e}const Y=m.union([m.literal(301),m.literal(302)]),X=m.string().min(1).max(2048).startsWith(`/`),re=m.object({locale:m.string().min(1).nullable(),fromPath:X,target:j,status:Y.default(301)}),ie=m.object({locale:m.string().min(1).nullable().optional(),fromPath:X.optional(),target:j.optional(),status:Y.optional()});function Z(t){return t===null||e().locales.map(e=>e.code).includes(t)?null:V(`Unknown locale "${t}" — not one of the configured content locales.`,400)}function Q(e){if(e instanceof C)return B({error:e.message,fromPath:e.fromPath,prefix:e.prefix},409);if(e instanceof w)return B({error:e.message,fromPath:e.fromPath,locale:e.locale,conflictingLocale:e.conflictingLocale},409);if(e instanceof D)return B({error:e.message,fromPath:e.fromPath,locale:e.locale},409);if(e instanceof T)return B({error:e.message,entity:e.entity},400);if(e instanceof E)return B({error:e.message,entity:e.entity,id:e.id},400);if(n(e))return B({error:`A redirect already exists for this locale + source path.`},409);throw e}async function $(n){let r=e(),a=r.reservedPathPrefixes??[],o=r.locales.map(e=>e.code),s=M(n.fromPath);N({fromPath:s,reservedPathPrefixes:a}),await P({target:n.target,resolveClient:n.resolveClient});let c=await K(),l=await F();return c.transaction(async e=>{let r=J(e),a=i.makeClient(r,l),c=t.makeClient(r,l);await te({fromPath:s,locale:n.locale,configuredLocales:o,pathsClient:c});let u=await ne({target:n.target,locale:n.locale,redirectsClient:a,...n.id===null?{}:{excludeId:n.id}});if(u.kind===`path`&&M(u.path)===s)throw new D(s,n.locale);if(n.id===null){let e=await a.insert({locale:n.locale,fromPath:s,target:u,status:n.status,createdBy:n.userId});return await k(r,e.id,u),e}let d=await a.update({id:n.id},{locale:n.locale,fromPath:s,target:u,status:n.status,updatedAt:new Date});return d?(await A(r,n.id),await k(r,n.id,u),d):null})}const ae=async e=>{let t=new URL(e.url),n=t.searchParams.get(`locale`),r=Math.min(Math.max(U(t.searchParams.get(`limit`),50),1),200),a=Math.min(U(t.searchParams.get(`offset`),0),1e5),o;if(n!==null&&n!==``){let e=Z(n);if(e)return e;o=n}let s=i.makeClient(await K(),await F());return B({items:o===void 0?await s.findMany({orderBy:[{column:`createdAt`,dir:`desc`}],limit:r,offset:a}):await s.findMany({where:{locale:o},orderBy:[{column:`createdAt`,dir:`desc`}],limit:r,offset:a}),total:o===void 0?await s.count():await s.count({locale:o})})},oe=async(e,t)=>{let n=await H(e,re);if(`response`in n)return n.response;let r=n.data,i=Z(r.locale);if(i)return i;try{let e=await q(),n=await $({id:null,locale:r.locale,fromPath:r.fromPath,target:r.target,status:r.status,userId:t.user.id,resolveClient:e});return n?(d(t,{action:`redirect.created`,entityType:`redirect`,entityId:n.id,...W(t.user),metadata:{locale:n.locale,fromPath:n.fromPath,targetKind:n.target.kind}}),B({item:n},201)):V(`Failed to create redirect`,500)}catch(e){return Q(e)}},se=async(e,t)=>{let n=t.segments[0];if(!n||!I.test(n))return V(`Invalid id`,400);let r=await H(e,ie);if(`response`in r)return r.response;let a=r.data,o=await i.makeClient(await K(),await F()).findOne({id:n});if(!o)return V(`Not found`,404);let s=a.locale===void 0?o.locale:a.locale,c=a.fromPath===void 0?o.fromPath:a.fromPath,l=a.target===void 0?o.target:a.target,u=a.status===void 0?o.status:a.status,f=Z(s);if(f)return f;try{let e=await q(),r=await $({id:n,locale:s,fromPath:c,target:l,status:u,userId:t.user.id,resolveClient:e});return r?(d(t,{action:`redirect.updated`,entityType:`redirect`,entityId:r.id,...W(t.user),metadata:{locale:r.locale,fromPath:r.fromPath,targetKind:r.target.kind}}),B({item:r})):V(`Not found`,404)}catch(e){return Q(e)}},ce=async(e,t)=>{let n=t.segments[0];if(!n||!I.test(n))return V(`Invalid id`,400);let r=await K(),a=await F(),o=await r.transaction(async e=>{let t=J(e),r=await i.makeClient(t,a).delete({id:n});return r&&await A(t,n),r});return o?(d(t,{action:`redirect.deleted`,entityType:`redirect`,entityId:n,...W(t.user),metadata:{locale:o.locale,fromPath:o.fromPath}}),B({ok:!0})):V(`Not found`,404)},le=async e=>{let n=new URL(e.url),r=n.searchParams.get(`entityType`),i=n.searchParams.get(`entityId`);if(!r||!L.test(r))return V(`entityType is required`,400);if(!i||!I.test(i))return V(`entityId is required and must be a UUID`,400);let a=n.searchParams.get(`locale`),o;if(a!==null&&a!==``){let e=Z(a);if(e)return e;o=a}return B({items:await t.makeClient(await K(),await F()).findMany({where:{entityType:r,entityId:i,isCanonical:!1,...o!==void 0&&{locale:o}},orderBy:[{column:`updatedAt`,dir:`desc`}],limit:100})})};function ue(){return s([c({prefix:`redirects`,path:``,method:`GET`,permission:R,defaultRoles:z,description:`List manual URL redirects (paginated, optional locale filter).`,tokens:`accept`,handler:ae}),c({prefix:`redirects`,path:`history`,method:`GET`,permission:R,defaultRoles:z,description:`Read-only automatic-redirect (rename) history for one document.`,tokens:`accept`,handler:le}),c({prefix:`redirects`,path:``,method:`POST`,permission:R,defaultRoles:z,description:`Create a manual URL redirect.`,tokens:`accept`,handler:oe}),c({prefix:`redirects`,path:``,method:`PATCH`,matchAnyPath:!0,permission:R,defaultRoles:z,description:`Update a manual URL redirect by id.`,tokens:`accept`,handler:se}),c({prefix:`redirects`,path:``,method:`DELETE`,matchAnyPath:!0,permission:R,defaultRoles:z,description:`Delete a manual URL redirect by id.`,tokens:`accept`,confirm:!0,handler:ce})])}export{g as n,S as r,ue as t};
2
- //# sourceMappingURL=routes-D-NWP0hK.mjs.map