@murumets-ee/content 0.87.0 → 0.89.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/admin.d.mts.map +1 -1
  2. package/dist/admin.mjs +1 -1
  3. package/dist/block-resolver.mjs +1 -1
  4. package/dist/client.d.mts +1 -1
  5. package/dist/client.mjs +1 -1
  6. package/dist/client.mjs.map +1 -1
  7. package/dist/{enumerate-CBqu63Kp.mjs → enumerate-CYHJKQz6.mjs} +2 -2
  8. package/dist/{enumerate-CBqu63Kp.mjs.map → enumerate-CYHJKQz6.mjs.map} +1 -1
  9. package/dist/{index-C2G4IJck.d.mts → index-C3VyZLjf.d.mts} +55 -2
  10. package/dist/{index-C2G4IJck.d.mts.map → index-C3VyZLjf.d.mts.map} +1 -1
  11. package/dist/index.d.mts +3 -3
  12. package/dist/index.mjs +1 -1
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/locale-entity-CoSq3C77.mjs +2 -0
  15. package/dist/locale-entity-CoSq3C77.mjs.map +1 -0
  16. package/dist/locale-registry-Bn6e_nM7.mjs +2 -0
  17. package/dist/locale-registry-Bn6e_nM7.mjs.map +1 -0
  18. package/dist/plugin-BmxghXzV.mjs +2 -0
  19. package/dist/plugin-BmxghXzV.mjs.map +1 -0
  20. package/dist/plugin.d.mts +4 -4
  21. package/dist/plugin.mjs +1 -1
  22. package/dist/resolve-context-Cvc1aNJe.mjs +2 -0
  23. package/dist/resolve-context-Cvc1aNJe.mjs.map +1 -0
  24. package/dist/resolve.d.mts +54 -16
  25. package/dist/resolve.d.mts.map +1 -1
  26. package/dist/resolve.mjs +6 -6
  27. package/dist/resolve.mjs.map +1 -1
  28. package/dist/routes-BmNSYyuT.mjs +2 -0
  29. package/dist/{routes-C9I1iZw8.mjs.map → routes-BmNSYyuT.mjs.map} +1 -1
  30. package/dist/routing-strategy-D6B4ngT9.mjs +2 -0
  31. package/dist/routing-strategy-D6B4ngT9.mjs.map +1 -0
  32. package/dist/{routing-strategy-D3Qowm3D.d.mts → routing-strategy-DV7KGarX.d.mts} +4 -4
  33. package/dist/{routing-strategy-D3Qowm3D.d.mts.map → routing-strategy-DV7KGarX.d.mts.map} +1 -1
  34. package/dist/schema-BYrYzsmO.mjs +2 -0
  35. package/dist/schema-BYrYzsmO.mjs.map +1 -0
  36. package/dist/schema-IML1rRU_.mjs +2 -0
  37. package/dist/{schema-0zayzpD_.mjs.map → schema-IML1rRU_.mjs.map} +1 -1
  38. package/dist/seed.d.mts.map +1 -1
  39. package/dist/seed.mjs +1 -1
  40. package/dist/seed.mjs.map +1 -1
  41. package/dist/{versions-C9A4rUAR.mjs → versions-DLrR7hIZ.mjs} +2 -2
  42. package/dist/{versions-C9A4rUAR.mjs.map → versions-DLrR7hIZ.mjs.map} +1 -1
  43. package/package.json +9 -9
  44. package/dist/plugin-D0i6odXQ.mjs +0 -2
  45. package/dist/plugin-D0i6odXQ.mjs.map +0 -1
  46. package/dist/resolve-context-MRZZ3rQl.mjs +0 -2
  47. package/dist/resolve-context-MRZZ3rQl.mjs.map +0 -1
  48. package/dist/routes-C9I1iZw8.mjs +0 -2
  49. package/dist/routing-strategy-CQdu1qV7.mjs +0 -2
  50. package/dist/routing-strategy-CQdu1qV7.mjs.map +0 -1
  51. package/dist/schema-0zayzpD_.mjs +0 -2
  52. package/dist/schema-D8sBQozw.mjs +0 -2
  53. package/dist/schema-D8sBQozw.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/paths/routes.ts","../src/redirects/routes.ts"],"mappings":";;;;cA2Da,uBAAA;;KAWD,mBAAA;WAEG,IAAA;EAAA,SAA2B,UAAA;EAAA,SAA6B,EAAA;AAAA;WAExD,IAAA;EAAA,SAA2B,EAAA;AAAA;WAE3B,IAAA;AAAA;;;;iBAiHC,UAAA,CAAA,GAAc,UAAU;;;;;;;;;;;iBC6VxB,cAAA,CAAA,GAAkB,UAAU"}
1
+ {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/paths/routes.ts","../src/redirects/routes.ts"],"mappings":";;;;cA2Da,uBAAA;;KAWD,mBAAA;WAEG,IAAA;EAAA,SAA2B,UAAA;EAAA,SAA6B,EAAA;AAAA;WAExD,IAAA;EAAA,SAA2B,EAAA;AAAA;WAE3B,IAAA;AAAA;;;;iBAiHC,UAAA,CAAA,GAAc,UAAU;;;;;;;;;;;iBCgXxB,cAAA,CAAA,GAAkB,UAAU"}
package/dist/admin.mjs CHANGED
@@ -1 +1 @@
1
- import{n as e,r as t,t as n}from"./routes-C9I1iZw8.mjs";export{e as PATH_RESOLVE_PERMISSION,t as pathRoutes,n as redirectRoutes};
1
+ import{n as e,r as t,t as n}from"./routes-BmNSYyuT.mjs";export{e as PATH_RESOLVE_PERMISSION,t as pathRoutes,n as redirectRoutes};
@@ -1,2 +1,2 @@
1
- import{r as e}from"./resolve-context-MRZZ3rQl.mjs";import{r as t}from"./enumerate-CBqu63Kp.mjs";import{collectLinkValues as n,linkValueKey as r}from"@murumets-ee/blocks/links";import{canvasSiteFormat as i,collectBoundColumnsInTree as a,createCollectionReader as o,effectiveCollectionSources as s,hasConfiguredLiveSources as c,loadCanvasLiveValues as l,materializeServerTemplate as u,readSpotCrop as d,resolveLinks as f,resolveTreeCollections as p,sameSpotCrop as m,serverBlockStructureLookup as h,serverBoundColumnsInTree as g,serverCollectionFieldLookup as _,serverTemplateMaterializer as v,templateNamespace as y}from"@murumets-ee/blocks/server";const b=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function x(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function S(e){return Array.isArray(e)}const C=new Set(Object.keys({id:!0,url:!0,mimeType:!0,width:!0,height:!0,alt:!0,title:!0,variants:!0,sources:!0,avifSources:!0,focalPoint:!0,blurDataUrl:!0,crop:!0,spotSources:!0}));function w(e,t){if(!x(e))return null;let n=e.id;if(typeof n!=`string`||!b.test(n)||typeof e.url!=`string`||e.url===``)return null;let r=Object.keys(e).filter(e=>!C.has(e));return r.length>0?(t?.(r),null):!T(e.alt,`string`)||!T(e.title,`string`)||!T(e.mimeType,`string`)||!T(e.width,`number`)||!T(e.height,`number`)||!T(e.blurDataUrl,`string`)||e.variants!==void 0&&e.variants!==null&&!x(e.variants)||e.focalPoint!==void 0&&e.focalPoint!==null&&!x(e.focalPoint)||e.sources!==void 0&&e.sources!==null&&(!x(e.sources)||!Object.values(e.sources).every(e=>Array.isArray(e)))||e.avifSources!==void 0&&e.avifSources!==null&&(!x(e.avifSources)||!Object.values(e.avifSources).every(e=>Array.isArray(e)))||e.crop!==void 0&&e.crop!==null&&!x(e.crop)||e.spotSources!==void 0&&e.spotSources!==null&&!Array.isArray(e.spotSources)?null:n}function T(e,t){return e==null||typeof e===t}function E(e,t,n,r){if(n>256){r.truncated=!0;return}let i=e=>{for(let t of e)r.nearMissKeys.add(t)};if(S(e)){for(let a=0;a<e.length;a+=1){let o=w(e[a],i);o===null?E(e[a],t,n+1,r):t.push({container:e,key:a,id:o,crop:d(e[a])})}return}if(x(e))for(let[a,o]of Object.entries(e)){let s=w(o,i);s===null?E(o,t,n+1,r):t.push({container:e,key:a,id:s,crop:d(o)})}}function D(e){let t={id:e.id,url:e.url,variants:e.variants,focalPoint:e.focalPoint,sources:e.sources};return e.avifSources!==void 0&&Object.keys(e.avifSources).length>0&&(t.avifSources=e.avifSources),e.mimeType!==null&&(t.mimeType=e.mimeType),e.width!==null&&(t.width=e.width),e.height!==null&&(t.height=e.height),e.alt!==null&&(t.alt=e.alt),e.title!==null&&(t.title=e.title),e.blurDataUrl!==null&&(t.blurDataUrl=e.blurDataUrl),t}function O(e,t){if(!(t===void 0||e.spotSources===void 0))return e.spotSources.find(e=>m(e.crop,t)&&e.sources.length>0)}function k(e,t){let n=D(e),r=O(e,t),i=r?.sources[r.sources.length-1];if(r!==void 0&&i!==void 0){n.url=i.url,n.width=i.width,n.height=i.height,n.focalPoint={x:.5,y:.5},n.sources={natural:r.sources};let e=r.avifSources;e!==void 0&&e.length===r.sources.length&&e.every((e,t)=>e.width===r.sources[t]?.width&&e.height===r.sources[t]?.height)?n.avifSources={natural:e}:delete n.avifSources}return n.crop={x:t.x,y:t.y,w:t.w,h:t.h},n}function A(e,t){let n=t===null?null:e.crop===void 0?D(t):k(t,e.crop);if(Array.isArray(e.container)){typeof e.key==`number`&&(e.container[e.key]=n);return}typeof e.key==`string`&&(e.container[e.key]=n)}async function j(e,t,i){let a=e,o=[];if(t.template!==void 0){let e=t.boundValues?.id;if(typeof e==`string`&&e!==``){let n=(i.materializeTemplate??u)(t.template,y(t.entity,e));a=n.blocks,o=n.boundColumns}else a=[],i.logger?.warn({entity:t.entity,field:t.field},`content: a templated document arrived without its row id, so its template is served EMPTY — its block ids could not be namespaced to it (editor F124)`)}else if(t.boundValues!==void 0&&S(e))try{o=(i.boundColumnsOf??g)(e)}catch(e){i.logger?.warn({err:e,entity:t.entity,field:t.field},`content: could not enumerate the bound columns of a block tree, so its bound-field links render plain`)}let s=t.boundValues??{},c=o.filter(e=>Object.hasOwn(s,e)).map(e=>s[e]);if(!S(a))throw i.logger?.warn({entity:t.entity,field:t.field},`content: stored block tree is not an array — nothing to resolve, so this field is served EMPTY rather than as stored (the caller retries this throw with an exhausted budget, and a non-array fails that identically)`),TypeError(`content: block tree \`${t.entity}.${t.field}\` is not an array; nothing to resolve.`);let l=[],d={truncated:!1,nearMissKeys:new Set};E(a,l,0,d),d.truncated&&i.logger?.warn({entity:t.entity,field:t.field,cap:256},`content: block tree is nested deeper than the media walk descends — any media ref below the bound keeps the url recorded when an editor picked it, which may be signed`),d.nearMissKeys.size>0&&i.logger?.debug({entity:t.entity,field:t.field,keys:[...d.nearMissKeys].sort()},`content: a value looked like a media ref but carried keys outside the projection, so it was left alone. If these are media refs, they keep their stored urls`);let p=[...n(a,64),...n(c,64)],m=[],h=[];for(let e of p)e.kind===`media`&&(b.test(e.id)?m.push(e.id):h.push({kind:`link`,ref:r(e),reason:`unresolvable`}));let _=P([...l.map(e=>e.id),...m]),v=[];for(let e of p)e.kind===`entity`&&(b.test(e.id)?v.push(F(e.entity,e.id)):h.push({kind:`link`,ref:r(e),reason:`unresolvable`}));let x=[...h];!i.resolveMedia&&_.length>0&&i.logger?.debug({entity:t.entity,field:t.field,count:_.length},`content: no media resolver wired for block trees — every media ref resolves to null`);let C=i.resolveMedia?t.budget.claim(`block`,_.length):0,w=_.slice(0,C),T=i.resolveMedia?new Set(_.slice(C)):new Set;for(let e of _.slice(C))x.push({kind:`media`,ref:e,reason:T.has(e)?`budget`:`unresolvable`});let D=P(v),O=t.budget.claim(`block`,D.length),k=D.slice(0,O),j=new Set(D.slice(O)),z=await L(w,t.locale,i,t.entity);for(let e of l)A(e,z.get(e.id)??null);for(let e of w)z.has(e)||x.push({kind:`media`,ref:e,reason:`unresolvable`});let B=await R(k,t.locale,i,t.entity),V=await f(p,{renderContext:`request`,locale:t.locale,resolveEntityHref:({entity:e,id:t})=>B.get(F(e,t))??null,resolveMediaHref:e=>z.get(e)?.url??null}),H={};for(let[e,t]of V)H[e]=t.href;for(let e of p){if(e.kind!==`entity`&&e.kind!==`media`)continue;let t=r(e);H[t]!==null&&H[t]!==void 0||x.push({kind:`link`,ref:t,reason:N(e,j,T)})}let U=i.resolveLiveValues,W=U===void 0?0:t.budget.claim(`block`,1),[{results:G,cardMedia:K},q]=await Promise.all([M(a,t,i),W>0&&U!==void 0?(async()=>U(a,t.locale))().catch(e=>{i.logger?.warn({err:e},`blocks: resolveLiveValues rejected; this tree draws no live values`)}):Promise.resolve(void 0)]),J=new Map;for(let e of z.keys())J.set(F(`media`,e),{entity:`media`,id:e});for(let e of K)J.set(F(`media`,e),{entity:`media`,id:e});for(let e of G.values())if(e.entity!==null)for(let t of e.rows)J.set(F(e.entity,t.id),{entity:e.entity,id:t.id});for(let e of B.keys()){if(J.has(e))continue;let t=e.indexOf(` `);if(t<=0)continue;let n=e.slice(0,t),r=e.slice(t+1);r!==``&&J.set(e,{entity:n,id:r})}return{resolved:1,blocks:a,links:H,...x.length>0&&{unresolved:I(x)},...J.size>0&&{refs:[...J.values()]},...G.size>0&&{collections:Object.fromEntries(G)},...q!==void 0&&Object.keys(q.sources).length>0&&{live:{sources:q.sources,values:q.values}},...i.currency!==void 0&&{currency:i.currency}}}async function M(e,t,n){let r=n.collections,i=t.canReadEntity,a=t.claimCollection;if(r===void 0||i===void 0||a===void 0)return{results:new Map,cardMedia:new Set};let o=n.resolveMedia,s=new Set;return{results:await p(e,r.sourceFieldOf,{sources:r.sources,getEntity:r.getEntity,clientFor:r.clientFor,canRead:i,resolveImages:o===void 0?void 0:async e=>{let n=[...await o(e,{locale:t.locale})].filter(([,e])=>e.mimeType?.startsWith(`image/`)===!0);for(let[e]of n)s.add(e);return new Map(n.map(([e,t])=>[e,{url:t.url,alt:t.alt,width:t.width,height:t.height,focalPoint:t.focalPoint,sources:t.sources,...t.avifSources!==void 0&&Object.keys(t.avifSources).length>0&&{avifSources:t.avifSources},blurDataUrl:t.blurDataUrl}]))},resolveHrefs:(e,r)=>n.resolvePublishedPaths(e,r,t.locale),locale:t.locale,renderContext:`request`,claim:()=>a()&&t.budget.claim(`block`,1)>=1,logger:n.logger}),cardMedia:s}}function N(e,t,n){return e.kind===`entity`&&t.has(F(e.entity,e.id))||e.kind===`media`&&n.has(e.id)?`budget`:`unresolvable`}function P(e){return[...new Set(e)]}function F(e,t){return[e,t].join(` `)}function I(e){let t=new Set,n=[];for(let r of e){let e=`${r.kind} ${r.ref}`;t.has(e)||(t.add(e),n.push(r))}return n}async function L(e,t,n,r){if(e.length===0||!n.resolveMedia)return new Map;try{return await n.resolveMedia(e,{locale:t,spots:!0})}catch(e){return n.logger?.warn({entity:r,err:e},"content: media resolver threw while resolving a block tree — its media refs resolve to null (if this is a ForbiddenError, the requesting role is missing `media:view`)"),new Map}}async function R(e,t,n,r){let i=new Map;if(e.length===0)return i;let a=new Map;for(let t of e){let e=t.indexOf(` `),n=t.slice(0,e),r=t.slice(e+1),i=a.get(n);i?i.push(r):a.set(n,[r])}for(let[e,o]of a)try{let r=await n.resolvePublishedPaths(e,o,t);for(let[t,n]of r)i.set(F(e,t),n)}catch(t){n.logger?.warn({entity:r,entityType:e,err:t},`content: path resolution threw for a block-tree link target — its links render as plain text`)}return i}function z(n,r){let u=null,d=n=>{if(u?.app===n)return u.deps;let d=e(n),{sources:f}=s(n),p=f.length===0?void 0:{sources:f,sourceFieldOf:_(),getEntity:e=>n.entities.get(e),clientFor:e=>o(e,n)},m=v(),g=h(),y={...p!==void 0&&{collections:p},boundColumnsOf:e=>a(e,g),materializeTemplate:(e,t)=>m(e,t),resolveMedia:r.resolveMedia,resolvePublishedPaths:async(e,n,r)=>{let i=await d.filterPublished(e,n,r);return i.size===0?new Map:t(e,[...i],r,d)},logger:n.logger,...c()&&{resolveLiveValues:(e,t)=>l(e,{locale:t,logger:n.logger})},currency:i(r.defaultLocale).currency};return u={app:n,deps:y},y};return async(e,t)=>{let i=await n();return j(e,{...t,locale:t.locale??r.defaultLocale},d(i))}}export{z as createBlockResolver,j as resolveBlockTree};
1
+ import{r as e}from"./resolve-context-Cvc1aNJe.mjs";import{r as t}from"./enumerate-CYHJKQz6.mjs";import{collectLinkValues as n,linkValueKey as r}from"@murumets-ee/blocks/links";import{canvasSiteFormat as i,collectBoundColumnsInTree as a,createCollectionReader as o,effectiveCollectionSources as s,hasConfiguredLiveSources as c,loadCanvasLiveValues as l,materializeServerTemplate as u,readSpotCrop as d,resolveLinks as f,resolveTreeCollections as p,sameSpotCrop as m,serverBlockStructureLookup as h,serverBoundColumnsInTree as g,serverCollectionFieldLookup as _,serverTemplateMaterializer as v,templateNamespace as y}from"@murumets-ee/blocks/server";const b=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function x(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function S(e){return Array.isArray(e)}const C=new Set(Object.keys({id:!0,url:!0,mimeType:!0,width:!0,height:!0,alt:!0,title:!0,variants:!0,sources:!0,avifSources:!0,focalPoint:!0,blurDataUrl:!0,crop:!0,spotSources:!0}));function w(e,t){if(!x(e))return null;let n=e.id;if(typeof n!=`string`||!b.test(n)||typeof e.url!=`string`||e.url===``)return null;let r=Object.keys(e).filter(e=>!C.has(e));return r.length>0?(t?.(r),null):!T(e.alt,`string`)||!T(e.title,`string`)||!T(e.mimeType,`string`)||!T(e.width,`number`)||!T(e.height,`number`)||!T(e.blurDataUrl,`string`)||e.variants!==void 0&&e.variants!==null&&!x(e.variants)||e.focalPoint!==void 0&&e.focalPoint!==null&&!x(e.focalPoint)||e.sources!==void 0&&e.sources!==null&&(!x(e.sources)||!Object.values(e.sources).every(e=>Array.isArray(e)))||e.avifSources!==void 0&&e.avifSources!==null&&(!x(e.avifSources)||!Object.values(e.avifSources).every(e=>Array.isArray(e)))||e.crop!==void 0&&e.crop!==null&&!x(e.crop)||e.spotSources!==void 0&&e.spotSources!==null&&!Array.isArray(e.spotSources)?null:n}function T(e,t){return e==null||typeof e===t}function E(e,t,n,r){if(n>256){r.truncated=!0;return}let i=e=>{for(let t of e)r.nearMissKeys.add(t)};if(S(e)){for(let a=0;a<e.length;a+=1){let o=w(e[a],i);o===null?E(e[a],t,n+1,r):t.push({container:e,key:a,id:o,crop:d(e[a])})}return}if(x(e))for(let[a,o]of Object.entries(e)){let s=w(o,i);s===null?E(o,t,n+1,r):t.push({container:e,key:a,id:s,crop:d(o)})}}function D(e){let t={id:e.id,url:e.url,variants:e.variants,focalPoint:e.focalPoint,sources:e.sources};return e.avifSources!==void 0&&Object.keys(e.avifSources).length>0&&(t.avifSources=e.avifSources),e.mimeType!==null&&(t.mimeType=e.mimeType),e.width!==null&&(t.width=e.width),e.height!==null&&(t.height=e.height),e.alt!==null&&(t.alt=e.alt),e.title!==null&&(t.title=e.title),e.blurDataUrl!==null&&(t.blurDataUrl=e.blurDataUrl),t}function O(e,t){if(!(t===void 0||e.spotSources===void 0))return e.spotSources.find(e=>m(e.crop,t)&&e.sources.length>0)}function k(e,t){let n=D(e),r=O(e,t),i=r?.sources[r.sources.length-1];if(r!==void 0&&i!==void 0){n.url=i.url,n.width=i.width,n.height=i.height,n.focalPoint={x:.5,y:.5},n.sources={natural:r.sources};let e=r.avifSources;e!==void 0&&e.length===r.sources.length&&e.every((e,t)=>e.width===r.sources[t]?.width&&e.height===r.sources[t]?.height)?n.avifSources={natural:e}:delete n.avifSources}return n.crop={x:t.x,y:t.y,w:t.w,h:t.h},n}function A(e,t){let n=t===null?null:e.crop===void 0?D(t):k(t,e.crop);if(Array.isArray(e.container)){typeof e.key==`number`&&(e.container[e.key]=n);return}typeof e.key==`string`&&(e.container[e.key]=n)}async function j(e,t,i){let a=e,o=[];if(t.template!==void 0){let e=t.boundValues?.id;if(typeof e==`string`&&e!==``){let n=(i.materializeTemplate??u)(t.template,y(t.entity,e));a=n.blocks,o=n.boundColumns}else a=[],i.logger?.warn({entity:t.entity,field:t.field},`content: a templated document arrived without its row id, so its template is served EMPTY — its block ids could not be namespaced to it (editor F124)`)}else if(t.boundValues!==void 0&&S(e))try{o=(i.boundColumnsOf??g)(e)}catch(e){i.logger?.warn({err:e,entity:t.entity,field:t.field},`content: could not enumerate the bound columns of a block tree, so its bound-field links render plain`)}let s=t.boundValues??{},c=o.filter(e=>Object.hasOwn(s,e)).map(e=>s[e]);if(!S(a))throw i.logger?.warn({entity:t.entity,field:t.field},`content: stored block tree is not an array — nothing to resolve, so this field is served EMPTY rather than as stored (the caller retries this throw with an exhausted budget, and a non-array fails that identically)`),TypeError(`content: block tree \`${t.entity}.${t.field}\` is not an array; nothing to resolve.`);let l=[],d={truncated:!1,nearMissKeys:new Set};E(a,l,0,d),d.truncated&&i.logger?.warn({entity:t.entity,field:t.field,cap:256},`content: block tree is nested deeper than the media walk descends — any media ref below the bound keeps the url recorded when an editor picked it, which may be signed`),d.nearMissKeys.size>0&&i.logger?.debug({entity:t.entity,field:t.field,keys:[...d.nearMissKeys].sort()},`content: a value looked like a media ref but carried keys outside the projection, so it was left alone. If these are media refs, they keep their stored urls`);let p=[...n(a,64),...n(c,64)],m=[],h=[];for(let e of p)e.kind===`media`&&(b.test(e.id)?m.push(e.id):h.push({kind:`link`,ref:r(e),reason:`unresolvable`}));let _=P([...l.map(e=>e.id),...m]),v=[];for(let e of p)e.kind===`entity`&&(b.test(e.id)?v.push(F(e.entity,e.id)):h.push({kind:`link`,ref:r(e),reason:`unresolvable`}));let x=[...h];!i.resolveMedia&&_.length>0&&i.logger?.debug({entity:t.entity,field:t.field,count:_.length},`content: no media resolver wired for block trees — every media ref resolves to null`);let C=i.resolveMedia?t.budget.claim(`block`,_.length):0,w=_.slice(0,C),T=i.resolveMedia?new Set(_.slice(C)):new Set;for(let e of _.slice(C))x.push({kind:`media`,ref:e,reason:T.has(e)?`budget`:`unresolvable`});let D=P(v),O=t.budget.claim(`block`,D.length),k=D.slice(0,O),j=new Set(D.slice(O)),z=await L(w,t.locale,i,t.entity);for(let e of l)A(e,z.get(e.id)??null);for(let e of w)z.has(e)||x.push({kind:`media`,ref:e,reason:`unresolvable`});let B=await R(k,t.locale,i,t.entity),V=await f(p,{renderContext:`request`,locale:t.locale,resolveEntityHref:({entity:e,id:t})=>B.get(F(e,t))??null,resolveMediaHref:e=>z.get(e)?.url??null}),H={};for(let[e,t]of V)H[e]=t.href;for(let e of p){if(e.kind!==`entity`&&e.kind!==`media`)continue;let t=r(e);H[t]!==null&&H[t]!==void 0||x.push({kind:`link`,ref:t,reason:N(e,j,T)})}let U=i.resolveLiveValues,W=U===void 0?0:t.budget.claim(`block`,1),[{results:G,cardMedia:K},q]=await Promise.all([M(a,t,i),W>0&&U!==void 0?(async()=>U(a,t.locale))().catch(e=>{i.logger?.warn({err:e},`blocks: resolveLiveValues rejected; this tree draws no live values`)}):Promise.resolve(void 0)]),J=new Map;for(let e of z.keys())J.set(F(`media`,e),{entity:`media`,id:e});for(let e of K)J.set(F(`media`,e),{entity:`media`,id:e});for(let e of G.values())if(e.entity!==null)for(let t of e.rows)J.set(F(e.entity,t.id),{entity:e.entity,id:t.id});for(let e of B.keys()){if(J.has(e))continue;let t=e.indexOf(` `);if(t<=0)continue;let n=e.slice(0,t),r=e.slice(t+1);r!==``&&J.set(e,{entity:n,id:r})}return{resolved:1,blocks:a,links:H,...x.length>0&&{unresolved:I(x)},...J.size>0&&{refs:[...J.values()]},...G.size>0&&{collections:Object.fromEntries(G)},...q!==void 0&&Object.keys(q.sources).length>0&&{live:{sources:q.sources,values:q.values}},...i.currency!==void 0&&{currency:i.currency}}}async function M(e,t,n){let r=n.collections,i=t.canReadEntity,a=t.claimCollection;if(r===void 0||i===void 0||a===void 0)return{results:new Map,cardMedia:new Set};let o=n.resolveMedia,s=new Set;return{results:await p(e,r.sourceFieldOf,{sources:r.sources,getEntity:r.getEntity,clientFor:r.clientFor,canRead:i,resolveImages:o===void 0?void 0:async e=>{let n=[...await o(e,{locale:t.locale})].filter(([,e])=>e.mimeType?.startsWith(`image/`)===!0);for(let[e]of n)s.add(e);return new Map(n.map(([e,t])=>[e,{url:t.url,alt:t.alt,width:t.width,height:t.height,focalPoint:t.focalPoint,sources:t.sources,...t.avifSources!==void 0&&Object.keys(t.avifSources).length>0&&{avifSources:t.avifSources},blurDataUrl:t.blurDataUrl}]))},resolveHrefs:(e,r)=>n.resolvePublishedPaths(e,r,t.locale),locale:t.locale,renderContext:`request`,claim:()=>a()&&t.budget.claim(`block`,1)>=1,logger:n.logger}),cardMedia:s}}function N(e,t,n){return e.kind===`entity`&&t.has(F(e.entity,e.id))||e.kind===`media`&&n.has(e.id)?`budget`:`unresolvable`}function P(e){return[...new Set(e)]}function F(e,t){return[e,t].join(` `)}function I(e){let t=new Set,n=[];for(let r of e){let e=`${r.kind} ${r.ref}`;t.has(e)||(t.add(e),n.push(r))}return n}async function L(e,t,n,r){if(e.length===0||!n.resolveMedia)return new Map;try{return await n.resolveMedia(e,{locale:t,spots:!0})}catch(e){return n.logger?.warn({entity:r,err:e},"content: media resolver threw while resolving a block tree — its media refs resolve to null (if this is a ForbiddenError, the requesting role is missing `media:view`)"),new Map}}async function R(e,t,n,r){let i=new Map;if(e.length===0)return i;let a=new Map;for(let t of e){let e=t.indexOf(` `),n=t.slice(0,e),r=t.slice(e+1),i=a.get(n);i?i.push(r):a.set(n,[r])}for(let[e,o]of a)try{let r=await n.resolvePublishedPaths(e,o,t);for(let[t,n]of r)i.set(F(e,t),n)}catch(t){n.logger?.warn({entity:r,entityType:e,err:t},`content: path resolution threw for a block-tree link target — its links render as plain text`)}return i}function z(n,r){let u=null,d=n=>{if(u?.app===n)return u.deps;let d=e(n),{sources:f}=s(n),p=f.length===0?void 0:{sources:f,sourceFieldOf:_(),getEntity:e=>n.entities.get(e),clientFor:e=>o(e,n)},m=v(),g=h(),y={...p!==void 0&&{collections:p},boundColumnsOf:e=>a(e,g),materializeTemplate:(e,t)=>m(e,t),resolveMedia:r.resolveMedia,resolvePublishedPaths:async(e,n,r)=>{let i=await d.filterPublished(e,n,r);return i.size===0?new Map:t(e,[...i],r,d)},logger:n.logger,...c()&&{resolveLiveValues:(e,t)=>l(e,{locale:t,logger:n.logger})},currency:i(r.defaultLocale).currency};return u={app:n,deps:y},y};return async(e,t)=>{let i=await n();return j(e,{...t,locale:t.locale??r.defaultLocale},d(i))}}export{z as createBlockResolver,j as resolveBlockTree};
2
2
  //# sourceMappingURL=block-resolver.mjs.map
package/dist/client.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { d as TranslationReport, f as TranslationStatus, n as DraftEntry, p as VersionEntry, s as LocalePublishStatus } from "./types-B3K2p9t8.mjs";
2
- import { f as FieldDiff } from "./index-C2G4IJck.mjs";
2
+ import { h as FieldDiff } from "./index-C3VyZLjf.mjs";
3
3
  import { Entity, FieldConfig } from "@murumets-ee/entity";
4
4
  import { Logger } from "@murumets-ee/core";
5
5
  import { AdminClient, FindManyOptions, InferCreateInput, InferEntityDTO, InferUpdateInput } from "@murumets-ee/entity/admin";
package/dist/client.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{t as e}from"./versionable-Br_G5VGR.mjs";import{n as t,t as n}from"./errors-NRNILhhm.mjs";import{t as r}from"./content-config-store-8omQF5p5.mjs";import{a as i,d as a,f as o,h as s,i as c,l,m as u,n as d,o as f,p,r as m,s as h,t as g,u as _}from"./versions-C9A4rUAR.mjs";import{n as v}from"./plugin-D0i6odXQ.mjs";import{t as y}from"./locale-utils-B0mFoV47.mjs";import{i as b,n as x,r as S}from"./context-D7eEItCY.mjs";import{blockTreeFieldsOf as C,buildPartitionCondition as w}from"@murumets-ee/entity";import{getApp as T,requireSiteFor as E}from"@murumets-ee/core";import{schemaRegistry as D}from"@murumets-ee/db";import{and as O,inArray as k}from"drizzle-orm";import"server-only";import{createAdminClient as A}from"@murumets-ee/core/clients";function j(e){return{id:e.id,entityId:e.entityId,locale:e.locale,data:e.data,createdBy:e.createdBy,createdByName:e.createdByName??null,createdAt:e.createdAt,updatedAt:e.updatedAt}}async function M(e,n,r,i=`_`,o){y(i);let s=await S(e);if(!s)throw new t(e.entity.name,`drafts`);let c=a(e.entity,r);e.admin.assertBlockTreesValid(c);let l=C(e.entity).some(({name:e})=>Object.hasOwn(c,e))?await s.findOne({entityId:n,locale:i}):null;await e.admin.assertMediaKindsValid(c,l?.data);let u=new Date,d=o?.id??``,f=o?.name??null;return j(await s.upsert({entityId:n,locale:i,data:c,createdBy:d,createdByName:f,createdAt:u,updatedAt:u},{target:[`entityId`,`locale`],set:{data:c,updatedAt:u}}))}const N=[`slug`,`slugMode`,`parent`];async function P(e,n,r,i=`_`,o){y(i);let s=await S(e);if(!s)throw new t(e.entity.name,`drafts`);let c=a(e.entity,r);if(Object.keys(c).length===0){let e=await s.findOne({entityId:n,locale:i});return e?j(e):null}e.admin.assertBlockTreesValid(c),e.admin.assertUpdateValid(c);let l=C(e.entity).some(({name:e})=>Object.hasOwn(c,e)),u=e.entity.frontend?.path!==void 0&&N.some(e=>Object.hasOwn(c,e)),d=l||u?await s.findOne({entityId:n,locale:i}):null;if(await e.admin.assertMediaKindsValid(c,d?.data),u){let t={...d?.data??{},...c};if(i===`_`)await e.admin.assertPathAvailable(n,t);else{let r=e.entity.allFields,a={},o={};for(let[e,n]of Object.entries(t))r[e]?.translatable===!0?a[e]=n:o[e]=n;let s=e=>N.some(t=>Object.hasOwn(e,t)&&Object.hasOwn(c,t));s(a)&&await e.admin.assertPathAvailable(n,a,i),s(o)&&await e.admin.assertPathAvailable(n,o)}}let f=new Date;return j(await s.upsert({entityId:n,locale:i,data:c,createdBy:o?.id??``,createdByName:o?.name??null,createdAt:f,updatedAt:f},{target:[`entityId`,`locale`],set:{updatedAt:f},mergeJsonb:[`data`]}))}async function F(e,t,n=`_`){y(n);let r=await S(e);if(!r)return null;let i=await r.findOne({entityId:t,locale:n});return i?j(i):null}async function I(e,t,n=`_`){y(n);let r=await S(e);return r?(await r.deleteMany({entityId:t,locale:n})).length>0:!1}async function L(e,r,i=`_`,s){if(y(i),!await S(e))throw new t(e.entity.name,`drafts`);let c=e.versionableOptions?await e.admin.findById(r,p(i)):null;return e.db.transaction(async l=>{let u={...e,db:l},f=await S(u);if(!f)throw new t(e.entity.name,`drafts`);let[p]=await f.deleteMany({entityId:r,locale:i});if(!p)throw new n(r,i);let{publishedAt:m,...h}=p.data??{},g=await o(u,r,a(e.entity,{...h,status:`published`}),i,l),_=R(e.entity,h);return _!==void 0&&await e.admin.updateInternal(r,{_overrides:_},{tx:l}),e.versionableOptions&&await d(u,r,g,c,i,s),g})}function R(e,t){if(!(e.behaviors??[]).some(e=>e.name===`withOverrides`))return;let n=e.allFields._overrides;if(n===void 0||n.internal!==!0)return;let r=t._overrides;if(!(typeof r!=`object`||!r||Array.isArray(r)))return r}async function z(e,t,n=`_`){y(n);let r=await S(e);return r?r.exists({entityId:t,locale:n}):!1}async function B(e,t=`_`){y(t);let n=await S(e);return n?n.count({locale:t}):0}async function V(e,t,n=`_`){y(n);let r=new Set;if(t.length===0)return r;let i=await S(e);if(!i)return r;let a=await i.findMany({where:{entityId:{in:t},locale:n}});for(let e of a)r.add(e.entityId);return r}async function H(e,t,n=`_`){y(n);let i=await F(e,t,n);if(!i)return null;let a=r().defaultLocale,o=n===`_`?{defaultLocale:a}:{locale:n,defaultLocale:a},c=await e.admin.findById(t,o);if(!c)return null;let l=e.entity.allFields,d=c,f=i.data,p={};for(let e of Object.keys(f))p[e]=d[e];let m=s(p,f,l);for(let e of m)e.type===`blockTree`&&(e.blockDiffs=u(Array.isArray(e.from)?e.from:[],Array.isArray(e.to)?e.to:[]));return{diffs:m,current:c,draft:i}}async function U(e,t){let n=(await G(e,[t])).get(t);return n?Object.values(n):[]}async function W(e,n,i,a){y(i);let o=r();if(i===o.defaultLocale){let t={status:a};a===`published`&&(t.publishedAt=new Date),await e.admin.update(n,t);return}let s=await b(e);if(!s)throw new t(e.entity.name,`localeStatus`);let c={status:a};await e.admin.updateForLocale(n,c,i,{defaultLocale:o.defaultLocale,localeStatusWriter:(e,t,n)=>v(s,e,t,n)})}async function G(e,t){let n=new Map;if(t.length===0)return n;let r=await b(e);if(!r)return n;let i=await r.findMany({where:{entityId:{in:t}}});for(let e of i){let t=e.entityId,r=e.locale,i=n.get(t);i||(i={},n.set(t,i)),i[r]={locale:r,status:e.status,publishedAt:e.publishedAt??null}}return n}function K(e,t,n,r){let i={};for(let a of n){if(a.code===r)continue;let n=t?.get(a.code)??{},o=[];for(let t of e)(n[t]===void 0||n[t]===null)&&o.push(t);i[a.code]={complete:o.length===0,total:e.length,translated:e.length-o.length,missing:o}}return i}async function q(e,t){return(await J(e,[t])).get(t)??{}}async function J(e,t){let n=new Map;if(t.length===0)return n;let i=r(),a=Object.entries(e.entity.allFields).filter(([e,t])=>t.translatable).map(([e])=>e);if(a.length===0)return n;let o=D.get(`${e.entity.name}_translations`);if(!o)return n;let s=await E(e.entity),c=k(o.entityId,t),l=await e.db.select().from(o).where(s===void 0?c:O(c,w(o,{site:s},e.entity.name))),u=new Map;for(let e of l){let t=e,n=t.entityId,r=t.locale,i={};for(let e of a)i[e]=t[e];let o=u.get(n);o||(o=new Map,u.set(n,o)),o.set(r,i)}for(let e of t)n.set(e,K(a,u.get(e),i.locales,i.defaultLocale));return n}async function Y(e,t){let n=r(),i=t?.locale??n.locales.find(e=>e.code!==n.defaultLocale)?.code;if(!i)return{total:0,complete:0,partial:0,missing:0};let a=await e.admin.findMany();if(a.length===0)return{total:0,complete:0,partial:0,missing:0};let o=await J(e,a.map(e=>e.id)),s=0,c=0,l=0;for(let e of a){let t=o.get(e.id)?.[i];!t||t.translated===0?l++:t.complete?s++:c++}return{total:a.length,complete:s,partial:c,missing:l}}var X=class{ctx;constructor(t){let n=e(t.entity.behaviors);this.ctx=x(t,n)}async create(e){let t=await this.ctx.admin.create(e);return this.ctx.versionableOptions&&await h(this.ctx,t.id,t,null,!1),t}findById(e,t){return this.ctx.admin.findById(e,t)}findMany(e){return this.ctx.admin.findMany(e)}async update(e,t){let n=null;this.ctx.versionableOptions&&(n=await this.ctx.admin.findById(e));let r=await this.ctx.admin.update(e,t);if(this.ctx.versionableOptions&&n){let t=_(n,r),a=await h(this.ctx,e,r,t,!1);if(a!==void 0)return await l(this.ctx,e,a),await i(this.ctx,e),{...r,_version:a};await i(this.ctx,e)}return r}delete(e){return this.ctx.admin.delete(e)}saveTranslation(e,t,n){return this.ctx.admin.saveTranslation(e,t,n)}deleteTranslation(e,t){return this.ctx.admin.deleteTranslation(e,t)}getVersions(e,t){return c(this.ctx,e,t)}getVersion(e,t,n){return m(this.ctx,e,t,n)}compareVersions(e,t,n,r){return g(this.ctx,e,t,n,r)}restoreVersion(e,t,n){return f(this.ctx,e,t,n)}pruneVersions(e,t){return i(this.ctx,e,t)}createVersionSnapshot(e,t,n,r,i){return d(this.ctx,e,t,n,r,i)}getTranslationStatus(e){return q(this.ctx,e)}getTranslationStatusBatch(e){return J(this.ctx,e)}getTranslationReport(e){return Y(this.ctx,e)}getLocaleStatus(e){return U(this.ctx,e)}setLocaleStatus(e,t,n){return W(this.ctx,e,t,n)}getLocaleStatusBatch(e){return G(this.ctx,e)}saveDraft(e,t,n,r){return M(this.ctx,e,t,n,r)}mergeDraft(e,t,n,r){return P(this.ctx,e,t,n,r)}getDraft(e,t){return F(this.ctx,e,t)}deleteDraft(e,t){return I(this.ctx,e,t)}publishDraft(e,t,n){return L(this.ctx,e,t,n)}hasDraft(e,t){return z(this.ctx,e,t)}countDrafts(e){return B(this.ctx,e)}hasDraftBatch(e,t){return V(this.ctx,e,t)}compareDraftWithCurrent(e,t){return H(this.ctx,e,t)}};function Z(e){return new X({admin:A(e),entity:e,logger:T().logger.child({content:e.name})})}export{X as ContentClient,Z as createContentClient};
1
+ import{t as e}from"./versionable-Br_G5VGR.mjs";import{n as t,r as n,t as r}from"./errors-NRNILhhm.mjs";import{t as i}from"./content-config-store-8omQF5p5.mjs";import{a,d as o,f as s,h as c,i as l,l as u,m as d,n as f,o as p,p as m,r as h,s as g,t as _,u as v}from"./versions-DLrR7hIZ.mjs";import{n as y}from"./plugin-BmxghXzV.mjs";import{t as b}from"./locale-utils-B0mFoV47.mjs";import{i as x,n as S,r as C}from"./context-D7eEItCY.mjs";import{blockTreeFieldsOf as w,buildPartitionCondition as T}from"@murumets-ee/entity";import{getApp as E,getContext as D,requireSiteFor as O}from"@murumets-ee/core";import{schemaRegistry as k}from"@murumets-ee/db";import{and as A,inArray as j}from"drizzle-orm";import"server-only";import{createAdminClient as M}from"@murumets-ee/core/clients";function N(e){return{id:e.id,entityId:e.entityId,locale:e.locale,data:e.data,createdBy:e.createdBy,createdByName:e.createdByName??null,createdAt:e.createdAt,updatedAt:e.updatedAt}}async function P(e,n,r,i=`_`,a){b(i);let s=await C(e);if(!s)throw new t(e.entity.name,`drafts`);let c=o(e.entity,r);e.admin.assertBlockTreesValid(c);let l=w(e.entity).some(({name:e})=>Object.hasOwn(c,e))?await s.findOne({entityId:n,locale:i}):null;await e.admin.assertMediaKindsValid(c,l?.data);let u=new Date,d=a?.id??``,f=a?.name??null;return N(await s.upsert({entityId:n,locale:i,data:c,createdBy:d,createdByName:f,createdAt:u,updatedAt:u},{target:[`entityId`,`locale`],set:{data:c,updatedAt:u}}))}const F=[`slug`,`slugMode`,`parent`];async function I(e,n,r,i=`_`,a){b(i);let s=await C(e);if(!s)throw new t(e.entity.name,`drafts`);let c=o(e.entity,r);if(Object.keys(c).length===0){let e=await s.findOne({entityId:n,locale:i});return e?N(e):null}e.admin.assertBlockTreesValid(c),e.admin.assertUpdateValid(c);let l=w(e.entity).some(({name:e})=>Object.hasOwn(c,e)),u=e.entity.frontend?.path!==void 0&&F.some(e=>Object.hasOwn(c,e)),d=l||u?await s.findOne({entityId:n,locale:i}):null;if(await e.admin.assertMediaKindsValid(c,d?.data),u){let t={...d?.data??{},...c};if(i===`_`)await e.admin.assertPathAvailable(n,t);else{let r=e.entity.allFields,a={},o={};for(let[e,n]of Object.entries(t))r[e]?.translatable===!0?a[e]=n:o[e]=n;let s=e=>F.some(t=>Object.hasOwn(e,t)&&Object.hasOwn(c,t));s(a)&&await e.admin.assertPathAvailable(n,a,i),s(o)&&await e.admin.assertPathAvailable(n,o)}}let f=new Date;return N(await s.upsert({entityId:n,locale:i,data:c,createdBy:a?.id??``,createdByName:a?.name??null,createdAt:f,updatedAt:f},{target:[`entityId`,`locale`],set:{updatedAt:f},mergeJsonb:[`data`]}))}async function L(e,t,n=`_`){b(n);let r=await C(e);if(!r)return null;let i=await r.findOne({entityId:t,locale:n});return i?N(i):null}async function R(e,t,n=`_`){b(n);let r=await C(e);return r?(await r.deleteMany({entityId:t,locale:n})).length>0:!1}async function z(e,n,i=`_`,a){if(b(i),!await C(e))throw new t(e.entity.name,`drafts`);let c=e.versionableOptions?await e.admin.findById(n,m(i)):null;return e.db.transaction(async l=>{let u={...e,db:l},d=await C(u);if(!d)throw new t(e.entity.name,`drafts`);let[p]=await d.deleteMany({entityId:n,locale:i});if(!p)throw new r(n,i);let{publishedAt:m,...h}=p.data??{},g=await s(u,n,o(e.entity,{...h,status:`published`}),i,l),_=B(e.entity,h);return _!==void 0&&await e.admin.updateInternal(n,{_overrides:_},{tx:l}),e.versionableOptions&&await f(u,n,g,c,i,a),g})}function B(e,t){if(!(e.behaviors??[]).some(e=>e.name===`withOverrides`))return;let n=e.allFields._overrides;if(n===void 0||n.internal!==!0)return;let r=t._overrides;if(!(typeof r!=`object`||!r||Array.isArray(r)))return r}async function V(e,t,n=`_`){b(n);let r=await C(e);return r?r.exists({entityId:t,locale:n}):!1}async function H(e,t=`_`){b(t);let n=await C(e);return n?n.count({locale:t}):0}async function U(e,t,n=`_`){b(n);let r=new Set;if(t.length===0)return r;let i=await C(e);if(!i)return r;let a=await i.findMany({where:{entityId:{in:t},locale:n}});for(let e of a)r.add(e.entityId);return r}async function W(e,t,n=`_`){b(n);let r=await L(e,t,n);if(!r)return null;let a=i().defaultLocale,o=n===`_`?{defaultLocale:a}:{locale:n,defaultLocale:a},s=await e.admin.findById(t,o);if(!s)return null;let l=e.entity.allFields,u=s,f=r.data,p={};for(let e of Object.keys(f))p[e]=u[e];let m=c(p,f,l);for(let e of m)e.type===`blockTree`&&(e.blockDiffs=d(Array.isArray(e.from)?e.from:[],Array.isArray(e.to)?e.to:[]));return{diffs:m,current:s,draft:r}}async function G(e,t){let n=(await q(e,[t])).get(t);return n?Object.values(n):[]}async function K(e,n,r,a){b(r);let o=i();if(r===o.defaultLocale){let t={status:a};a===`published`&&(t.publishedAt=new Date),await e.admin.update(n,t);return}let s=await x(e);if(!s)throw new t(e.entity.name,`localeStatus`);let c={status:a};await e.admin.updateForLocale(n,c,r,{defaultLocale:o.defaultLocale,localeStatusWriter:(e,t,n)=>y(s,e,t,n)})}async function q(e,t){let n=new Map;if(t.length===0)return n;let r=await x(e);if(!r)return n;let i=await r.findMany({where:{entityId:{in:t}}});for(let e of i){let t=e.entityId,r=e.locale,i=n.get(t);i||(i={},n.set(t,i)),i[r]={locale:r,status:e.status,publishedAt:e.publishedAt??null}}return n}function J(){let e=i(),t=D();return{locales:(t?.availableLocales??e.locales.map(e=>e.code)).map(e=>({code:e})),defaultLocale:t?.defaultLocale??e.defaultLocale}}function Y(e,t,n,r){let i={};for(let a of n){if(a.code===r)continue;let n=t?.get(a.code)??{},o=[];for(let t of e)(n[t]===void 0||n[t]===null)&&o.push(t);i[a.code]={complete:o.length===0,total:e.length,translated:e.length-o.length,missing:o}}return i}async function X(e,t){return(await Z(e,[t])).get(t)??{}}async function Z(e,t){let n=new Map;if(t.length===0)return n;let{locales:r,defaultLocale:i}=J(),a=Object.entries(e.entity.allFields).filter(([e,t])=>t.translatable).map(([e])=>e);if(a.length===0)return n;let o=k.get(`${e.entity.name}_translations`);if(!o)return n;let s=await O(e.entity),c=j(o.entityId,t),l=await e.db.select().from(o).where(s===void 0?c:A(c,T(o,{site:s},e.entity.name))),u=new Map;for(let e of l){let t=e,n=t.entityId,r=t.locale,i={};for(let e of a)i[e]=t[e];let o=u.get(n);o||(o=new Map,u.set(n,o)),o.set(r,i)}for(let e of t)n.set(e,Y(a,u.get(e),r,i));return n}async function Q(e,t){let{locales:r,defaultLocale:i}=J(),a=t?.locale??r.find(e=>e.code!==i)?.code;if(!a)return{total:0,complete:0,partial:0,missing:0};if(!r.some(e=>e.code===a))throw new n(a,r.map(e=>e.code));let o=await e.admin.findMany();if(o.length===0)return{total:0,complete:0,partial:0,missing:0};let s=await Z(e,o.map(e=>e.id)),c=0,l=0,u=0;for(let e of o){let t=s.get(e.id)?.[a];!t||t.translated===0?u++:t.complete?c++:l++}return{total:o.length,complete:c,partial:l,missing:u}}var $=class{ctx;constructor(t){let n=e(t.entity.behaviors);this.ctx=S(t,n)}async create(e){let t=await this.ctx.admin.create(e);return this.ctx.versionableOptions&&await g(this.ctx,t.id,t,null,!1),t}findById(e,t){return this.ctx.admin.findById(e,t)}findMany(e){return this.ctx.admin.findMany(e)}async update(e,t){let n=null;this.ctx.versionableOptions&&(n=await this.ctx.admin.findById(e));let r=await this.ctx.admin.update(e,t);if(this.ctx.versionableOptions&&n){let t=v(n,r),i=await g(this.ctx,e,r,t,!1);if(i!==void 0)return await u(this.ctx,e,i),await a(this.ctx,e),{...r,_version:i};await a(this.ctx,e)}return r}delete(e){return this.ctx.admin.delete(e)}saveTranslation(e,t,n){return this.ctx.admin.saveTranslation(e,t,n)}deleteTranslation(e,t){return this.ctx.admin.deleteTranslation(e,t)}getVersions(e,t){return l(this.ctx,e,t)}getVersion(e,t,n){return h(this.ctx,e,t,n)}compareVersions(e,t,n,r){return _(this.ctx,e,t,n,r)}restoreVersion(e,t,n){return p(this.ctx,e,t,n)}pruneVersions(e,t){return a(this.ctx,e,t)}createVersionSnapshot(e,t,n,r,i){return f(this.ctx,e,t,n,r,i)}getTranslationStatus(e){return X(this.ctx,e)}getTranslationStatusBatch(e){return Z(this.ctx,e)}getTranslationReport(e){return Q(this.ctx,e)}getLocaleStatus(e){return G(this.ctx,e)}setLocaleStatus(e,t,n){return K(this.ctx,e,t,n)}getLocaleStatusBatch(e){return q(this.ctx,e)}saveDraft(e,t,n,r){return P(this.ctx,e,t,n,r)}mergeDraft(e,t,n,r){return I(this.ctx,e,t,n,r)}getDraft(e,t){return L(this.ctx,e,t)}deleteDraft(e,t){return R(this.ctx,e,t)}publishDraft(e,t,n){return z(this.ctx,e,t,n)}hasDraft(e,t){return V(this.ctx,e,t)}countDrafts(e){return H(this.ctx,e)}hasDraftBatch(e,t){return U(this.ctx,e,t)}compareDraftWithCurrent(e,t){return W(this.ctx,e,t)}};function ee(e){return new $({admin:M(e),entity:e,logger:E().logger.child({content:e.name})})}export{$ as ContentClient,ee as createContentClient};
2
2
  //# sourceMappingURL=client.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../src/drafts.ts","../src/locale-status.ts","../src/translation-status.ts","../src/client.ts"],"sourcesContent":["/**\n * Draft-overlay operations.\n *\n * Capability: requires the entity to declare `publishable()`. Reads\n * (`getDraft`, `hasDraft`, `countDrafts`, `hasDraftBatch`) silently\n * return falsy when the entity doesn't support drafts; writes\n * (`saveDraft`, `mergeDraft`, `publishDraft`) throw `EntityCapabilityError`.\n */\n\nimport 'server-only'\n\nimport { blockTreeFieldsOf, type FieldConfig } from '@murumets-ee/entity'\nimport type { InferEntityDTO, InferUpdateInput } from '@murumets-ee/entity/admin'\nimport { diffBlocks } from './diff/block-diff.js'\nimport { diffFields } from './diff/field-diff.js'\nimport type { FieldDiff } from './diff/types.js'\nimport { DraftNotFoundError, EntityCapabilityError } from './errors.js'\nimport {\n applyLocaleRoutedUpdate,\n localeFindOptions,\n} from './internal/apply-locale-routed-update.js'\nimport { type ContentContext, getDraftsClient } from './internal/context.js'\nimport { projectToEntityFields } from './internal/projection.js'\nimport { DEFAULT_LOCALE_SENTINEL, validateLocale } from './locale-utils.js'\nimport { getContentConfig } from './plugin.js'\nimport type { DraftsTable } from './schema/drafts.js'\nimport type { DraftEntry } from './types.js'\nimport { createVersionSnapshot } from './versions.js'\n\ntype DraftRow = NonNullable<Awaited<ReturnType<ReturnType<DraftsTable['makeClient']>['findOne']>>>\n\nfunction toDraftEntry(row: DraftRow): DraftEntry {\n return {\n id: row.id,\n entityId: row.entityId,\n locale: row.locale,\n data: row.data,\n createdBy: row.createdBy,\n createdByName: row.createdByName ?? null,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }\n}\n\n/**\n * Save (upsert) a draft for an entity+locale. One draft per\n * entity+locale — overwrites any existing draft.\n *\n * `data` is projected through the entity's declared field whitelist\n * BEFORE it is stored (same `projectToEntityFields` call `publishDraft`\n * makes on the way out). Storing the caller's JSON verbatim let a draft\n * carry keys the entity never declared, identity/audit columns and\n * `internal: true` fields; publishing filtered them, but `getDraft` and\n * `compareDraftWithCurrent` echoed them straight back to the admin UI,\n * and the drafts table accumulated arbitrary caller-controlled JSON\n * (`plan/admin-api-hardening` F002). Validating at the write closes the\n * asymmetry — the stored draft is now always a subset of what the entity\n * can actually hold.\n */\nexport async function saveDraft<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n data: InferUpdateInput<F>,\n locale = DEFAULT_LOCALE_SENTINEL,\n user?: { id: string; name?: string },\n): Promise<DraftEntry<InferUpdateInput<F>>> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) throw new EntityCapabilityError(ctx.entity.name, 'drafts')\n\n const projected = projectToEntityFields(ctx.entity, data as Record<string, unknown>)\n\n // `projectToEntityFields` is a field-NAME whitelist — it never inspects a\n // value, so a `field.blockTree()` column in a draft took arbitrary JSON, and\n // `editor-state` hands the stored draft back as the CANVAS BASE STATE\n // (editor F002). Publishing is already gated by the entity write path, but\n // waiting until publish means the editor loads a tree it cannot represent.\n // Same validator, same judgement — asked here because the draft blob carries\n // no Zod schema of its own.\n ctx.admin.assertBlockTreesValid(projected)\n // F116, same reasoning — and this is precisely the write path the finding\n // was filed against: dava's home-page DRAFT already holds a mismatched\n // media reference. Grandfathering against the draft ALREADY STORED (rather\n // than validating strictly) is what keeps that draft's next autosave\n // working instead of turning \"a broken image\" into \"a draft that can never\n // be saved again\".\n //\n // The lookup is skipped entirely when the PROJECTED payload carries no\n // block-tree field at all — `assertMediaKindsValid` would no-op anyway\n // (same optimization as `saveTranslation`'s), and most entities with\n // drafts have no block-tree field to begin with.\n const hasBlockTreeValue = blockTreeFieldsOf(ctx.entity).some(({ name }) =>\n Object.hasOwn(projected, name),\n )\n const existing = hasBlockTreeValue ? await client.findOne({ entityId, locale }) : null\n await ctx.admin.assertMediaKindsValid(\n projected,\n existing?.data as Record<string, unknown> | undefined,\n )\n\n const now = new Date()\n const createdBy = user?.id ?? ''\n const createdByName = user?.name ?? null\n\n const row = await client.upsert(\n {\n entityId,\n locale,\n data: projected,\n createdBy,\n createdByName,\n createdAt: now,\n updatedAt: now,\n },\n {\n target: ['entityId', 'locale'],\n // On conflict, only update the mutable fields — preserve createdBy/createdAt.\n set: { data: projected, updatedAt: now },\n },\n )\n\n return toDraftEntry(row) as DraftEntry<InferUpdateInput<F>>\n}\n\n/**\n * The keys that move a document's canonical URL in the patterns the toolkit\n * ships (`[slug]`, `[parent.path]`) plus the slug mode that decides whether a\n * clash is refused or auto-suffixed. A merge naming one of them, on a ROUTABLE\n * entity, runs the path-conflict check (see `mergeDraft`); any other merge\n * skips its lookups — the staged draft, the row, and `toolkit_paths` (twice\n * under a parent).\n *\n * A pattern built from another field (`/[category]/[slug]`) is not covered:\n * staging only `category` is refused at Publish rather than here.\n */\nconst PATH_KEYS = ['slug', 'slugMode', 'parent'] as const\n\n/**\n * Stage a PARTIAL change: merge `patch` into the entity+locale draft, creating\n * the draft if there is none. Keys not in `patch` are left exactly as the draft\n * (or, absent a draft key, the live row) has them.\n *\n * This is the write every editing surface uses since editor D019 — \"every edit\n * on every surface saves to the document's one shared draft\". `saveDraft`\n * REPLACES the blob, which is right for a surface that owns the whole document\n * (the menu editor) and wrong for one that owns a few fields of it: page\n * settings saving `title` through `saveDraft` would erase the block edits the\n * canvas staged a second earlier. A draft is therefore a patch over the row, and\n * every reader that shows editable values reads row ⊕ draft.\n *\n * Judged like the entity PATCH it replaces, BEFORE it is stored:\n * - projected through the field whitelist (`internal`, undeclared keys dropped)\n * - `assertBlockTreesValid` + `assertMediaKindsValid` (grandfathered against the\n * stored draft, same as `saveDraft`)\n * - `assertUpdateValid` — the entity's own update schema. `saveDraft` never ran\n * it, which was harmless while the form's Publish still PATCHed; moving a\n * surface into the draft must not move its refusal to Publish, where the\n * author is no longer looking at the field.\n * - `assertPathAvailable` — the path-conflict guard, as a dry run, when the\n * merge moves the URL (see {@link PATH_KEYS}) on a routable entity (F150).\n *\n * 🔴 ONE STATEMENT, not read-modify-write. The merge is `data || patch` inside\n * `ON CONFLICT DO UPDATE` (`TableClient.upsert`'s `mergeJsonb`), so it cannot\n * lose a concurrent write: a read-then-upsert that interleaved with op-commit's\n * or a bound field's stage would write back the draft as it was read — silently\n * reverting the other writer's change. `writeDraft` merges the same way, so\n * neither can erase the other. Top-level `||`, a SHALLOW merge.\n *\n * Resolves `null` when the projected patch is EMPTY and no draft exists: nothing\n * the entity declares was sent, so nothing is staged and no empty draft row is\n * created.\n *\n * ⚠️ The rest of the write path's hooks — `pathParent()`'s cycle check, the\n * partial-unique slug index, `sluggable()`'s regeneration — still run only at\n * Publish, inside its transaction, where a refusal stops the whole publish. The\n * path check here answers \"as of now\"; Publish runs the real guard again.\n */\nexport async function mergeDraft<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n patch: InferUpdateInput<F>,\n locale = DEFAULT_LOCALE_SENTINEL,\n user?: { id: string; name?: string },\n): Promise<DraftEntry<InferUpdateInput<F>> | null> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) throw new EntityCapabilityError(ctx.entity.name, 'drafts')\n\n const projected = projectToEntityFields(ctx.entity, patch as Record<string, unknown>)\n // Nothing the entity declares: stage nothing. Upserting `{}` would CREATE an\n // empty draft, and the document would then read \"Draft saved · not live\n // yet\" about a change that does not exist.\n if (Object.keys(projected).length === 0) {\n const current = await client.findOne({ entityId, locale })\n return current ? (toDraftEntry(current) as DraftEntry<InferUpdateInput<F>>) : null\n }\n ctx.admin.assertBlockTreesValid(projected)\n ctx.admin.assertUpdateValid(projected)\n const hasBlockTreeValue = blockTreeFieldsOf(ctx.entity).some(({ name }) =>\n Object.hasOwn(projected, name),\n )\n const touchesPath =\n ctx.entity.frontend?.path !== undefined &&\n PATH_KEYS.some((key) => Object.hasOwn(projected, key))\n const existing =\n hasBlockTreeValue || touchesPath ? await client.findOne({ entityId, locale }) : null\n await ctx.admin.assertMediaKindsValid(\n projected,\n existing?.data as Record<string, unknown> | undefined,\n )\n // A staged slug that another document's URL already holds is refused NOW,\n // beside the field the author typed it in — not first at Publish (editor\n // PR60, F150). Checked against what the draft will hold after this merge,\n // since an earlier stage may have set `slugMode` or `parent`.\n if (touchesPath) {\n const staged = {\n ...((existing?.data as Record<string, unknown> | undefined) ?? {}),\n ...projected,\n }\n if (locale === DEFAULT_LOCALE_SENTINEL) {\n await ctx.admin.assertPathAvailable(entityId, staged)\n } else {\n // Checked the way Publish writes it: `updateForLocale` sends a locale's\n // TRANSLATABLE fields through that locale's guard and the rest (a\n // `parent`, a non-translatable slug) through `update()`, whose guard\n // checks the default locale. One guard over everything would test a\n // shared `parent` against the wrong locale's URLs.\n const fields = ctx.entity.allFields as Record<string, { translatable?: boolean } | undefined>\n const translatable: Record<string, unknown> = {}\n const shared: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(staged)) {\n if (fields[key]?.translatable === true) translatable[key] = value\n else shared[key] = value\n }\n const moves = (part: Record<string, unknown>) =>\n PATH_KEYS.some((key) => Object.hasOwn(part, key) && Object.hasOwn(projected, key))\n if (moves(translatable)) await ctx.admin.assertPathAvailable(entityId, translatable, locale)\n if (moves(shared)) await ctx.admin.assertPathAvailable(entityId, shared)\n }\n }\n\n const now = new Date()\n const row = await client.upsert(\n {\n entityId,\n locale,\n data: projected,\n createdBy: user?.id ?? '',\n createdByName: user?.name ?? null,\n createdAt: now,\n updatedAt: now,\n },\n { target: ['entityId', 'locale'], set: { updatedAt: now }, mergeJsonb: ['data'] },\n )\n return toDraftEntry(row) as DraftEntry<InferUpdateInput<F>>\n}\n\n/**\n * Get the draft for an entity+locale, or null if none exists (or the\n * entity doesn't support drafts).\n */\nexport async function getDraft<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<DraftEntry<InferUpdateInput<F>> | null> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) return null\n const row = await client.findOne({ entityId, locale })\n return row ? (toDraftEntry(row) as DraftEntry<InferUpdateInput<F>>) : null\n}\n\n/**\n * Delete (discard) a draft for an entity+locale. Returns whether\n * anything was deleted; safely no-ops on entities without drafts.\n */\nexport async function deleteDraft(\n ctx: ContentContext,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<boolean> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) return false\n const deleted = await client.deleteMany({ entityId, locale })\n return deleted.length > 0\n}\n\n/**\n * Publish a draft: make THIS document's staged changes live.\n *\n * \"Publish\" means one thing on every surface (editor D019): the draft's fields\n * are written to the row for the draft's locale AND the row is put live. The\n * draft is then gone and, if versionable, a version snapshot records what went\n * live.\n *\n * Locale routing mirrors the admin PATCH handler (and the taxonomy fix\n * in 10a7cc2b): a default-locale draft writes the base row; a\n * non-default-locale draft routes translatable fields to\n * `saveTranslation` (→ `{entity}_translations`), `status` to the\n * per-locale status table, and only non-translatable fields (which have\n * no per-locale home) to the base row. Without this split, publishing\n * an `et` draft overwrote the default-locale base row wholesale.\n *\n * ## ONE TRANSACTION (editor PR60, R055 §4)\n *\n * The row write, the locale status, `_overrides`, the draft delete and the\n * version snapshot commit together or not at all. They used to be five separate\n * statements, so a failure part-way left a document whose content was live and\n * whose draft still claimed to be pending — or, worse, whose draft was gone while\n * the row write had not landed.\n *\n * The draft is CLAIMED first, by deleting it and publishing what the delete\n * returned. The delete takes the row lock, so an op-commit that is mid-flight on\n * the same draft either committed before (and its edit is in what we publish) or\n * blocks until we commit and then retries its SERIALIZABLE transaction — or, if it\n * had not read the draft yet, inserts a fresh draft holding only its own patch.\n * Either way it does not write a stale copy back over the published one. The editor also flushes and awaits its own in-flight saves\n * before it calls this, so the lock is the backstop, not the mechanism.\n *\n * Path-index and write notifications fire after COMMIT, not inside it —\n * `AdminClient.afterCommit` defers them on a tracked transaction.\n */\nexport async function publishDraft<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n user?: { id: string; name?: string },\n): Promise<InferEntityDTO<F>> {\n validateLocale(locale)\n if (!(await getDraftsClient(ctx))) {\n throw new EntityCapabilityError(ctx.entity.name, 'drafts')\n }\n\n // Previous state for the version delta — locale-merged, so the delta compares\n // against what this locale's readers actually saw. Read BEFORE the\n // transaction: it is the committed state the publish replaces.\n const previousState: InferEntityDTO<F> | null = ctx.versionableOptions\n ? await ctx.admin.findById(entityId, localeFindOptions(locale))\n : null\n\n return ctx.db.transaction(async (tx) => {\n // Every table client below binds to `tx` through this context — drafts,\n // locale status, versions and the `_version` column write.\n const txCtx: ContentContext<F> = { ...ctx, db: tx }\n const draftsClient = await getDraftsClient(txCtx)\n if (!draftsClient) throw new EntityCapabilityError(ctx.entity.name, 'drafts')\n const [claimed] = await draftsClient.deleteMany({ entityId, locale })\n if (!claimed) {\n throw new DraftNotFoundError(entityId, locale)\n }\n\n // `?? {}` because the column is `jsonb NOT NULL`, and that forbids SQL NULL\n // while permitting the JSON literal `null` — which deserializes to JS `null`.\n // A bare cast then made `draftData.status` a TypeError, so a malformed row\n // 500'd the publish route instead of taking the ordinary \"nothing declared,\n // publish the row as-is\" path below. Nothing in this package writes such a\n // row; a seed, a migration or a hand-edit can.\n const claimedData = (claimed.data ?? {}) as Record<string, unknown>\n // `publishedAt` is Publish's to set, like `status` below: a staged value\n // would let whoever wrote the draft back- or forward-date the publication.\n // publishable()'s hook fills it when the UPDATE carries none — which, with\n // the staged value dropped, is every publish: it records the LATEST\n // publication, not the first.\n const { publishedAt: _stagedPublishedAt, ...draftData } = claimedData\n // \"Publish\" means MAKE LIVE, always — the status is not the draft's to\n // decide. Block-editor drafts carry no `status` at all, and before D019 a\n // form-saved draft could carry `status: 'draft'`, which then won: Publish\n // wrote the content and left the document unpublished, so the same button\n // meant \"publish\" on one surface and \"stage\" on another. The publishable()\n // behavior auto-sets `publishedAt` on the transition. The publish-permission\n // gate lives in the admin route, not here.\n const updateData = projectToEntityFields(ctx.entity, { ...draftData, status: 'published' })\n const result = await applyLocaleRoutedUpdate(txCtx, entityId, updateData, locale, tx)\n\n // F009 — promote the page's chrome overrides.\n //\n // `projectToEntityFields` drops every `internal: true` field, deliberately, so\n // `_overrides` cannot ride the generic promotion. That guard is NOT relaxed:\n // this is one named field, written through the trusted `updateInternal` path\n // that `with-overrides.ts` documents as \"the only path that can mutate it\".\n // Before F009 that call site did not exist, so an override survived in the\n // draft buffer and never reached the column — the op committed, the canvas\n // updated, and a reload brought the hidden header back.\n //\n // AFTER the main update, so the override never lands against content that\n // failed to — and inside the same transaction, so neither lands alone.\n // `_overrides` is not translatable, so it belongs on the base row for every\n // locale.\n const overrides = readOverridesForPublish(ctx.entity, draftData)\n if (overrides !== undefined) {\n await ctx.admin.updateInternal(entityId, { _overrides: overrides }, { tx })\n }\n\n if (ctx.versionableOptions) {\n await createVersionSnapshot(txCtx, entityId, result, previousState, locale, user)\n }\n\n return result\n })\n}\n\n/**\n * The page's override map from a draft payload, when the entity declares the\n * field and the draft carries one.\n *\n * Returns `undefined` — never `{}` — so an ordinary content publish does not\n * touch the column. Shape-guarded because a draft payload is stored jsonb: an\n * array or a scalar is malformed data, not an override map. ← F009\n */\nfunction readOverridesForPublish(\n entity: { allFields: Record<string, unknown>; behaviors?: readonly { name: string }[] },\n draftData: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n // The BEHAVIOR, not just a field with the right name — a draft payload is\n // stored jsonb and can be crafted or legacy, so this must not become a way\n // to promote arbitrary JSON into an unrelated internal field on an entity\n // that never opted in. Requiring the behavior also means `beforeUpdate` runs\n // `validateOverridesMap` on the way through `updateInternal`.\n if (!(entity.behaviors ?? []).some((b) => b.name === 'withOverrides')) return undefined\n const field = entity.allFields._overrides as { internal?: boolean } | undefined\n if (field === undefined || field.internal !== true) return undefined\n const value = draftData._overrides\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined\n return value as Record<string, unknown>\n}\n\n/**\n * Quick existence check for list badges — does a draft exist?\n */\nexport async function hasDraft(\n ctx: ContentContext,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<boolean> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) return false\n return client.exists({ entityId, locale })\n}\n\n/**\n * Count drafts for this entity type, optionally filtered by locale.\n * Single indexed COUNT — used by dashboard widgets.\n */\nexport async function countDrafts(\n ctx: ContentContext,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<number> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) return 0\n return client.count({ locale })\n}\n\n/**\n * Check draft existence for multiple entities in one query. Returns\n * a set of entity IDs that have a draft for `locale`.\n */\nexport async function hasDraftBatch(\n ctx: ContentContext,\n entityIds: string[],\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<Set<string>> {\n validateLocale(locale)\n const result = new Set<string>()\n if (entityIds.length === 0) return result\n const client = await getDraftsClient(ctx)\n if (!client) return result\n\n const rows = await client.findMany({\n where: { entityId: { in: entityIds }, locale },\n })\n for (const row of rows) {\n result.add(row.entityId as string)\n }\n return result\n}\n\n/**\n * Compare the current draft with the live entity. Returns field-level\n * diffs between the published data and the draft (only for fields\n * present in the draft — missing fields are NOT \"changed to empty\").\n */\nexport async function compareDraftWithCurrent<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<{\n diffs: FieldDiff[]\n current: InferEntityDTO<F>\n draft: DraftEntry<InferUpdateInput<F>>\n} | null> {\n validateLocale(locale)\n const draft = await getDraft(ctx, entityId, locale)\n if (!draft) return null\n\n // Fetch the live entity with the same locale context as the draft.\n const config = getContentConfig()\n const defaultLocale = config.defaultLocale\n const findOptions =\n locale === DEFAULT_LOCALE_SENTINEL ? { defaultLocale } : { locale, defaultLocale }\n const current = await ctx.admin.findById(entityId, findOptions)\n if (!current) return null\n\n const fieldConfigs = ctx.entity.allFields as Record<string, FieldConfig> | undefined\n const currentRecord = current as Record<string, unknown>\n const draftRecord = draft.data as Record<string, unknown>\n const currentSubset: Record<string, unknown> = {}\n for (const key of Object.keys(draftRecord)) {\n currentSubset[key] = currentRecord[key]\n }\n const diffs = diffFields(currentSubset, draftRecord, 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\n return { diffs, current, draft }\n}\n","/**\n * Per-locale publish status — independent draft/published state for\n * each locale of a publishable + translatable entity.\n *\n * Capability: requires `publishable() + at least one translatable\n * field` (the `_locale_status` table is registered only for that\n * combination). Reads silently return falsy on entities without it;\n * `setLocaleStatus` throws `EntityCapabilityError`.\n */\n\nimport 'server-only'\n\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport type { InferUpdateInput } from '@murumets-ee/entity/admin'\nimport { EntityCapabilityError } from './errors.js'\nimport { type ContentContext, getLocaleStatusClient } from './internal/context.js'\nimport { upsertLocaleStatusRow } from './locale-status-slot.js'\nimport { validateLocale } from './locale-utils.js'\nimport { getContentConfig } from './plugin.js'\nimport type { LocalePublishStatus } from './types.js'\n\n/**\n * Get per-locale publish status overrides for an entity.\n * Missing locale = inherits the base entity status (no row written).\n *\n * **Not permission-gated, deliberately — see {@link getLocaleStatusBatch}.**\n */\nexport async function getLocaleStatus(\n ctx: ContentContext,\n entityId: string,\n): Promise<LocalePublishStatus[]> {\n const map = await getLocaleStatusBatch(ctx, [entityId])\n const entry = map.get(entityId)\n if (!entry) return []\n return Object.values(entry)\n}\n\n/**\n * Set per-locale publish status. Upserts into the locale_status\n * table — except when `locale === defaultLocale`, which updates the\n * base entity status column directly (the per-locale table only\n * stores overrides for non-default locales).\n *\n * **Both branches are publish-gated by the entity layer (D041).** The default\n * branch inherits it from `AdminClient.update`; the non-default branch routes\n * through `AdminClient.updateForLocale`, which asserts the SAME\n * `assertPublishAuthority` from the caller's payload before it hands the write\n * to the locale-status writer. This is deliberately a delegation and not a\n * second predicate in this package: `<entity>_locale_status` is the table the\n * public read predicate (`COALESCE(locale_status.status, main.status)`) resolves\n * against, so a direct `TableClient.upsert` here — which is what this function\n * used to do — is a fourth publish door, reachable from any plugin/route/job\n * holding a `ContentClient` (a PUBLIC subpath export) with no `publish` grant\n * ever consulted.\n */\nexport async function setLocaleStatus<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n locale: string,\n status: 'draft' | 'published',\n): Promise<void> {\n validateLocale(locale)\n const config = getContentConfig()\n\n // Default locale → update base entity status column.\n if (locale === config.defaultLocale) {\n const data: Record<string, unknown> = { status }\n if (status === 'published') data.publishedAt = new Date()\n await ctx.admin.update(entityId, data as InferUpdateInput<F>)\n return\n }\n\n const client = await getLocaleStatusClient(ctx)\n if (!client) {\n // Per-locale status only exists for publishable + translatable\n // entities. Calling on a non-translatable publishable (where the\n // only valid locale is the default) is a programmer error.\n // Checked BEFORE delegating: `updateForLocale` would otherwise fall back to\n // the base `status` column for a missing writer, silently publishing the\n // default locale instead of refusing.\n throw new EntityCapabilityError(ctx.entity.name, 'localeStatus')\n }\n\n // The writer we hand down is the RAW upsert, not this function — the\n // delegation terminates here rather than re-entering `updateForLocale`.\n //\n // Routing through `updateForLocale` rather than calling the upsert directly\n // is what closes the publish door: this branch used to write\n // `<entity>_locale_status` with no permission check at all, so any holder of\n // this public export could make a translation publicly live without\n // `publish`. `updateForLocale` asserts publish authority at the operation\n // entry (D041), so the gate is inherited rather than duplicated here.\n //\n // Widened through `Record<string, unknown>` for the same reason the\n // default-locale branch above does it: `InferUpdateInput<F>` is keyed on the\n // entity's own field map, which a generic `F` cannot be proven to overlap.\n const localeData: Record<string, unknown> = { status }\n await ctx.admin.updateForLocale(entityId, localeData as InferUpdateInput<F>, locale, {\n defaultLocale: config.defaultLocale,\n localeStatusWriter: (id, loc, s) => upsertLocaleStatusRow(client, id, loc, s),\n })\n}\n\n/**\n * Get per-locale publish status for multiple entities in one query.\n *\n * ## Why this read is NOT permission-gated (and what would gate it)\n *\n * It returns publish-state METADATA only — `{ locale, status, publishedAt }` —\n * never field data, so it is not a content-disclosure door the way the write\n * side above is a publish door. What it does expose is an existence oracle for\n * unpublished TRANSLATIONS: a principal without `view_unpublished` learns that\n * `et` is a draft, which `QueryClient` would have hidden. The correct predicate\n * is therefore `view_unpublished` (D017), NOT `publish`.\n *\n * A CodeRabbit pass proposed gating it on exactly that predicate. Deliberately\n * declined, on the strength of **F063**: `view_unpublished` governs `QueryClient`\n * reads — the PUBLIC delivery surface — and is not a disclosure boundary over\n * the admin API, where `AdminClient` shows editors their own drafts by design.\n * `ContentClient` is an in-process server-side API whose consumers are plugins,\n * routes and jobs on the admin side, so gating it here would extend the control\n * onto the surface F063 explicitly recommended NOT extending it to. The concrete\n * cost would be an editor lacking `view_unpublished` no longer able to see that\n * their OWN translation is still a draft.\n *\n * The asymmetry with the write side above is real, not convenient:\n * `setLocaleStatus` MINTS public visibility, so it is gated regardless of which\n * surface calls it. This read hands publish-state metadata to a caller who\n * already holds `view` on the row — they needed its id to ask.\n *\n * It is not applied here because the predicate (`mayViewUnpublishedEntity`) is\n * module-private to `@murumets-ee/entity` — re-deriving it in this package\n * would create exactly the second, drifting copy of a security predicate that\n * D041 exists to eliminate, and there is no read-path equivalent of\n * `updateForLocale` to delegate to. The one in-repo caller\n * (`admin-ui`'s versions route) is already gated by `checkPermission`. Closing\n * this properly means exporting the predicate from `@murumets-ee/entity` and\n * dropping non-`published` rows for principals without it — tracked as the\n * read-side sibling of F060.\n */\nexport async function getLocaleStatusBatch(\n ctx: ContentContext,\n entityIds: string[],\n): Promise<Map<string, Record<string, LocalePublishStatus>>> {\n const result = new Map<string, Record<string, LocalePublishStatus>>()\n if (entityIds.length === 0) return result\n const client = await getLocaleStatusClient(ctx)\n if (!client) return result\n\n const rows = await client.findMany({ where: { entityId: { in: entityIds } } })\n for (const r of rows) {\n const eid = r.entityId as string\n const locale = r.locale as string\n let entry = result.get(eid)\n if (!entry) {\n entry = {}\n result.set(eid, entry)\n }\n entry[locale] = {\n locale,\n status: r.status as 'draft' | 'published',\n publishedAt: (r.publishedAt as Date | null) ?? null,\n }\n }\n return result\n}\n","/**\n * Translation status reporting — per-locale completion of translatable\n * fields for one entity, batches of entities, or the whole entity type.\n *\n * Capability: requires the entity to have at least one translatable\n * field. All methods return falsy / empty when the entity has no\n * translatable fields (read-only and defensive).\n */\n\nimport 'server-only'\n\nimport { requireSiteFor } from '@murumets-ee/core'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { buildPartitionCondition, type FieldConfig } from '@murumets-ee/entity'\nimport { and, inArray } from 'drizzle-orm'\nimport type { ContentContext } from './internal/context.js'\nimport { getContentConfig } from './plugin.js'\nimport type { TranslationReport, TranslationStatus } from './types.js'\n\n/**\n * For one entity, compute per-locale completion status across every\n * translatable field. Default-locale rows are skipped.\n */\nfunction computeStatusForEntity(\n translatableFields: string[],\n translatedByLocale: Map<string, Record<string, unknown>> | undefined,\n locales: { code: string }[],\n defaultLocale: string,\n): TranslationStatus {\n const status: TranslationStatus = {}\n for (const locale of locales) {\n if (locale.code === defaultLocale) continue\n const translated = translatedByLocale?.get(locale.code) ?? {}\n const missing: string[] = []\n for (const fieldName of translatableFields) {\n if (translated[fieldName] === undefined || translated[fieldName] === null) {\n missing.push(fieldName)\n }\n }\n status[locale.code] = {\n complete: missing.length === 0,\n total: translatableFields.length,\n translated: translatableFields.length - missing.length,\n missing,\n }\n }\n return status\n}\n\n/**\n * Get translation status for an entity — per-locale completion report.\n */\nexport async function getTranslationStatus(\n ctx: ContentContext,\n entityId: string,\n): Promise<TranslationStatus> {\n // Single-entity is the batch path with one ID — keeps the two\n // codepaths from drifting.\n const map = await getTranslationStatusBatch(ctx, [entityId])\n return map.get(entityId) ?? {}\n}\n\n/**\n * Get translation status for multiple entities in a single query.\n * O(1) DB roundtrips regardless of `entityIds.length`.\n */\nexport async function getTranslationStatusBatch(\n ctx: ContentContext,\n entityIds: string[],\n): Promise<Map<string, TranslationStatus>> {\n const result = new Map<string, TranslationStatus>()\n if (entityIds.length === 0) return result\n\n const config = getContentConfig()\n\n const translatableFields = Object.entries(ctx.entity.allFields as Record<string, FieldConfig>)\n .filter(([_, f]) => f.translatable)\n .map(([name]) => name)\n if (translatableFields.length === 0) return result\n\n // Translation tables are generated in @murumets-ee/entity (with FK\n // refs); accessed via schemaRegistry + direct Drizzle here.\n const translationTable = schemaRegistry.get(`${ctx.entity.name}_translations`)\n if (!translationTable) return result\n\n // A site-bound entity's translation rows are confined to the request's\n // site (multisite D003): ids alone are no boundary — a caller holding\n // another site's id must learn nothing about its translations.\n const site = await requireSiteFor(ctx.entity)\n const byIds = inArray(translationTable.entityId, entityIds)\n const translations = await ctx.db\n .select()\n .from(translationTable)\n .where(\n site !== undefined\n ? and(byIds, buildPartitionCondition(translationTable, { site }, ctx.entity.name))\n : byIds,\n )\n\n // Group: entityId → locale → { fieldName: value }\n const translationsByEntity = new Map<string, Map<string, Record<string, unknown>>>()\n for (const t of translations) {\n const row = t as Record<string, unknown>\n const eid = row.entityId as string\n const locale = row.locale as string\n const fieldValues: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n fieldValues[fieldName] = row[fieldName]\n }\n let entry = translationsByEntity.get(eid)\n if (!entry) {\n entry = new Map()\n translationsByEntity.set(eid, entry)\n }\n entry.set(locale, fieldValues)\n }\n\n for (const entityId of entityIds) {\n result.set(\n entityId,\n computeStatusForEntity(\n translatableFields,\n translationsByEntity.get(entityId),\n config.locales,\n config.defaultLocale,\n ),\n )\n }\n\n return result\n}\n\n/**\n * Roll up translation status across every entity of this type.\n *\n * Defaults to the first non-default configured locale; pass\n * `options.locale` to target a specific one. Returns counts of\n * complete / partial / missing entities for that locale.\n */\nexport async function getTranslationReport(\n ctx: ContentContext,\n options?: { locale?: string },\n): Promise<TranslationReport> {\n const config = getContentConfig()\n const targetLocale =\n options?.locale ?? config.locales.find((l) => l.code !== config.defaultLocale)?.code\n if (!targetLocale) return { total: 0, complete: 0, partial: 0, missing: 0 }\n\n const entities = await ctx.admin.findMany()\n if (entities.length === 0) return { total: 0, complete: 0, partial: 0, missing: 0 }\n\n // Single batched query covers every entity at once.\n const statusMap = await getTranslationStatusBatch(\n ctx,\n entities.map((e) => e.id),\n )\n\n let complete = 0\n let partial = 0\n let missing = 0\n for (const entity of entities) {\n const status = statusMap.get(entity.id)\n const localeStatus = status?.[targetLocale]\n if (!localeStatus || localeStatus.translated === 0) missing++\n else if (localeStatus.complete) complete++\n else partial++\n }\n\n return { total: entities.length, complete, partial, missing }\n}\n","/**\n * ContentClient — thin shell over per-feature modules.\n *\n * Wraps AdminClient with version history (`./versions.ts`), draft\n * overlays (`./drafts.ts`), translation status reporting\n * (`./translation-status.ts`), per-locale publish status\n * (`./locale-status.ts`), and block-layout operations (`./layout.ts`).\n *\n * The class is generic over the entity's `AllFields` map: when\n * constructed with a typed entity, every method's input/output is\n * inferred end-to-end. When constructed with a runtime `Entity`\n * (admin-ui's heterogeneous entity loops), the generic defaults to\n * `Record<string, FieldConfig>` and methods accept/return the loose\n * shape — same behavior as the pre-generic version.\n *\n * Usage:\n * import { createContentClient } from '@murumets-ee/content/client'\n * const pages = createContentClient(Page)\n * await pages.create({ title: 'Hello', layout: [...] })\n */\n\nimport 'server-only'\n\nimport { getApp, type Logger } from '@murumets-ee/core'\nimport { createAdminClient } from '@murumets-ee/core/clients'\nimport type { Entity, FieldConfig } from '@murumets-ee/entity'\nimport type {\n AdminClient,\n FindManyOptions,\n InferCreateInput,\n InferEntityDTO,\n InferUpdateInput,\n} from '@murumets-ee/entity/admin'\nimport { getVersionableOptions } from './behaviors/versionable.js'\nimport {\n compareDraftWithCurrent,\n countDrafts,\n deleteDraft,\n getDraft,\n hasDraft,\n hasDraftBatch,\n mergeDraft,\n publishDraft,\n saveDraft,\n} from './drafts.js'\nimport { type ContentContext, createContentContext } from './internal/context.js'\nimport { calculateDelta } from './internal/projection.js'\nimport { getLocaleStatus, getLocaleStatusBatch, setLocaleStatus } from './locale-status.js'\nimport {\n getTranslationReport,\n getTranslationStatus,\n getTranslationStatusBatch,\n} from './translation-status.js'\nimport {\n compareVersions,\n createVersionSnapshot,\n getVersion,\n getVersions,\n pruneVersions,\n restoreVersion,\n saveVersionSnapshot,\n writeVersionColumn,\n} from './versions.js'\n\nexport interface ContentClientConfig<\n AllFields extends Record<string, FieldConfig> = Record<string, FieldConfig>,\n> {\n admin: AdminClient<AllFields>\n entity: Entity<AllFields>\n logger?: Logger\n}\n\nexport class ContentClient<\n AllFields extends Record<string, FieldConfig> = Record<string, FieldConfig>,\n> {\n private ctx: ContentContext<AllFields>\n\n constructor(config: ContentClientConfig<AllFields>) {\n const versionableOptions = getVersionableOptions(config.entity.behaviors)\n this.ctx = createContentContext(config, versionableOptions)\n }\n\n // -----------------------------------------------------------------\n // CRUD (delegated to AdminClient + version snapshots)\n // -----------------------------------------------------------------\n\n async create(data: InferCreateInput<AllFields>): Promise<InferEntityDTO<AllFields>> {\n const result = await this.ctx.admin.create(data)\n if (this.ctx.versionableOptions) {\n await saveVersionSnapshot(this.ctx, result.id, result, null, false)\n }\n return result\n }\n\n findById(id: string, options?: { locale?: string }): Promise<InferEntityDTO<AllFields> | null> {\n return this.ctx.admin.findById(id, options)\n }\n\n findMany(options?: FindManyOptions): Promise<InferEntityDTO<AllFields>[]> {\n return this.ctx.admin.findMany(options)\n }\n\n /**\n * Base-row update + version snapshot. Subject to AdminClient's\n * cross-locale write guard (SD021): under a non-default request-context\n * locale, translatable fields throw `CrossLocaleWriteError` — that's\n * enforcement, not a gap. Locale-belonging writes go through the draft\n * flow (`saveDraft` → `publishDraft`) or `admin.updateForLocale` directly.\n */\n async update(id: string, data: InferUpdateInput<AllFields>): Promise<InferEntityDTO<AllFields>> {\n let previousState: InferEntityDTO<AllFields> | null = null\n if (this.ctx.versionableOptions) {\n previousState = await this.ctx.admin.findById(id)\n }\n\n const result = await this.ctx.admin.update(id, data)\n\n if (this.ctx.versionableOptions && previousState) {\n const delta = calculateDelta(previousState, result)\n const version = await saveVersionSnapshot(this.ctx, id, result, delta, false)\n if (version !== undefined) {\n await writeVersionColumn(this.ctx, id, version)\n await pruneVersions(this.ctx, id)\n return { ...result, _version: version } as InferEntityDTO<AllFields>\n }\n await pruneVersions(this.ctx, id)\n }\n\n return result\n }\n\n delete(id: string): Promise<unknown> {\n return this.ctx.admin.delete(id)\n }\n\n // -----------------------------------------------------------------\n // Translation delegation\n // -----------------------------------------------------------------\n\n saveTranslation(entityId: string, locale: string, translations: Record<string, unknown>) {\n return this.ctx.admin.saveTranslation(entityId, locale, translations)\n }\n\n deleteTranslation(entityId: string, locale?: string) {\n return this.ctx.admin.deleteTranslation(entityId, locale)\n }\n\n // -----------------------------------------------------------------\n // Version history\n // -----------------------------------------------------------------\n\n getVersions(entityId: string, options?: { limit?: number; offset?: number; locale?: string }) {\n return getVersions(this.ctx, entityId, options)\n }\n\n getVersion(entityId: string, version: number, locale?: string) {\n return getVersion(this.ctx, entityId, version, locale)\n }\n\n compareVersions(entityId: string, v1: number, v2: number, locale?: string) {\n return compareVersions(this.ctx, entityId, v1, v2, locale)\n }\n\n restoreVersion(entityId: string, version: number, locale?: string) {\n return restoreVersion(this.ctx, entityId, version, locale)\n }\n\n pruneVersions(entityId: string, locale?: string) {\n return pruneVersions(this.ctx, entityId, locale)\n }\n\n createVersionSnapshot(\n entityId: string,\n currentData: Record<string, unknown>,\n previousData?: Record<string, unknown> | null,\n locale?: string,\n user?: { id: string; name?: string },\n ) {\n return createVersionSnapshot(this.ctx, entityId, currentData, previousData, locale, user)\n }\n\n // -----------------------------------------------------------------\n // Translation status\n // -----------------------------------------------------------------\n\n getTranslationStatus(entityId: string) {\n return getTranslationStatus(this.ctx, entityId)\n }\n\n getTranslationStatusBatch(entityIds: string[]) {\n return getTranslationStatusBatch(this.ctx, entityIds)\n }\n\n getTranslationReport(options?: { locale?: string }) {\n return getTranslationReport(this.ctx, options)\n }\n\n // -----------------------------------------------------------------\n // Per-locale publish status\n // -----------------------------------------------------------------\n\n getLocaleStatus(entityId: string) {\n return getLocaleStatus(this.ctx, entityId)\n }\n\n setLocaleStatus(entityId: string, locale: string, status: 'draft' | 'published') {\n return setLocaleStatus(this.ctx, entityId, locale, status)\n }\n\n getLocaleStatusBatch(entityIds: string[]) {\n return getLocaleStatusBatch(this.ctx, entityIds)\n }\n\n // -----------------------------------------------------------------\n // Drafts\n // -----------------------------------------------------------------\n\n saveDraft(\n entityId: string,\n data: InferUpdateInput<AllFields>,\n locale?: string,\n user?: { id: string; name?: string },\n ) {\n return saveDraft(this.ctx, entityId, data, locale, user)\n }\n\n mergeDraft(\n entityId: string,\n patch: InferUpdateInput<AllFields>,\n locale?: string,\n user?: { id: string; name?: string },\n ) {\n return mergeDraft(this.ctx, entityId, patch, locale, user)\n }\n\n getDraft(entityId: string, locale?: string) {\n return getDraft(this.ctx, entityId, locale)\n }\n\n deleteDraft(entityId: string, locale?: string) {\n return deleteDraft(this.ctx, entityId, locale)\n }\n\n publishDraft(entityId: string, locale?: string, user?: { id: string; name?: string }) {\n return publishDraft(this.ctx, entityId, locale, user)\n }\n\n hasDraft(entityId: string, locale?: string) {\n return hasDraft(this.ctx, entityId, locale)\n }\n\n countDrafts(locale?: string) {\n return countDrafts(this.ctx, locale)\n }\n\n hasDraftBatch(entityIds: string[], locale?: string) {\n return hasDraftBatch(this.ctx, entityIds, locale)\n }\n\n compareDraftWithCurrent(entityId: string, locale?: string) {\n return compareDraftWithCurrent(this.ctx, entityId, locale)\n }\n}\n\n/**\n * Factory — creates a ContentClient for an entity. Must be called\n * after `createApp()`. Generic flows from the entity definition:\n * `createContentClient(Article)` returns a `ContentClient<typeof\n * Article['allFields']>` typed end-to-end.\n */\nexport function createContentClient<AllFields extends Record<string, FieldConfig>>(\n entity: Entity<AllFields>,\n): ContentClient<AllFields> {\n const admin = createAdminClient(entity)\n return new ContentClient<AllFields>({\n admin,\n entity,\n logger: getApp().logger.child({ content: entity.name }),\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../src/drafts.ts","../src/locale-status.ts","../src/translation-status.ts","../src/client.ts"],"sourcesContent":["/**\n * Draft-overlay operations.\n *\n * Capability: requires the entity to declare `publishable()`. Reads\n * (`getDraft`, `hasDraft`, `countDrafts`, `hasDraftBatch`) silently\n * return falsy when the entity doesn't support drafts; writes\n * (`saveDraft`, `mergeDraft`, `publishDraft`) throw `EntityCapabilityError`.\n */\n\nimport 'server-only'\n\nimport { blockTreeFieldsOf, type FieldConfig } from '@murumets-ee/entity'\nimport type { InferEntityDTO, InferUpdateInput } from '@murumets-ee/entity/admin'\nimport { diffBlocks } from './diff/block-diff.js'\nimport { diffFields } from './diff/field-diff.js'\nimport type { FieldDiff } from './diff/types.js'\nimport { DraftNotFoundError, EntityCapabilityError } from './errors.js'\nimport {\n applyLocaleRoutedUpdate,\n localeFindOptions,\n} from './internal/apply-locale-routed-update.js'\nimport { type ContentContext, getDraftsClient } from './internal/context.js'\nimport { projectToEntityFields } from './internal/projection.js'\nimport { DEFAULT_LOCALE_SENTINEL, validateLocale } from './locale-utils.js'\nimport { getContentConfig } from './plugin.js'\nimport type { DraftsTable } from './schema/drafts.js'\nimport type { DraftEntry } from './types.js'\nimport { createVersionSnapshot } from './versions.js'\n\ntype DraftRow = NonNullable<Awaited<ReturnType<ReturnType<DraftsTable['makeClient']>['findOne']>>>\n\nfunction toDraftEntry(row: DraftRow): DraftEntry {\n return {\n id: row.id,\n entityId: row.entityId,\n locale: row.locale,\n data: row.data,\n createdBy: row.createdBy,\n createdByName: row.createdByName ?? null,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }\n}\n\n/**\n * Save (upsert) a draft for an entity+locale. One draft per\n * entity+locale — overwrites any existing draft.\n *\n * `data` is projected through the entity's declared field whitelist\n * BEFORE it is stored (same `projectToEntityFields` call `publishDraft`\n * makes on the way out). Storing the caller's JSON verbatim let a draft\n * carry keys the entity never declared, identity/audit columns and\n * `internal: true` fields; publishing filtered them, but `getDraft` and\n * `compareDraftWithCurrent` echoed them straight back to the admin UI,\n * and the drafts table accumulated arbitrary caller-controlled JSON\n * (`plan/admin-api-hardening` F002). Validating at the write closes the\n * asymmetry — the stored draft is now always a subset of what the entity\n * can actually hold.\n */\nexport async function saveDraft<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n data: InferUpdateInput<F>,\n locale = DEFAULT_LOCALE_SENTINEL,\n user?: { id: string; name?: string },\n): Promise<DraftEntry<InferUpdateInput<F>>> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) throw new EntityCapabilityError(ctx.entity.name, 'drafts')\n\n const projected = projectToEntityFields(ctx.entity, data as Record<string, unknown>)\n\n // `projectToEntityFields` is a field-NAME whitelist — it never inspects a\n // value, so a `field.blockTree()` column in a draft took arbitrary JSON, and\n // `editor-state` hands the stored draft back as the CANVAS BASE STATE\n // (editor F002). Publishing is already gated by the entity write path, but\n // waiting until publish means the editor loads a tree it cannot represent.\n // Same validator, same judgement — asked here because the draft blob carries\n // no Zod schema of its own.\n ctx.admin.assertBlockTreesValid(projected)\n // F116, same reasoning — and this is precisely the write path the finding\n // was filed against: dava's home-page DRAFT already holds a mismatched\n // media reference. Grandfathering against the draft ALREADY STORED (rather\n // than validating strictly) is what keeps that draft's next autosave\n // working instead of turning \"a broken image\" into \"a draft that can never\n // be saved again\".\n //\n // The lookup is skipped entirely when the PROJECTED payload carries no\n // block-tree field at all — `assertMediaKindsValid` would no-op anyway\n // (same optimization as `saveTranslation`'s), and most entities with\n // drafts have no block-tree field to begin with.\n const hasBlockTreeValue = blockTreeFieldsOf(ctx.entity).some(({ name }) =>\n Object.hasOwn(projected, name),\n )\n const existing = hasBlockTreeValue ? await client.findOne({ entityId, locale }) : null\n await ctx.admin.assertMediaKindsValid(\n projected,\n existing?.data as Record<string, unknown> | undefined,\n )\n\n const now = new Date()\n const createdBy = user?.id ?? ''\n const createdByName = user?.name ?? null\n\n const row = await client.upsert(\n {\n entityId,\n locale,\n data: projected,\n createdBy,\n createdByName,\n createdAt: now,\n updatedAt: now,\n },\n {\n target: ['entityId', 'locale'],\n // On conflict, only update the mutable fields — preserve createdBy/createdAt.\n set: { data: projected, updatedAt: now },\n },\n )\n\n return toDraftEntry(row) as DraftEntry<InferUpdateInput<F>>\n}\n\n/**\n * The keys that move a document's canonical URL in the patterns the toolkit\n * ships (`[slug]`, `[parent.path]`) plus the slug mode that decides whether a\n * clash is refused or auto-suffixed. A merge naming one of them, on a ROUTABLE\n * entity, runs the path-conflict check (see `mergeDraft`); any other merge\n * skips its lookups — the staged draft, the row, and `toolkit_paths` (twice\n * under a parent).\n *\n * A pattern built from another field (`/[category]/[slug]`) is not covered:\n * staging only `category` is refused at Publish rather than here.\n */\nconst PATH_KEYS = ['slug', 'slugMode', 'parent'] as const\n\n/**\n * Stage a PARTIAL change: merge `patch` into the entity+locale draft, creating\n * the draft if there is none. Keys not in `patch` are left exactly as the draft\n * (or, absent a draft key, the live row) has them.\n *\n * This is the write every editing surface uses since editor D019 — \"every edit\n * on every surface saves to the document's one shared draft\". `saveDraft`\n * REPLACES the blob, which is right for a surface that owns the whole document\n * (the menu editor) and wrong for one that owns a few fields of it: page\n * settings saving `title` through `saveDraft` would erase the block edits the\n * canvas staged a second earlier. A draft is therefore a patch over the row, and\n * every reader that shows editable values reads row ⊕ draft.\n *\n * Judged like the entity PATCH it replaces, BEFORE it is stored:\n * - projected through the field whitelist (`internal`, undeclared keys dropped)\n * - `assertBlockTreesValid` + `assertMediaKindsValid` (grandfathered against the\n * stored draft, same as `saveDraft`)\n * - `assertUpdateValid` — the entity's own update schema. `saveDraft` never ran\n * it, which was harmless while the form's Publish still PATCHed; moving a\n * surface into the draft must not move its refusal to Publish, where the\n * author is no longer looking at the field.\n * - `assertPathAvailable` — the path-conflict guard, as a dry run, when the\n * merge moves the URL (see {@link PATH_KEYS}) on a routable entity (F150).\n *\n * 🔴 ONE STATEMENT, not read-modify-write. The merge is `data || patch` inside\n * `ON CONFLICT DO UPDATE` (`TableClient.upsert`'s `mergeJsonb`), so it cannot\n * lose a concurrent write: a read-then-upsert that interleaved with op-commit's\n * or a bound field's stage would write back the draft as it was read — silently\n * reverting the other writer's change. `writeDraft` merges the same way, so\n * neither can erase the other. Top-level `||`, a SHALLOW merge.\n *\n * Resolves `null` when the projected patch is EMPTY and no draft exists: nothing\n * the entity declares was sent, so nothing is staged and no empty draft row is\n * created.\n *\n * ⚠️ The rest of the write path's hooks — `pathParent()`'s cycle check, the\n * partial-unique slug index, `sluggable()`'s regeneration — still run only at\n * Publish, inside its transaction, where a refusal stops the whole publish. The\n * path check here answers \"as of now\"; Publish runs the real guard again.\n */\nexport async function mergeDraft<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n patch: InferUpdateInput<F>,\n locale = DEFAULT_LOCALE_SENTINEL,\n user?: { id: string; name?: string },\n): Promise<DraftEntry<InferUpdateInput<F>> | null> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) throw new EntityCapabilityError(ctx.entity.name, 'drafts')\n\n const projected = projectToEntityFields(ctx.entity, patch as Record<string, unknown>)\n // Nothing the entity declares: stage nothing. Upserting `{}` would CREATE an\n // empty draft, and the document would then read \"Draft saved · not live\n // yet\" about a change that does not exist.\n if (Object.keys(projected).length === 0) {\n const current = await client.findOne({ entityId, locale })\n return current ? (toDraftEntry(current) as DraftEntry<InferUpdateInput<F>>) : null\n }\n ctx.admin.assertBlockTreesValid(projected)\n ctx.admin.assertUpdateValid(projected)\n const hasBlockTreeValue = blockTreeFieldsOf(ctx.entity).some(({ name }) =>\n Object.hasOwn(projected, name),\n )\n const touchesPath =\n ctx.entity.frontend?.path !== undefined &&\n PATH_KEYS.some((key) => Object.hasOwn(projected, key))\n const existing =\n hasBlockTreeValue || touchesPath ? await client.findOne({ entityId, locale }) : null\n await ctx.admin.assertMediaKindsValid(\n projected,\n existing?.data as Record<string, unknown> | undefined,\n )\n // A staged slug that another document's URL already holds is refused NOW,\n // beside the field the author typed it in — not first at Publish (editor\n // PR60, F150). Checked against what the draft will hold after this merge,\n // since an earlier stage may have set `slugMode` or `parent`.\n if (touchesPath) {\n const staged = {\n ...((existing?.data as Record<string, unknown> | undefined) ?? {}),\n ...projected,\n }\n if (locale === DEFAULT_LOCALE_SENTINEL) {\n await ctx.admin.assertPathAvailable(entityId, staged)\n } else {\n // Checked the way Publish writes it: `updateForLocale` sends a locale's\n // TRANSLATABLE fields through that locale's guard and the rest (a\n // `parent`, a non-translatable slug) through `update()`, whose guard\n // checks the default locale. One guard over everything would test a\n // shared `parent` against the wrong locale's URLs.\n const fields = ctx.entity.allFields as Record<string, { translatable?: boolean } | undefined>\n const translatable: Record<string, unknown> = {}\n const shared: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(staged)) {\n if (fields[key]?.translatable === true) translatable[key] = value\n else shared[key] = value\n }\n const moves = (part: Record<string, unknown>) =>\n PATH_KEYS.some((key) => Object.hasOwn(part, key) && Object.hasOwn(projected, key))\n if (moves(translatable)) await ctx.admin.assertPathAvailable(entityId, translatable, locale)\n if (moves(shared)) await ctx.admin.assertPathAvailable(entityId, shared)\n }\n }\n\n const now = new Date()\n const row = await client.upsert(\n {\n entityId,\n locale,\n data: projected,\n createdBy: user?.id ?? '',\n createdByName: user?.name ?? null,\n createdAt: now,\n updatedAt: now,\n },\n { target: ['entityId', 'locale'], set: { updatedAt: now }, mergeJsonb: ['data'] },\n )\n return toDraftEntry(row) as DraftEntry<InferUpdateInput<F>>\n}\n\n/**\n * Get the draft for an entity+locale, or null if none exists (or the\n * entity doesn't support drafts).\n */\nexport async function getDraft<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<DraftEntry<InferUpdateInput<F>> | null> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) return null\n const row = await client.findOne({ entityId, locale })\n return row ? (toDraftEntry(row) as DraftEntry<InferUpdateInput<F>>) : null\n}\n\n/**\n * Delete (discard) a draft for an entity+locale. Returns whether\n * anything was deleted; safely no-ops on entities without drafts.\n */\nexport async function deleteDraft(\n ctx: ContentContext,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<boolean> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) return false\n const deleted = await client.deleteMany({ entityId, locale })\n return deleted.length > 0\n}\n\n/**\n * Publish a draft: make THIS document's staged changes live.\n *\n * \"Publish\" means one thing on every surface (editor D019): the draft's fields\n * are written to the row for the draft's locale AND the row is put live. The\n * draft is then gone and, if versionable, a version snapshot records what went\n * live.\n *\n * Locale routing mirrors the admin PATCH handler (and the taxonomy fix\n * in 10a7cc2b): a default-locale draft writes the base row; a\n * non-default-locale draft routes translatable fields to\n * `saveTranslation` (→ `{entity}_translations`), `status` to the\n * per-locale status table, and only non-translatable fields (which have\n * no per-locale home) to the base row. Without this split, publishing\n * an `et` draft overwrote the default-locale base row wholesale.\n *\n * ## ONE TRANSACTION (editor PR60, R055 §4)\n *\n * The row write, the locale status, `_overrides`, the draft delete and the\n * version snapshot commit together or not at all. They used to be five separate\n * statements, so a failure part-way left a document whose content was live and\n * whose draft still claimed to be pending — or, worse, whose draft was gone while\n * the row write had not landed.\n *\n * The draft is CLAIMED first, by deleting it and publishing what the delete\n * returned. The delete takes the row lock, so an op-commit that is mid-flight on\n * the same draft either committed before (and its edit is in what we publish) or\n * blocks until we commit and then retries its SERIALIZABLE transaction — or, if it\n * had not read the draft yet, inserts a fresh draft holding only its own patch.\n * Either way it does not write a stale copy back over the published one. The editor also flushes and awaits its own in-flight saves\n * before it calls this, so the lock is the backstop, not the mechanism.\n *\n * Path-index and write notifications fire after COMMIT, not inside it —\n * `AdminClient.afterCommit` defers them on a tracked transaction.\n */\nexport async function publishDraft<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n user?: { id: string; name?: string },\n): Promise<InferEntityDTO<F>> {\n validateLocale(locale)\n if (!(await getDraftsClient(ctx))) {\n throw new EntityCapabilityError(ctx.entity.name, 'drafts')\n }\n\n // Previous state for the version delta — locale-merged, so the delta compares\n // against what this locale's readers actually saw. Read BEFORE the\n // transaction: it is the committed state the publish replaces.\n const previousState: InferEntityDTO<F> | null = ctx.versionableOptions\n ? await ctx.admin.findById(entityId, localeFindOptions(locale))\n : null\n\n return ctx.db.transaction(async (tx) => {\n // Every table client below binds to `tx` through this context — drafts,\n // locale status, versions and the `_version` column write.\n const txCtx: ContentContext<F> = { ...ctx, db: tx }\n const draftsClient = await getDraftsClient(txCtx)\n if (!draftsClient) throw new EntityCapabilityError(ctx.entity.name, 'drafts')\n const [claimed] = await draftsClient.deleteMany({ entityId, locale })\n if (!claimed) {\n throw new DraftNotFoundError(entityId, locale)\n }\n\n // `?? {}` because the column is `jsonb NOT NULL`, and that forbids SQL NULL\n // while permitting the JSON literal `null` — which deserializes to JS `null`.\n // A bare cast then made `draftData.status` a TypeError, so a malformed row\n // 500'd the publish route instead of taking the ordinary \"nothing declared,\n // publish the row as-is\" path below. Nothing in this package writes such a\n // row; a seed, a migration or a hand-edit can.\n const claimedData = (claimed.data ?? {}) as Record<string, unknown>\n // `publishedAt` is Publish's to set, like `status` below: a staged value\n // would let whoever wrote the draft back- or forward-date the publication.\n // publishable()'s hook fills it when the UPDATE carries none — which, with\n // the staged value dropped, is every publish: it records the LATEST\n // publication, not the first.\n const { publishedAt: _stagedPublishedAt, ...draftData } = claimedData\n // \"Publish\" means MAKE LIVE, always — the status is not the draft's to\n // decide. Block-editor drafts carry no `status` at all, and before D019 a\n // form-saved draft could carry `status: 'draft'`, which then won: Publish\n // wrote the content and left the document unpublished, so the same button\n // meant \"publish\" on one surface and \"stage\" on another. The publishable()\n // behavior auto-sets `publishedAt` on the transition. The publish-permission\n // gate lives in the admin route, not here.\n const updateData = projectToEntityFields(ctx.entity, { ...draftData, status: 'published' })\n const result = await applyLocaleRoutedUpdate(txCtx, entityId, updateData, locale, tx)\n\n // F009 — promote the page's chrome overrides.\n //\n // `projectToEntityFields` drops every `internal: true` field, deliberately, so\n // `_overrides` cannot ride the generic promotion. That guard is NOT relaxed:\n // this is one named field, written through the trusted `updateInternal` path\n // that `with-overrides.ts` documents as \"the only path that can mutate it\".\n // Before F009 that call site did not exist, so an override survived in the\n // draft buffer and never reached the column — the op committed, the canvas\n // updated, and a reload brought the hidden header back.\n //\n // AFTER the main update, so the override never lands against content that\n // failed to — and inside the same transaction, so neither lands alone.\n // `_overrides` is not translatable, so it belongs on the base row for every\n // locale.\n const overrides = readOverridesForPublish(ctx.entity, draftData)\n if (overrides !== undefined) {\n await ctx.admin.updateInternal(entityId, { _overrides: overrides }, { tx })\n }\n\n if (ctx.versionableOptions) {\n await createVersionSnapshot(txCtx, entityId, result, previousState, locale, user)\n }\n\n return result\n })\n}\n\n/**\n * The page's override map from a draft payload, when the entity declares the\n * field and the draft carries one.\n *\n * Returns `undefined` — never `{}` — so an ordinary content publish does not\n * touch the column. Shape-guarded because a draft payload is stored jsonb: an\n * array or a scalar is malformed data, not an override map. ← F009\n */\nfunction readOverridesForPublish(\n entity: { allFields: Record<string, unknown>; behaviors?: readonly { name: string }[] },\n draftData: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n // The BEHAVIOR, not just a field with the right name — a draft payload is\n // stored jsonb and can be crafted or legacy, so this must not become a way\n // to promote arbitrary JSON into an unrelated internal field on an entity\n // that never opted in. Requiring the behavior also means `beforeUpdate` runs\n // `validateOverridesMap` on the way through `updateInternal`.\n if (!(entity.behaviors ?? []).some((b) => b.name === 'withOverrides')) return undefined\n const field = entity.allFields._overrides as { internal?: boolean } | undefined\n if (field === undefined || field.internal !== true) return undefined\n const value = draftData._overrides\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined\n return value as Record<string, unknown>\n}\n\n/**\n * Quick existence check for list badges — does a draft exist?\n */\nexport async function hasDraft(\n ctx: ContentContext,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<boolean> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) return false\n return client.exists({ entityId, locale })\n}\n\n/**\n * Count drafts for this entity type, optionally filtered by locale.\n * Single indexed COUNT — used by dashboard widgets.\n */\nexport async function countDrafts(\n ctx: ContentContext,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<number> {\n validateLocale(locale)\n const client = await getDraftsClient(ctx)\n if (!client) return 0\n return client.count({ locale })\n}\n\n/**\n * Check draft existence for multiple entities in one query. Returns\n * a set of entity IDs that have a draft for `locale`.\n */\nexport async function hasDraftBatch(\n ctx: ContentContext,\n entityIds: string[],\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<Set<string>> {\n validateLocale(locale)\n const result = new Set<string>()\n if (entityIds.length === 0) return result\n const client = await getDraftsClient(ctx)\n if (!client) return result\n\n const rows = await client.findMany({\n where: { entityId: { in: entityIds }, locale },\n })\n for (const row of rows) {\n result.add(row.entityId as string)\n }\n return result\n}\n\n/**\n * Compare the current draft with the live entity. Returns field-level\n * diffs between the published data and the draft (only for fields\n * present in the draft — missing fields are NOT \"changed to empty\").\n */\nexport async function compareDraftWithCurrent<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n locale = DEFAULT_LOCALE_SENTINEL,\n): Promise<{\n diffs: FieldDiff[]\n current: InferEntityDTO<F>\n draft: DraftEntry<InferUpdateInput<F>>\n} | null> {\n validateLocale(locale)\n const draft = await getDraft(ctx, entityId, locale)\n if (!draft) return null\n\n // Fetch the live entity with the same locale context as the draft.\n const config = getContentConfig()\n const defaultLocale = config.defaultLocale\n const findOptions =\n locale === DEFAULT_LOCALE_SENTINEL ? { defaultLocale } : { locale, defaultLocale }\n const current = await ctx.admin.findById(entityId, findOptions)\n if (!current) return null\n\n const fieldConfigs = ctx.entity.allFields as Record<string, FieldConfig> | undefined\n const currentRecord = current as Record<string, unknown>\n const draftRecord = draft.data as Record<string, unknown>\n const currentSubset: Record<string, unknown> = {}\n for (const key of Object.keys(draftRecord)) {\n currentSubset[key] = currentRecord[key]\n }\n const diffs = diffFields(currentSubset, draftRecord, 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\n return { diffs, current, draft }\n}\n","/**\n * Per-locale publish status — independent draft/published state for\n * each locale of a publishable + translatable entity.\n *\n * Capability: requires `publishable() + at least one translatable\n * field` (the `_locale_status` table is registered only for that\n * combination). Reads silently return falsy on entities without it;\n * `setLocaleStatus` throws `EntityCapabilityError`.\n */\n\nimport 'server-only'\n\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport type { InferUpdateInput } from '@murumets-ee/entity/admin'\nimport { EntityCapabilityError } from './errors.js'\nimport { type ContentContext, getLocaleStatusClient } from './internal/context.js'\nimport { upsertLocaleStatusRow } from './locale-status-slot.js'\nimport { validateLocale } from './locale-utils.js'\nimport { getContentConfig } from './plugin.js'\nimport type { LocalePublishStatus } from './types.js'\n\n/**\n * Get per-locale publish status overrides for an entity.\n * Missing locale = inherits the base entity status (no row written).\n *\n * **Not permission-gated, deliberately — see {@link getLocaleStatusBatch}.**\n */\nexport async function getLocaleStatus(\n ctx: ContentContext,\n entityId: string,\n): Promise<LocalePublishStatus[]> {\n const map = await getLocaleStatusBatch(ctx, [entityId])\n const entry = map.get(entityId)\n if (!entry) return []\n return Object.values(entry)\n}\n\n/**\n * Set per-locale publish status. Upserts into the locale_status\n * table — except when `locale === defaultLocale`, which updates the\n * base entity status column directly (the per-locale table only\n * stores overrides for non-default locales).\n *\n * **Both branches are publish-gated by the entity layer (D041).** The default\n * branch inherits it from `AdminClient.update`; the non-default branch routes\n * through `AdminClient.updateForLocale`, which asserts the SAME\n * `assertPublishAuthority` from the caller's payload before it hands the write\n * to the locale-status writer. This is deliberately a delegation and not a\n * second predicate in this package: `<entity>_locale_status` is the table the\n * public read predicate (`COALESCE(locale_status.status, main.status)`) resolves\n * against, so a direct `TableClient.upsert` here — which is what this function\n * used to do — is a fourth publish door, reachable from any plugin/route/job\n * holding a `ContentClient` (a PUBLIC subpath export) with no `publish` grant\n * ever consulted.\n */\nexport async function setLocaleStatus<F extends Record<string, FieldConfig>>(\n ctx: ContentContext<F>,\n entityId: string,\n locale: string,\n status: 'draft' | 'published',\n): Promise<void> {\n validateLocale(locale)\n const config = getContentConfig()\n\n // Default locale → update base entity status column.\n if (locale === config.defaultLocale) {\n const data: Record<string, unknown> = { status }\n if (status === 'published') data.publishedAt = new Date()\n await ctx.admin.update(entityId, data as InferUpdateInput<F>)\n return\n }\n\n const client = await getLocaleStatusClient(ctx)\n if (!client) {\n // Per-locale status only exists for publishable + translatable\n // entities. Calling on a non-translatable publishable (where the\n // only valid locale is the default) is a programmer error.\n // Checked BEFORE delegating: `updateForLocale` would otherwise fall back to\n // the base `status` column for a missing writer, silently publishing the\n // default locale instead of refusing.\n throw new EntityCapabilityError(ctx.entity.name, 'localeStatus')\n }\n\n // The writer we hand down is the RAW upsert, not this function — the\n // delegation terminates here rather than re-entering `updateForLocale`.\n //\n // Routing through `updateForLocale` rather than calling the upsert directly\n // is what closes the publish door: this branch used to write\n // `<entity>_locale_status` with no permission check at all, so any holder of\n // this public export could make a translation publicly live without\n // `publish`. `updateForLocale` asserts publish authority at the operation\n // entry (D041), so the gate is inherited rather than duplicated here.\n //\n // Widened through `Record<string, unknown>` for the same reason the\n // default-locale branch above does it: `InferUpdateInput<F>` is keyed on the\n // entity's own field map, which a generic `F` cannot be proven to overlap.\n const localeData: Record<string, unknown> = { status }\n await ctx.admin.updateForLocale(entityId, localeData as InferUpdateInput<F>, locale, {\n defaultLocale: config.defaultLocale,\n localeStatusWriter: (id, loc, s) => upsertLocaleStatusRow(client, id, loc, s),\n })\n}\n\n/**\n * Get per-locale publish status for multiple entities in one query.\n *\n * ## Why this read is NOT permission-gated (and what would gate it)\n *\n * It returns publish-state METADATA only — `{ locale, status, publishedAt }` —\n * never field data, so it is not a content-disclosure door the way the write\n * side above is a publish door. What it does expose is an existence oracle for\n * unpublished TRANSLATIONS: a principal without `view_unpublished` learns that\n * `et` is a draft, which `QueryClient` would have hidden. The correct predicate\n * is therefore `view_unpublished` (D017), NOT `publish`.\n *\n * A CodeRabbit pass proposed gating it on exactly that predicate. Deliberately\n * declined, on the strength of **F063**: `view_unpublished` governs `QueryClient`\n * reads — the PUBLIC delivery surface — and is not a disclosure boundary over\n * the admin API, where `AdminClient` shows editors their own drafts by design.\n * `ContentClient` is an in-process server-side API whose consumers are plugins,\n * routes and jobs on the admin side, so gating it here would extend the control\n * onto the surface F063 explicitly recommended NOT extending it to. The concrete\n * cost would be an editor lacking `view_unpublished` no longer able to see that\n * their OWN translation is still a draft.\n *\n * The asymmetry with the write side above is real, not convenient:\n * `setLocaleStatus` MINTS public visibility, so it is gated regardless of which\n * surface calls it. This read hands publish-state metadata to a caller who\n * already holds `view` on the row — they needed its id to ask.\n *\n * It is not applied here because the predicate (`mayViewUnpublishedEntity`) is\n * module-private to `@murumets-ee/entity` — re-deriving it in this package\n * would create exactly the second, drifting copy of a security predicate that\n * D041 exists to eliminate, and there is no read-path equivalent of\n * `updateForLocale` to delegate to. The one in-repo caller\n * (`admin-ui`'s versions route) is already gated by `checkPermission`. Closing\n * this properly means exporting the predicate from `@murumets-ee/entity` and\n * dropping non-`published` rows for principals without it — tracked as the\n * read-side sibling of F060.\n */\nexport async function getLocaleStatusBatch(\n ctx: ContentContext,\n entityIds: string[],\n): Promise<Map<string, Record<string, LocalePublishStatus>>> {\n const result = new Map<string, Record<string, LocalePublishStatus>>()\n if (entityIds.length === 0) return result\n const client = await getLocaleStatusClient(ctx)\n if (!client) return result\n\n const rows = await client.findMany({ where: { entityId: { in: entityIds } } })\n for (const r of rows) {\n const eid = r.entityId as string\n const locale = r.locale as string\n let entry = result.get(eid)\n if (!entry) {\n entry = {}\n result.set(eid, entry)\n }\n entry[locale] = {\n locale,\n status: r.status as 'draft' | 'published',\n publishedAt: (r.publishedAt as Date | null) ?? null,\n }\n }\n return result\n}\n","/**\n * Translation status reporting — per-locale completion of translatable\n * fields for one entity, batches of entities, or the whole entity type.\n *\n * Capability: requires the entity to have at least one translatable\n * field. All methods return falsy / empty when the entity has no\n * translatable fields (read-only and defensive).\n */\n\nimport 'server-only'\n\nimport { getContext, requireSiteFor } from '@murumets-ee/core'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { buildPartitionCondition, type FieldConfig } from '@murumets-ee/entity'\nimport { and, inArray } from 'drizzle-orm'\nimport { InvalidLocaleError } from './errors.js'\nimport type { ContentContext } from './internal/context.js'\nimport { getContentConfig } from './plugin.js'\nimport type { TranslationReport, TranslationStatus } from './types.js'\n\n/**\n * The locales a status report covers and the default it skips: the request's\n * — which the admin narrows to its SITE's locales and default (plan `multisite`\n * phase 03) — else the configured content locales.\n */\nfunction statusLocales(): { locales: { code: string }[]; defaultLocale: string } {\n const config = getContentConfig()\n const ctx = getContext()\n const codes = ctx?.availableLocales ?? config.locales.map((l) => l.code)\n return {\n locales: codes.map((code) => ({ code })),\n defaultLocale: ctx?.defaultLocale ?? config.defaultLocale,\n }\n}\n\n/**\n * For one entity, compute per-locale completion status across every\n * translatable field. Default-locale rows are skipped.\n */\nfunction computeStatusForEntity(\n translatableFields: string[],\n translatedByLocale: Map<string, Record<string, unknown>> | undefined,\n locales: { code: string }[],\n defaultLocale: string,\n): TranslationStatus {\n const status: TranslationStatus = {}\n for (const locale of locales) {\n if (locale.code === defaultLocale) continue\n const translated = translatedByLocale?.get(locale.code) ?? {}\n const missing: string[] = []\n for (const fieldName of translatableFields) {\n if (translated[fieldName] === undefined || translated[fieldName] === null) {\n missing.push(fieldName)\n }\n }\n status[locale.code] = {\n complete: missing.length === 0,\n total: translatableFields.length,\n translated: translatableFields.length - missing.length,\n missing,\n }\n }\n return status\n}\n\n/**\n * Get translation status for an entity — per-locale completion report.\n */\nexport async function getTranslationStatus(\n ctx: ContentContext,\n entityId: string,\n): Promise<TranslationStatus> {\n // Single-entity is the batch path with one ID — keeps the two\n // codepaths from drifting.\n const map = await getTranslationStatusBatch(ctx, [entityId])\n return map.get(entityId) ?? {}\n}\n\n/**\n * Get translation status for multiple entities in a single query.\n * O(1) DB roundtrips regardless of `entityIds.length`.\n */\nexport async function getTranslationStatusBatch(\n ctx: ContentContext,\n entityIds: string[],\n): Promise<Map<string, TranslationStatus>> {\n const result = new Map<string, TranslationStatus>()\n if (entityIds.length === 0) return result\n\n const { locales, defaultLocale } = statusLocales()\n\n const translatableFields = Object.entries(ctx.entity.allFields as Record<string, FieldConfig>)\n .filter(([_, f]) => f.translatable)\n .map(([name]) => name)\n if (translatableFields.length === 0) return result\n\n // Translation tables are generated in @murumets-ee/entity (with FK\n // refs); accessed via schemaRegistry + direct Drizzle here.\n const translationTable = schemaRegistry.get(`${ctx.entity.name}_translations`)\n if (!translationTable) return result\n\n // A site-bound entity's translation rows are confined to the request's\n // site (multisite D003): ids alone are no boundary — a caller holding\n // another site's id must learn nothing about its translations.\n const site = await requireSiteFor(ctx.entity)\n const byIds = inArray(translationTable.entityId, entityIds)\n const translations = await ctx.db\n .select()\n .from(translationTable)\n .where(\n site !== undefined\n ? and(byIds, buildPartitionCondition(translationTable, { site }, ctx.entity.name))\n : byIds,\n )\n\n // Group: entityId → locale → { fieldName: value }\n const translationsByEntity = new Map<string, Map<string, Record<string, unknown>>>()\n for (const t of translations) {\n const row = t as Record<string, unknown>\n const eid = row.entityId as string\n const locale = row.locale as string\n const fieldValues: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n fieldValues[fieldName] = row[fieldName]\n }\n let entry = translationsByEntity.get(eid)\n if (!entry) {\n entry = new Map()\n translationsByEntity.set(eid, entry)\n }\n entry.set(locale, fieldValues)\n }\n\n for (const entityId of entityIds) {\n result.set(\n entityId,\n computeStatusForEntity(\n translatableFields,\n translationsByEntity.get(entityId),\n locales,\n defaultLocale,\n ),\n )\n }\n\n return result\n}\n\n/**\n * Roll up translation status across every entity of this type.\n *\n * Defaults to the first non-default configured locale; pass\n * `options.locale` to target a specific one. Returns counts of\n * complete / partial / missing entities for that locale.\n */\nexport async function getTranslationReport(\n ctx: ContentContext,\n options?: { locale?: string },\n): Promise<TranslationReport> {\n const { locales, defaultLocale } = statusLocales()\n const targetLocale = options?.locale ?? locales.find((l) => l.code !== defaultLocale)?.code\n if (!targetLocale) return { total: 0, complete: 0, partial: 0, missing: 0 }\n // A locale the site does not serve has no report on that site.\n if (!locales.some((l) => l.code === targetLocale)) {\n throw new InvalidLocaleError(\n targetLocale,\n locales.map((l) => l.code),\n )\n }\n\n const entities = await ctx.admin.findMany()\n if (entities.length === 0) return { total: 0, complete: 0, partial: 0, missing: 0 }\n\n // Single batched query covers every entity at once.\n const statusMap = await getTranslationStatusBatch(\n ctx,\n entities.map((e) => e.id),\n )\n\n let complete = 0\n let partial = 0\n let missing = 0\n for (const entity of entities) {\n const status = statusMap.get(entity.id)\n const localeStatus = status?.[targetLocale]\n if (!localeStatus || localeStatus.translated === 0) missing++\n else if (localeStatus.complete) complete++\n else partial++\n }\n\n return { total: entities.length, complete, partial, missing }\n}\n","/**\n * ContentClient — thin shell over per-feature modules.\n *\n * Wraps AdminClient with version history (`./versions.ts`), draft\n * overlays (`./drafts.ts`), translation status reporting\n * (`./translation-status.ts`), per-locale publish status\n * (`./locale-status.ts`), and block-layout operations (`./layout.ts`).\n *\n * The class is generic over the entity's `AllFields` map: when\n * constructed with a typed entity, every method's input/output is\n * inferred end-to-end. When constructed with a runtime `Entity`\n * (admin-ui's heterogeneous entity loops), the generic defaults to\n * `Record<string, FieldConfig>` and methods accept/return the loose\n * shape — same behavior as the pre-generic version.\n *\n * Usage:\n * import { createContentClient } from '@murumets-ee/content/client'\n * const pages = createContentClient(Page)\n * await pages.create({ title: 'Hello', layout: [...] })\n */\n\nimport 'server-only'\n\nimport { getApp, type Logger } from '@murumets-ee/core'\nimport { createAdminClient } from '@murumets-ee/core/clients'\nimport type { Entity, FieldConfig } from '@murumets-ee/entity'\nimport type {\n AdminClient,\n FindManyOptions,\n InferCreateInput,\n InferEntityDTO,\n InferUpdateInput,\n} from '@murumets-ee/entity/admin'\nimport { getVersionableOptions } from './behaviors/versionable.js'\nimport {\n compareDraftWithCurrent,\n countDrafts,\n deleteDraft,\n getDraft,\n hasDraft,\n hasDraftBatch,\n mergeDraft,\n publishDraft,\n saveDraft,\n} from './drafts.js'\nimport { type ContentContext, createContentContext } from './internal/context.js'\nimport { calculateDelta } from './internal/projection.js'\nimport { getLocaleStatus, getLocaleStatusBatch, setLocaleStatus } from './locale-status.js'\nimport {\n getTranslationReport,\n getTranslationStatus,\n getTranslationStatusBatch,\n} from './translation-status.js'\nimport {\n compareVersions,\n createVersionSnapshot,\n getVersion,\n getVersions,\n pruneVersions,\n restoreVersion,\n saveVersionSnapshot,\n writeVersionColumn,\n} from './versions.js'\n\nexport interface ContentClientConfig<\n AllFields extends Record<string, FieldConfig> = Record<string, FieldConfig>,\n> {\n admin: AdminClient<AllFields>\n entity: Entity<AllFields>\n logger?: Logger\n}\n\nexport class ContentClient<\n AllFields extends Record<string, FieldConfig> = Record<string, FieldConfig>,\n> {\n private ctx: ContentContext<AllFields>\n\n constructor(config: ContentClientConfig<AllFields>) {\n const versionableOptions = getVersionableOptions(config.entity.behaviors)\n this.ctx = createContentContext(config, versionableOptions)\n }\n\n // -----------------------------------------------------------------\n // CRUD (delegated to AdminClient + version snapshots)\n // -----------------------------------------------------------------\n\n async create(data: InferCreateInput<AllFields>): Promise<InferEntityDTO<AllFields>> {\n const result = await this.ctx.admin.create(data)\n if (this.ctx.versionableOptions) {\n await saveVersionSnapshot(this.ctx, result.id, result, null, false)\n }\n return result\n }\n\n findById(id: string, options?: { locale?: string }): Promise<InferEntityDTO<AllFields> | null> {\n return this.ctx.admin.findById(id, options)\n }\n\n findMany(options?: FindManyOptions): Promise<InferEntityDTO<AllFields>[]> {\n return this.ctx.admin.findMany(options)\n }\n\n /**\n * Base-row update + version snapshot. Subject to AdminClient's\n * cross-locale write guard (SD021): under a non-default request-context\n * locale, translatable fields throw `CrossLocaleWriteError` — that's\n * enforcement, not a gap. Locale-belonging writes go through the draft\n * flow (`saveDraft` → `publishDraft`) or `admin.updateForLocale` directly.\n */\n async update(id: string, data: InferUpdateInput<AllFields>): Promise<InferEntityDTO<AllFields>> {\n let previousState: InferEntityDTO<AllFields> | null = null\n if (this.ctx.versionableOptions) {\n previousState = await this.ctx.admin.findById(id)\n }\n\n const result = await this.ctx.admin.update(id, data)\n\n if (this.ctx.versionableOptions && previousState) {\n const delta = calculateDelta(previousState, result)\n const version = await saveVersionSnapshot(this.ctx, id, result, delta, false)\n if (version !== undefined) {\n await writeVersionColumn(this.ctx, id, version)\n await pruneVersions(this.ctx, id)\n return { ...result, _version: version } as InferEntityDTO<AllFields>\n }\n await pruneVersions(this.ctx, id)\n }\n\n return result\n }\n\n delete(id: string): Promise<unknown> {\n return this.ctx.admin.delete(id)\n }\n\n // -----------------------------------------------------------------\n // Translation delegation\n // -----------------------------------------------------------------\n\n saveTranslation(entityId: string, locale: string, translations: Record<string, unknown>) {\n return this.ctx.admin.saveTranslation(entityId, locale, translations)\n }\n\n deleteTranslation(entityId: string, locale?: string) {\n return this.ctx.admin.deleteTranslation(entityId, locale)\n }\n\n // -----------------------------------------------------------------\n // Version history\n // -----------------------------------------------------------------\n\n getVersions(entityId: string, options?: { limit?: number; offset?: number; locale?: string }) {\n return getVersions(this.ctx, entityId, options)\n }\n\n getVersion(entityId: string, version: number, locale?: string) {\n return getVersion(this.ctx, entityId, version, locale)\n }\n\n compareVersions(entityId: string, v1: number, v2: number, locale?: string) {\n return compareVersions(this.ctx, entityId, v1, v2, locale)\n }\n\n restoreVersion(entityId: string, version: number, locale?: string) {\n return restoreVersion(this.ctx, entityId, version, locale)\n }\n\n pruneVersions(entityId: string, locale?: string) {\n return pruneVersions(this.ctx, entityId, locale)\n }\n\n createVersionSnapshot(\n entityId: string,\n currentData: Record<string, unknown>,\n previousData?: Record<string, unknown> | null,\n locale?: string,\n user?: { id: string; name?: string },\n ) {\n return createVersionSnapshot(this.ctx, entityId, currentData, previousData, locale, user)\n }\n\n // -----------------------------------------------------------------\n // Translation status\n // -----------------------------------------------------------------\n\n getTranslationStatus(entityId: string) {\n return getTranslationStatus(this.ctx, entityId)\n }\n\n getTranslationStatusBatch(entityIds: string[]) {\n return getTranslationStatusBatch(this.ctx, entityIds)\n }\n\n getTranslationReport(options?: { locale?: string }) {\n return getTranslationReport(this.ctx, options)\n }\n\n // -----------------------------------------------------------------\n // Per-locale publish status\n // -----------------------------------------------------------------\n\n getLocaleStatus(entityId: string) {\n return getLocaleStatus(this.ctx, entityId)\n }\n\n setLocaleStatus(entityId: string, locale: string, status: 'draft' | 'published') {\n return setLocaleStatus(this.ctx, entityId, locale, status)\n }\n\n getLocaleStatusBatch(entityIds: string[]) {\n return getLocaleStatusBatch(this.ctx, entityIds)\n }\n\n // -----------------------------------------------------------------\n // Drafts\n // -----------------------------------------------------------------\n\n saveDraft(\n entityId: string,\n data: InferUpdateInput<AllFields>,\n locale?: string,\n user?: { id: string; name?: string },\n ) {\n return saveDraft(this.ctx, entityId, data, locale, user)\n }\n\n mergeDraft(\n entityId: string,\n patch: InferUpdateInput<AllFields>,\n locale?: string,\n user?: { id: string; name?: string },\n ) {\n return mergeDraft(this.ctx, entityId, patch, locale, user)\n }\n\n getDraft(entityId: string, locale?: string) {\n return getDraft(this.ctx, entityId, locale)\n }\n\n deleteDraft(entityId: string, locale?: string) {\n return deleteDraft(this.ctx, entityId, locale)\n }\n\n publishDraft(entityId: string, locale?: string, user?: { id: string; name?: string }) {\n return publishDraft(this.ctx, entityId, locale, user)\n }\n\n hasDraft(entityId: string, locale?: string) {\n return hasDraft(this.ctx, entityId, locale)\n }\n\n countDrafts(locale?: string) {\n return countDrafts(this.ctx, locale)\n }\n\n hasDraftBatch(entityIds: string[], locale?: string) {\n return hasDraftBatch(this.ctx, entityIds, locale)\n }\n\n compareDraftWithCurrent(entityId: string, locale?: string) {\n return compareDraftWithCurrent(this.ctx, entityId, locale)\n }\n}\n\n/**\n * Factory — creates a ContentClient for an entity. Must be called\n * after `createApp()`. Generic flows from the entity definition:\n * `createContentClient(Article)` returns a `ContentClient<typeof\n * Article['allFields']>` typed end-to-end.\n */\nexport function createContentClient<AllFields extends Record<string, FieldConfig>>(\n entity: Entity<AllFields>,\n): ContentClient<AllFields> {\n const admin = createAdminClient(entity)\n return new ContentClient<AllFields>({\n admin,\n entity,\n logger: getApp().logger.child({ content: entity.name }),\n })\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{a as t}from"./schema-D8sBQozw.mjs";var n=e({enumeratePaths:()=>r,getCanonicalPaths:()=>i});async function*r(e,n,r={}){let i=r.pageSize??200,a=r.publishConcurrency??5,{locale:o,entityType:s}=e,c=e.afterId;for(;;){let e={locale:o,isCanonical:!0};s!==void 0&&(e.entityType=s),c!==void 0&&(e.id={gt:c});let r=await n.pathsClient.findMany({where:e,orderBy:[{column:`id`,dir:`asc`}],limit:i});if(r.length===0)break;let l=new Map;for(let e of r){let t=l.get(e.entityType);t?t.push(e):l.set(e.entityType,[e])}let u=new Map;await t([...l.entries()],a,async([e,t])=>{let r=await n.filterPublished(e,t.map(e=>e.entityId),o);u.set(e,r)});for(let e of r)u.get(e.entityType)?.has(e.entityId)&&(yield{path:e.path,entityType:e.entityType,id:e.entityId,updatedAt:e.updatedAt});let d=r[r.length-1];if(!d||(c=d.id,r.length<i))break}}async function i(e,t,n,r){let i=new Map;for(let a=0;a<t.length;a+=500){let o=t.slice(a,a+500),s=await r.pathsClient.findMany({where:{entityType:e,locale:n,isCanonical:!0,entityId:{in:[...o]}},limit:o.length});for(let e of s)i.set(e.entityId,e.path)}return i}export{n,i as r,r as t};
2
- //# sourceMappingURL=enumerate-CBqu63Kp.mjs.map
1
+ import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{c as t}from"./schema-BYrYzsmO.mjs";var n=e({enumeratePaths:()=>r,getCanonicalPaths:()=>i});async function*r(e,n,r={}){let i=r.pageSize??200,a=r.publishConcurrency??5,{locale:o,entityType:s}=e,c=e.afterId;for(;;){let e={locale:o,isCanonical:!0};s!==void 0&&(e.entityType=s),c!==void 0&&(e.id={gt:c});let r=await n.pathsClient.findMany({where:e,orderBy:[{column:`id`,dir:`asc`}],limit:i});if(r.length===0)break;let l=new Map;for(let e of r){let t=l.get(e.entityType);t?t.push(e):l.set(e.entityType,[e])}let u=new Map;await t([...l.entries()],a,async([e,t])=>{let r=await n.filterPublished(e,t.map(e=>e.entityId),o);u.set(e,r)});for(let e of r)u.get(e.entityType)?.has(e.entityId)&&(yield{path:e.path,entityType:e.entityType,id:e.entityId,updatedAt:e.updatedAt});let d=r[r.length-1];if(!d||(c=d.id,r.length<i))break}}async function i(e,t,n,r){let i=new Map;for(let a=0;a<t.length;a+=500){let o=t.slice(a,a+500),s=await r.pathsClient.findMany({where:{entityType:e,locale:n,isCanonical:!0,entityId:{in:[...o]}},limit:o.length});for(let e of s)i.set(e.entityId,e.path)}return i}export{n,i as r,r as t};
2
+ //# sourceMappingURL=enumerate-CYHJKQz6.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"enumerate-CBqu63Kp.mjs","names":[],"sources":["../src/paths/enumerate.ts"],"sourcesContent":["/**\n * Path enumeration for SSG / sitemaps (plan/routing PR06; phase\n * `05-resolution-and-enumeration-api.md` §2).\n *\n * `enumeratePaths` streams the PUBLISHED canonical `(path, entityType, id,\n * updatedAt)` rows for one locale. It powers `getStaticPaths` + the sitemap\n * helper, so it must NEVER leak an unpublished document (CLAUDE.md #1 — the\n * output feeds public build artifacts).\n *\n * **Security + availability.** Two properties matter here:\n * 1. *No unpublished leak.* `toolkit_paths` carries no publish-status column,\n * so each page's rows are publish-checked against the live entity via the\n * injected {@link EnumeratePathsDeps.filterPublished} — an unpublished (or\n * trashed) document is dropped from the stream.\n * 2. *No unbounded fan-out* (CLAUDE.md \"no amplification\"). Work is bounded by\n * PAGE SIZE, not by total document count: rows are keyset-paginated on the\n * immutable `id` PK (one index query per page), and the publish check is\n * BATCHED — ONE call per distinct entity-type bucket per page (a handful of\n * types at most; the routable set is config-time-fixed, never\n * attacker-influenced), never one call per row.\n *\n * **Leaf module.** Every cross-package dependency (the index reader, the publish\n * filter) is injected via `deps`; this module imports no `@murumets-ee/core` and\n * no `server-only` graph. Concrete deps are built from an app in\n * `./resolve-context.ts` (`buildEnumerateDeps`), keeping the resolution surface\n * off the `lumi` CLI command graph (F027 guardrail).\n *\n * Function-form-path entities need NO special handling — their rows were already\n * materialized into `toolkit_paths` at write time (F004 / phase 05 §2), so a\n * plain index read includes them.\n */\n\nimport { mapBounded } from './apply-path.js'\n\n// ---------------------------------------------------------------------------\n// Read surface (a `type` alias, per index-reader.ts's Deviation-1 rationale, so\n// the real `ToolkitPathsClient` stays assignable to it without any cast).\n// ---------------------------------------------------------------------------\n\n/** The projected `toolkit_paths` row the enumeration + batch lookup read. */\nexport type PathEnumRow = {\n id: string\n entityType: string\n entityId: string\n locale: string\n path: string\n updatedAt: Date\n}\n\n/**\n * The (operator-carrying) where shapes enumeration issues: keyset `id > cursor`,\n * `entityId IN (...)` for the batch lookup, plus plain-value equality filters.\n * A subset of the real client's `ColumnOperators` where-clause, so the real\n * `ToolkitPathsClient` is assignable to {@link PathEnumReader}.\n */\nexport type PathEnumWhere = {\n id?: { gt: string }\n entityType?: string\n entityId?: { in: string[] }\n locale?: string\n path?: string\n isCanonical?: boolean\n}\n\n/**\n * The batched keyset read surface {@link enumeratePaths} / {@link getCanonicalPaths}\n * need. NARROWER than Task 1's point-lookup `PathIndexReader` (which only orders\n * by `updatedAt` and takes plain-value where): enumeration needs `id`-ordering,\n * a `{ gt }` cursor, and `{ in }` batching. Construct the real client directly\n * (`toolkitPathsTable.makeClient(db)`) — it satisfies this type.\n */\nexport type PathEnumReader = {\n findMany(options: {\n where?: PathEnumWhere\n orderBy?: Array<{ column: 'id'; dir?: 'asc' | 'desc' }>\n limit?: number\n }): Promise<PathEnumRow[]>\n}\n\n/**\n * Batch publish check: return the SUBSET of `entityIds` that is published in\n * `locale`. Called ONCE per entity-type group per page. The concrete\n * implementation (see `resolve-context.ts`) runs an elevated-context\n * `QueryClient.findMany({ where: inArray(getTable().id, ids), locale })` — so no\n * unpublished/trashed id comes back. Since D017 the publish filter is\n * CONDITIONAL (it holds for any principal without `view_unpublished`); what\n * keeps that true here is that the elevated context is a REACH context which\n * withholds that action, not that the filter is unconditional.\n */\nexport type PublishFilter = (\n entityType: string,\n entityIds: readonly string[],\n locale: string,\n) => Promise<ReadonlySet<string>>\n\n// ---------------------------------------------------------------------------\n// Public surface\n// ---------------------------------------------------------------------------\n\n/** One enumerated path row (phase 05 §2 — exact shape). */\nexport interface PathEntry {\n path: string\n entityType: string\n id: string\n updatedAt: Date\n}\n\n/** Options for {@link enumeratePaths} (phase 05 §2 — exact shape). */\nexport interface EnumeratePathsOptions {\n /** Enumerate this locale's canonical rows only. */\n locale: string\n /** Optionally narrow to a single routable entity type. */\n entityType?: string\n /**\n * Resume the keyset AFTER this `toolkit_paths` row id (exclusive); omit to\n * start from the beginning. The value is the opaque `toolkit_paths` PK the\n * internal page cursor advances on — NOT the yielded `PathEntry.id`, which is\n * the DOCUMENT (entity) id. A cross-request paginating caller (e.g. the\n * content-api `_paths` endpoint) resolves the last consumed entry's row PK and\n * passes it here to continue exactly where the prior page stopped, with no row\n * skipped or repeated. Fully backward-compatible: existing callers omit it and\n * traverse from the start unchanged.\n */\n afterId?: string\n}\n\n/** Injected dependencies for {@link enumeratePaths}. */\nexport interface EnumeratePathsDeps {\n pathsClient: PathEnumReader\n filterPublished: PublishFilter\n}\n\n/** Internal tuning seam (page size / batch concurrency). Real callers omit it. */\nexport interface EnumeratePathsTuning {\n /** Rows per index page. Defaults to {@link ENUMERATE_PAGE_SIZE}. Tests force a small value. */\n pageSize?: number\n /** Peak concurrent publish-check groups. Defaults to {@link PUBLISH_CHECK_CONCURRENCY}. */\n publishConcurrency?: number\n}\n\n/**\n * Index page size — mirrors the rebuild job's `REBUILD_PAGE_SIZE`. Must stay\n * ≤ the `defineTable` `MAX_LIMIT` (1000), which THROWS rather than clamps.\n */\nconst ENUMERATE_PAGE_SIZE = 200\n\n/**\n * Peak concurrent publish-check groups per page. A page holds a handful of\n * distinct entity types at most, so this bound is rarely reached — it exists so\n * the fan-out is EXPLICITLY capped (CLAUDE.md), never `Promise.all` over an\n * unbounded set.\n */\nconst PUBLISH_CHECK_CONCURRENCY = 5\n\n/**\n * Stream the published canonical paths for `opts.locale` (optionally narrowed to\n * `opts.entityType`), keyset-paginated with a per-page batched publish filter.\n * See the module header for the security + fan-out guarantees.\n */\nexport async function* enumeratePaths(\n opts: EnumeratePathsOptions,\n deps: EnumeratePathsDeps,\n tuning: EnumeratePathsTuning = {},\n): AsyncGenerator<PathEntry> {\n const pageSize = tuning.pageSize ?? ENUMERATE_PAGE_SIZE\n const concurrency = tuning.publishConcurrency ?? PUBLISH_CHECK_CONCURRENCY\n const { locale, entityType } = opts\n\n // Seed from `opts.afterId` so a caller can resume mid-stream; undefined starts\n // at the beginning (every existing caller's behavior, unchanged).\n let cursorId: string | undefined = opts.afterId\n for (;;) {\n // Keyset cursor on the immutable `id` PK (the rebuild job's precedent):\n // visits every canonical row exactly once even under concurrent mutation,\n // which OFFSET paging cannot guarantee.\n const where: PathEnumWhere = { locale, isCanonical: true }\n if (entityType !== undefined) where.entityType = entityType\n if (cursorId !== undefined) where.id = { gt: cursorId }\n\n const page = await deps.pathsClient.findMany({\n where,\n orderBy: [{ column: 'id', dir: 'asc' }],\n limit: pageSize,\n })\n if (page.length === 0) break\n\n // Group this page's rows by entity type (bounded: the routable-type set is\n // small + config-fixed), then ONE publish check per group.\n const byType = new Map<string, PathEnumRow[]>()\n for (const row of page) {\n const bucket = byType.get(row.entityType)\n if (bucket) bucket.push(row)\n else byType.set(row.entityType, [row])\n }\n\n const publishedByType = new Map<string, ReadonlySet<string>>()\n await mapBounded([...byType.entries()], concurrency, async ([type, rows]) => {\n const published = await deps.filterPublished(\n type,\n rows.map((r) => r.entityId),\n locale,\n )\n publishedByType.set(type, published)\n })\n\n // Yield only the page's PUBLISHED rows, preserving the id-ascending order.\n for (const row of page) {\n if (publishedByType.get(row.entityType)?.has(row.entityId)) {\n yield { path: row.path, entityType: row.entityType, id: row.entityId, updatedAt: row.updatedAt }\n }\n }\n\n // Advance the cursor to the last (max-id) row of this raw page. `page` is\n // non-empty here (length-0 broke above). Publish-filtering is a POST-filter,\n // so the cursor always steps over every index row regardless of publish state.\n const lastRow = page[page.length - 1]\n if (!lastRow) break\n cursorId = lastRow.id\n if (page.length < pageSize) break\n }\n}\n\n// ---------------------------------------------------------------------------\n// getCanonicalPaths — batched forward lookup (search url re-point), NO publish filter\n// ---------------------------------------------------------------------------\n\n/** Deps for {@link getCanonicalPaths}. */\nexport interface CanonicalPathsBatchDeps {\n pathsClient: PathEnumReader\n}\n\n/**\n * Max ids per batched index query — well under `defineTable`'s `MAX_LIMIT`\n * (1000, which throws rather than clamps). Search result pages are far smaller\n * than this, so this is one query in practice; larger id sets chunk safely.\n */\nconst CANONICAL_BATCH_SIZE = 500\n\n/**\n * The current canonical paths for many `(entityType, entityId)` in ONE locale,\n * as an `entityId → path` map — the batched sibling of `getCanonicalPath`.\n *\n * **No publish filter — deliberate**, matching `getCanonicalPath` (and the\n * search url builder it re-points): a search result's url is display-only and\n * the caller already gates row visibility. One index query per {@link\n * CANONICAL_BATCH_SIZE}-sized chunk (one query total for a normal search page),\n * so it never fans out per row.\n */\nexport async function getCanonicalPaths(\n entityType: string,\n entityIds: readonly string[],\n locale: string,\n deps: CanonicalPathsBatchDeps,\n): Promise<Map<string, string>> {\n const result = new Map<string, string>()\n for (let i = 0; i < entityIds.length; i += CANONICAL_BATCH_SIZE) {\n const chunk = entityIds.slice(i, i + CANONICAL_BATCH_SIZE)\n const rows = await deps.pathsClient.findMany({\n where: { entityType, locale, isCanonical: true, entityId: { in: [...chunk] } },\n limit: chunk.length,\n })\n for (const row of rows) result.set(row.entityId, row.path)\n }\n return result\n}\n"],"mappings":"sJA+JA,eAAuB,EACrB,EACA,EACA,EAA+B,CAAC,EACL,CAC3B,IAAM,EAAW,EAAO,UAAY,IAC9B,EAAc,EAAO,oBAAsB,EAC3C,CAAE,SAAQ,cAAe,EAI3B,EAA+B,EAAK,QACxC,OAAS,CAIP,IAAM,EAAuB,CAAE,SAAQ,YAAa,EAAK,EACrD,IAAe,IAAA,KAAW,EAAM,WAAa,GAC7C,IAAa,IAAA,KAAW,EAAM,GAAK,CAAE,GAAI,CAAS,GAEtD,IAAM,EAAO,MAAM,EAAK,YAAY,SAAS,CAC3C,QACA,QAAS,CAAC,CAAE,OAAQ,KAAM,IAAK,KAAM,CAAC,EACtC,MAAO,CACT,CAAC,EACD,GAAI,EAAK,SAAW,EAAG,MAIvB,IAAM,EAAS,IAAI,IACnB,IAAK,IAAM,KAAO,EAAM,CACtB,IAAM,EAAS,EAAO,IAAI,EAAI,UAAU,EACpC,EAAQ,EAAO,KAAK,CAAG,EACtB,EAAO,IAAI,EAAI,WAAY,CAAC,CAAG,CAAC,CACvC,CAEA,IAAM,EAAkB,IAAI,IAC5B,MAAM,EAAW,CAAC,GAAG,EAAO,QAAQ,CAAC,EAAG,EAAa,MAAO,CAAC,EAAM,KAAU,CAC3E,IAAM,EAAY,MAAM,EAAK,gBAC3B,EACA,EAAK,IAAK,GAAM,EAAE,QAAQ,EAC1B,CACF,EACA,EAAgB,IAAI,EAAM,CAAS,CACrC,CAAC,EAGD,IAAK,IAAM,KAAO,EACZ,EAAgB,IAAI,EAAI,UAAU,CAAC,EAAE,IAAI,EAAI,QAAQ,IACvD,KAAM,CAAE,KAAM,EAAI,KAAM,WAAY,EAAI,WAAY,GAAI,EAAI,SAAU,UAAW,EAAI,SAAU,GAOnG,IAAM,EAAU,EAAK,EAAK,OAAS,GAGnC,GAFI,CAAC,IACL,EAAW,EAAQ,GACf,EAAK,OAAS,GAAU,KAC9B,CACF,CA4BA,eAAsB,EACpB,EACA,EACA,EACA,EAC8B,CAC9B,IAAM,EAAS,IAAI,IACnB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAU,OAAQ,GAAK,IAAsB,CAC/D,IAAM,EAAQ,EAAU,MAAM,EAAG,EAAI,GAAoB,EACnD,EAAO,MAAM,EAAK,YAAY,SAAS,CAC3C,MAAO,CAAE,aAAY,SAAQ,YAAa,GAAM,SAAU,CAAE,GAAI,CAAC,GAAG,CAAK,CAAE,CAAE,EAC7E,MAAO,EAAM,MACf,CAAC,EACD,IAAK,IAAM,KAAO,EAAM,EAAO,IAAI,EAAI,SAAU,EAAI,IAAI,CAC3D,CACA,OAAO,CACT"}
1
+ {"version":3,"file":"enumerate-CYHJKQz6.mjs","names":[],"sources":["../src/paths/enumerate.ts"],"sourcesContent":["/**\n * Path enumeration for SSG / sitemaps (plan/routing PR06; phase\n * `05-resolution-and-enumeration-api.md` §2).\n *\n * `enumeratePaths` streams the PUBLISHED canonical `(path, entityType, id,\n * updatedAt)` rows for one locale. It powers `getStaticPaths` + the sitemap\n * helper, so it must NEVER leak an unpublished document (CLAUDE.md #1 — the\n * output feeds public build artifacts).\n *\n * **Security + availability.** Two properties matter here:\n * 1. *No unpublished leak.* `toolkit_paths` carries no publish-status column,\n * so each page's rows are publish-checked against the live entity via the\n * injected {@link EnumeratePathsDeps.filterPublished} — an unpublished (or\n * trashed) document is dropped from the stream.\n * 2. *No unbounded fan-out* (CLAUDE.md \"no amplification\"). Work is bounded by\n * PAGE SIZE, not by total document count: rows are keyset-paginated on the\n * immutable `id` PK (one index query per page), and the publish check is\n * BATCHED — ONE call per distinct entity-type bucket per page (a handful of\n * types at most; the routable set is config-time-fixed, never\n * attacker-influenced), never one call per row.\n *\n * **Leaf module.** Every cross-package dependency (the index reader, the publish\n * filter) is injected via `deps`; this module imports no `@murumets-ee/core` and\n * no `server-only` graph. Concrete deps are built from an app in\n * `./resolve-context.ts` (`buildEnumerateDeps`), keeping the resolution surface\n * off the `lumi` CLI command graph (F027 guardrail).\n *\n * Function-form-path entities need NO special handling — their rows were already\n * materialized into `toolkit_paths` at write time (F004 / phase 05 §2), so a\n * plain index read includes them.\n */\n\nimport { mapBounded } from './apply-path.js'\n\n// ---------------------------------------------------------------------------\n// Read surface (a `type` alias, per index-reader.ts's Deviation-1 rationale, so\n// the real `ToolkitPathsClient` stays assignable to it without any cast).\n// ---------------------------------------------------------------------------\n\n/** The projected `toolkit_paths` row the enumeration + batch lookup read. */\nexport type PathEnumRow = {\n id: string\n entityType: string\n entityId: string\n locale: string\n path: string\n updatedAt: Date\n}\n\n/**\n * The (operator-carrying) where shapes enumeration issues: keyset `id > cursor`,\n * `entityId IN (...)` for the batch lookup, plus plain-value equality filters.\n * A subset of the real client's `ColumnOperators` where-clause, so the real\n * `ToolkitPathsClient` is assignable to {@link PathEnumReader}.\n */\nexport type PathEnumWhere = {\n id?: { gt: string }\n entityType?: string\n entityId?: { in: string[] }\n locale?: string\n path?: string\n isCanonical?: boolean\n}\n\n/**\n * The batched keyset read surface {@link enumeratePaths} / {@link getCanonicalPaths}\n * need. NARROWER than Task 1's point-lookup `PathIndexReader` (which only orders\n * by `updatedAt` and takes plain-value where): enumeration needs `id`-ordering,\n * a `{ gt }` cursor, and `{ in }` batching. Construct the real client directly\n * (`toolkitPathsTable.makeClient(db)`) — it satisfies this type.\n */\nexport type PathEnumReader = {\n findMany(options: {\n where?: PathEnumWhere\n orderBy?: Array<{ column: 'id'; dir?: 'asc' | 'desc' }>\n limit?: number\n }): Promise<PathEnumRow[]>\n}\n\n/**\n * Batch publish check: return the SUBSET of `entityIds` that is published in\n * `locale`. Called ONCE per entity-type group per page. The concrete\n * implementation (see `resolve-context.ts`) runs an elevated-context\n * `QueryClient.findMany({ where: inArray(getTable().id, ids), locale })` — so no\n * unpublished/trashed id comes back. Since D017 the publish filter is\n * CONDITIONAL (it holds for any principal without `view_unpublished`); what\n * keeps that true here is that the elevated context is a REACH context which\n * withholds that action, not that the filter is unconditional.\n */\nexport type PublishFilter = (\n entityType: string,\n entityIds: readonly string[],\n locale: string,\n) => Promise<ReadonlySet<string>>\n\n// ---------------------------------------------------------------------------\n// Public surface\n// ---------------------------------------------------------------------------\n\n/** One enumerated path row (phase 05 §2 — exact shape). */\nexport interface PathEntry {\n path: string\n entityType: string\n id: string\n updatedAt: Date\n}\n\n/** Options for {@link enumeratePaths} (phase 05 §2 — exact shape). */\nexport interface EnumeratePathsOptions {\n /** Enumerate this locale's canonical rows only. */\n locale: string\n /** Optionally narrow to a single routable entity type. */\n entityType?: string\n /**\n * Resume the keyset AFTER this `toolkit_paths` row id (exclusive); omit to\n * start from the beginning. The value is the opaque `toolkit_paths` PK the\n * internal page cursor advances on — NOT the yielded `PathEntry.id`, which is\n * the DOCUMENT (entity) id. A cross-request paginating caller (e.g. the\n * content-api `_paths` endpoint) resolves the last consumed entry's row PK and\n * passes it here to continue exactly where the prior page stopped, with no row\n * skipped or repeated. Fully backward-compatible: existing callers omit it and\n * traverse from the start unchanged.\n */\n afterId?: string\n}\n\n/** Injected dependencies for {@link enumeratePaths}. */\nexport interface EnumeratePathsDeps {\n pathsClient: PathEnumReader\n filterPublished: PublishFilter\n}\n\n/** Internal tuning seam (page size / batch concurrency). Real callers omit it. */\nexport interface EnumeratePathsTuning {\n /** Rows per index page. Defaults to {@link ENUMERATE_PAGE_SIZE}. Tests force a small value. */\n pageSize?: number\n /** Peak concurrent publish-check groups. Defaults to {@link PUBLISH_CHECK_CONCURRENCY}. */\n publishConcurrency?: number\n}\n\n/**\n * Index page size — mirrors the rebuild job's `REBUILD_PAGE_SIZE`. Must stay\n * ≤ the `defineTable` `MAX_LIMIT` (1000), which THROWS rather than clamps.\n */\nconst ENUMERATE_PAGE_SIZE = 200\n\n/**\n * Peak concurrent publish-check groups per page. A page holds a handful of\n * distinct entity types at most, so this bound is rarely reached — it exists so\n * the fan-out is EXPLICITLY capped (CLAUDE.md), never `Promise.all` over an\n * unbounded set.\n */\nconst PUBLISH_CHECK_CONCURRENCY = 5\n\n/**\n * Stream the published canonical paths for `opts.locale` (optionally narrowed to\n * `opts.entityType`), keyset-paginated with a per-page batched publish filter.\n * See the module header for the security + fan-out guarantees.\n */\nexport async function* enumeratePaths(\n opts: EnumeratePathsOptions,\n deps: EnumeratePathsDeps,\n tuning: EnumeratePathsTuning = {},\n): AsyncGenerator<PathEntry> {\n const pageSize = tuning.pageSize ?? ENUMERATE_PAGE_SIZE\n const concurrency = tuning.publishConcurrency ?? PUBLISH_CHECK_CONCURRENCY\n const { locale, entityType } = opts\n\n // Seed from `opts.afterId` so a caller can resume mid-stream; undefined starts\n // at the beginning (every existing caller's behavior, unchanged).\n let cursorId: string | undefined = opts.afterId\n for (;;) {\n // Keyset cursor on the immutable `id` PK (the rebuild job's precedent):\n // visits every canonical row exactly once even under concurrent mutation,\n // which OFFSET paging cannot guarantee.\n const where: PathEnumWhere = { locale, isCanonical: true }\n if (entityType !== undefined) where.entityType = entityType\n if (cursorId !== undefined) where.id = { gt: cursorId }\n\n const page = await deps.pathsClient.findMany({\n where,\n orderBy: [{ column: 'id', dir: 'asc' }],\n limit: pageSize,\n })\n if (page.length === 0) break\n\n // Group this page's rows by entity type (bounded: the routable-type set is\n // small + config-fixed), then ONE publish check per group.\n const byType = new Map<string, PathEnumRow[]>()\n for (const row of page) {\n const bucket = byType.get(row.entityType)\n if (bucket) bucket.push(row)\n else byType.set(row.entityType, [row])\n }\n\n const publishedByType = new Map<string, ReadonlySet<string>>()\n await mapBounded([...byType.entries()], concurrency, async ([type, rows]) => {\n const published = await deps.filterPublished(\n type,\n rows.map((r) => r.entityId),\n locale,\n )\n publishedByType.set(type, published)\n })\n\n // Yield only the page's PUBLISHED rows, preserving the id-ascending order.\n for (const row of page) {\n if (publishedByType.get(row.entityType)?.has(row.entityId)) {\n yield { path: row.path, entityType: row.entityType, id: row.entityId, updatedAt: row.updatedAt }\n }\n }\n\n // Advance the cursor to the last (max-id) row of this raw page. `page` is\n // non-empty here (length-0 broke above). Publish-filtering is a POST-filter,\n // so the cursor always steps over every index row regardless of publish state.\n const lastRow = page[page.length - 1]\n if (!lastRow) break\n cursorId = lastRow.id\n if (page.length < pageSize) break\n }\n}\n\n// ---------------------------------------------------------------------------\n// getCanonicalPaths — batched forward lookup (search url re-point), NO publish filter\n// ---------------------------------------------------------------------------\n\n/** Deps for {@link getCanonicalPaths}. */\nexport interface CanonicalPathsBatchDeps {\n pathsClient: PathEnumReader\n}\n\n/**\n * Max ids per batched index query — well under `defineTable`'s `MAX_LIMIT`\n * (1000, which throws rather than clamps). Search result pages are far smaller\n * than this, so this is one query in practice; larger id sets chunk safely.\n */\nconst CANONICAL_BATCH_SIZE = 500\n\n/**\n * The current canonical paths for many `(entityType, entityId)` in ONE locale,\n * as an `entityId → path` map — the batched sibling of `getCanonicalPath`.\n *\n * **No publish filter — deliberate**, matching `getCanonicalPath` (and the\n * search url builder it re-points): a search result's url is display-only and\n * the caller already gates row visibility. One index query per {@link\n * CANONICAL_BATCH_SIZE}-sized chunk (one query total for a normal search page),\n * so it never fans out per row.\n */\nexport async function getCanonicalPaths(\n entityType: string,\n entityIds: readonly string[],\n locale: string,\n deps: CanonicalPathsBatchDeps,\n): Promise<Map<string, string>> {\n const result = new Map<string, string>()\n for (let i = 0; i < entityIds.length; i += CANONICAL_BATCH_SIZE) {\n const chunk = entityIds.slice(i, i + CANONICAL_BATCH_SIZE)\n const rows = await deps.pathsClient.findMany({\n where: { entityType, locale, isCanonical: true, entityId: { in: [...chunk] } },\n limit: chunk.length,\n })\n for (const row of rows) result.set(row.entityId, row.path)\n }\n return result\n}\n"],"mappings":"sJA+JA,eAAuB,EACrB,EACA,EACA,EAA+B,CAAC,EACL,CAC3B,IAAM,EAAW,EAAO,UAAY,IAC9B,EAAc,EAAO,oBAAsB,EAC3C,CAAE,SAAQ,cAAe,EAI3B,EAA+B,EAAK,QACxC,OAAS,CAIP,IAAM,EAAuB,CAAE,SAAQ,YAAa,EAAK,EACrD,IAAe,IAAA,KAAW,EAAM,WAAa,GAC7C,IAAa,IAAA,KAAW,EAAM,GAAK,CAAE,GAAI,CAAS,GAEtD,IAAM,EAAO,MAAM,EAAK,YAAY,SAAS,CAC3C,QACA,QAAS,CAAC,CAAE,OAAQ,KAAM,IAAK,KAAM,CAAC,EACtC,MAAO,CACT,CAAC,EACD,GAAI,EAAK,SAAW,EAAG,MAIvB,IAAM,EAAS,IAAI,IACnB,IAAK,IAAM,KAAO,EAAM,CACtB,IAAM,EAAS,EAAO,IAAI,EAAI,UAAU,EACpC,EAAQ,EAAO,KAAK,CAAG,EACtB,EAAO,IAAI,EAAI,WAAY,CAAC,CAAG,CAAC,CACvC,CAEA,IAAM,EAAkB,IAAI,IAC5B,MAAM,EAAW,CAAC,GAAG,EAAO,QAAQ,CAAC,EAAG,EAAa,MAAO,CAAC,EAAM,KAAU,CAC3E,IAAM,EAAY,MAAM,EAAK,gBAC3B,EACA,EAAK,IAAK,GAAM,EAAE,QAAQ,EAC1B,CACF,EACA,EAAgB,IAAI,EAAM,CAAS,CACrC,CAAC,EAGD,IAAK,IAAM,KAAO,EACZ,EAAgB,IAAI,EAAI,UAAU,CAAC,EAAE,IAAI,EAAI,QAAQ,IACvD,KAAM,CAAE,KAAM,EAAI,KAAM,WAAY,EAAI,WAAY,GAAI,EAAI,SAAU,UAAW,EAAI,SAAU,GAOnG,IAAM,EAAU,EAAK,EAAK,OAAS,GAGnC,GAFI,CAAC,IACL,EAAW,EAAQ,GACf,EAAK,OAAS,GAAU,KAC9B,CACF,CA4BA,eAAsB,EACpB,EACA,EACA,EACA,EAC8B,CAC9B,IAAM,EAAS,IAAI,IACnB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAU,OAAQ,GAAK,IAAsB,CAC/D,IAAM,EAAQ,EAAU,MAAM,EAAG,EAAI,GAAoB,EACnD,EAAO,MAAM,EAAK,YAAY,SAAS,CAC3C,MAAO,CAAE,aAAY,SAAQ,YAAa,GAAM,SAAU,CAAE,GAAI,CAAC,GAAG,CAAK,CAAE,CAAE,EAC7E,MAAO,EAAM,MACf,CAAC,EACD,IAAK,IAAM,KAAO,EAAM,EAAO,IAAI,EAAI,SAAU,EAAI,IAAI,CAC3D,CACA,OAAO,CACT"}
@@ -209,6 +209,59 @@ declare function enabledLocaleCodes(entries: readonly LocaleEntry[]): string[];
209
209
  * effective set (avoids a second registry read within one request).
210
210
  */
211
211
  declare function resolveI18nConfig(app: ToolkitApp, entries?: readonly LocaleEntry[]): Promise<I18nConfig>;
212
+ /** What {@link resolveSiteI18n} returns. */
213
+ interface SiteI18n {
214
+ /** The site resolved, or `undefined` when none could be named (see below). */
215
+ site: string | undefined;
216
+ /** The effective registry narrowed to the site (disabled locales kept). */
217
+ entries: LocaleEntry[];
218
+ /**
219
+ * The site's PRESENTATION defaults: the locale a switcher, a sitemap or an
220
+ * unlocalised request falls back to (an ENABLED locale of the site), and its
221
+ * routing strategy.
222
+ *
223
+ * ⚠️ Not the STORAGE default. Which locale a row's base columns hold is an
224
+ * install-wide fact (`content({ defaultLocale })`, `i18n.defaultLocale`) —
225
+ * shared rows (media) are written from every site, so it cannot vary by
226
+ * site. Read {@link install} for it.
227
+ */
228
+ i18n: I18nConfig;
229
+ /** The install-wide i18n singletons — the storage default. */
230
+ install: I18nConfig;
231
+ }
232
+ /**
233
+ * The effective registry narrowed to ONE SITE's locales (plan `multisite`
234
+ * phase 03) — the whitelist alone, without the i18n settings read, for the
235
+ * per-request callers (the admin API handler) that need only the set.
236
+ */
237
+ declare function resolveSiteLocales(app: ToolkitApp, options?: {
238
+ site?: string;
239
+ entries?: readonly LocaleEntry[];
240
+ }): Promise<{
241
+ site: string | undefined;
242
+ entries: LocaleEntry[];
243
+ }>;
244
+ /**
245
+ * The locale set and presentation defaults of ONE SITE (plan `multisite` D007,
246
+ * phase 03): the effective registry narrowed to the site record's `locales`
247
+ * (empty = every locale), with the site's own default and routing strategy.
248
+ *
249
+ * - `entries` keeps disabled locales (like {@link resolveEffectiveLocales});
250
+ * the whitelist is `enabledLocaleCodes(entries)`.
251
+ * - `i18n.defaultLocale` is an ENABLED locale of the site. On an app serving
252
+ * several sites the record's `defaultLocale` / `routingStrategy` win; on a
253
+ * single-site install the install's i18n settings stay authoritative (the
254
+ * record only holds a one-time copy of them, and the Languages editor writes
255
+ * the settings), so nothing about a one-site install changes.
256
+ * - The site is `options.site`, else the request's, else the single site of a
257
+ * one-site install. With none of those (a multi-site request that names no
258
+ * site) the install-wide set is returned — what a SHARED entity (media) edits
259
+ * against; a bound entity refuses on its own, without a site.
260
+ */
261
+ declare function resolveSiteI18n(app: ToolkitApp, options?: {
262
+ site?: string;
263
+ entries?: readonly LocaleEntry[];
264
+ }): Promise<SiteI18n>;
212
265
  //#endregion
213
266
  //#region src/message-registry.d.ts
214
267
  /**
@@ -218,5 +271,5 @@ declare function resolveI18nConfig(app: ToolkitApp, entries?: readonly LocaleEnt
218
271
  */
219
272
  declare function resolveMessageOverrides(app: ToolkitApp, locale: string): Promise<MessageEntry[]>;
220
273
  //#endregion
221
- export { OverrideDirective as C, withOverrides as D, hasWithOverrides as E, workflowable as S, WithOverridesFields as T, WORKFLOW_TRANSITIONS as _, DraftNotFoundError as a, WorkflowableFields as b, VersionNotFoundError as c, DiffSegment as d, FieldDiff as f, WORKFLOW_STATES as g, entitySupports as h, resolveI18nConfig as i, BlockDiffEntry as l, EntityCapability as m, enabledLocaleCodes as n, EntityCapabilityError as o, PropDiff as p, resolveEffectiveLocales as r, InvalidLocaleError as s, resolveMessageOverrides as t, BlockDiffStatus as u, WorkflowState as v, OverridesMap as w, isAllowedTransition as x, WorkflowTransitionError as y };
222
- //# sourceMappingURL=index-C2G4IJck.d.mts.map
274
+ export { withOverrides as A, WorkflowableFields as C, OverridesMap as D, OverrideDirective as E, WithOverridesFields as O, WorkflowTransitionError as S, workflowable as T, EntityCapability as _, resolveI18nConfig as a, WORKFLOW_TRANSITIONS as b, DraftNotFoundError as c, VersionNotFoundError as d, BlockDiffEntry as f, PropDiff as g, FieldDiff as h, resolveEffectiveLocales as i, hasWithOverrides as k, EntityCapabilityError as l, DiffSegment as m, SiteI18n as n, resolveSiteI18n as o, BlockDiffStatus as p, enabledLocaleCodes as r, resolveSiteLocales as s, resolveMessageOverrides as t, InvalidLocaleError as u, entitySupports as v, isAllowedTransition as w, WorkflowState as x, WORKFLOW_STATES as y };
275
+ //# sourceMappingURL=index-C3VyZLjf.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index-C2G4IJck.d.mts","names":[],"sources":["../src/behaviors/with-overrides.ts","../src/behaviors/workflowable.ts","../src/capabilities.ts","../src/diff/types.ts","../src/errors.ts","../src/locale-registry.ts","../src/message-registry.ts"],"mappings":";;;;;;;;;;;KAqDK,iBAAA;EACC,IAAA;AAAA;EAEA,IAAA;EACA,KAAA,GAAQ,MAAA;EACR,MAAA,GAAS,MAAA,SAAe,MAAA;AAAA;EAExB,IAAA;EAAiB,KAAA,EAAO,MAAA;AAAA;AAAA,KAEzB,YAAA,GAAe,MAAM,SAAS,iBAAA;AAAA,KAEvB,mBAAA;EAcV,UAAA,EAAY,SAAS;AAAA;;;AC/B4B;AASnD;iBD4LgB,aAAA,CAAA,GAAiB,QAAQ,CAAC,mBAAA;;;;;;iBAmD1B,gBAAA,CACd,SAAA,EAAW,aAAa;EAAG,IAAA;AAAA,IAAiB,MAAA;;;cC1PjC,eAAA;AAAA,KACD,aAAA,WAAwB,eAAe;ADqMnD;;;;AAA6D;AAmD7D;;AAnDA,cC5La,oBAAA;EAAA;;;;;;ADgPqE;;;AAAA,iBCrOlE,mBAAA,CAAoB,IAAA,EAAM,aAAA,EAAe,EAAA,EAAI,aAAa;AArB1E;;;;AAAA,cA+Ba,uBAAA,SAAgC,KAAA;EAAA,SAClC,IAAA,EAAM,aAAA;EAAA,SACN,EAAA,EAAI,aAAA;cAED,IAAA,EAAM,aAAA,EAAe,EAAA,EAAI,aAAA;AAAA;AAAA,KAW3B,kBAAA;EACV,eAAA,EAAiB,WAAA;IAAgB,QAAA;EAAA;EACjC,iBAAA,EAAmB,SAAA;EACnB,aAAA,EAAe,SAAA;EACf,kBAAA,EAAoB,SAAA;AAAA;AAAA,iBAGN,YAAA,CAAA,GAAgB,QAAQ,CAAC,kBAAA;;;KC7E7B,gBAAA;;;;;;iBAOI,cAAA,CAAe,MAAA,EAAQ,MAAA,EAAQ,UAAA,EAAY,gBAAgB;;;;;;;UCxB1D,WAAA;EACf,IAAA;EACA,KAAK;AAAA;AHsCuD;AAAA,UGlC7C,SAAA;EACf,KAAA;EACA,IAAA;EAWA,IAAA;EACA,EAAA;EHmC4B;EGjC5B,QAAA,GAAW,WAAA;EHiCuB;EG/BlC,UAAA,GAAa,cAAc;AAAA;;KAIjB,eAAA;;UAGK,cAAA;EACf,OAAA;EACA,SAAA;EACA,MAAA,EAAQ,eAAA;EACR,SAAA;EACA,OAAA;EHmBkC;EGjBlC,SAAA,GAAY,QAAQ;AAAA;;UAIL,QAAA;EACf,KAAA;EACA,IAAA;EACA,EAAA;;EAEA,QAAA,GAAW,WAAW;AAAA;;;;;;;;;;;AHPsC;;;;cIhCjD,qBAAA,SAA8B,KAAK;EAAA,SACrC,UAAA;EAAA,SACA,UAAA;cAGP,UAAA,UACA,UAAA,EAAY,qBAAA,gBACZ,OAAA;AAAA;;;;;;;;;;cAsBS,kBAAA,SAA2B,KAAK;EAAA,SAClC,QAAA;EAAA,SACA,MAAA;cAEG,QAAA,UAAkB,MAAA;AAAA;;;AJgBoB;AAEpD;;;;AAcuB;AAsKvB;;;;cI5Ka,oBAAA,SAA6B,KAAK;EAAA,SACpC,QAAA;EAAA,SACA,OAAA;EAAA,SACA,MAAA;cAEG,QAAA,UAAkB,OAAA,UAAiB,MAAA;AAAA;;;;;AJ2NiC;;;;AC1PlF;cGiDa,kBAAA,SAA2B,KAAK;EAAA,SAClC,MAAA;EAAA,SACA,OAAA;cAEG,MAAA,UAAgB,OAAA;AAAA;;;;;;;;iBCZR,uBAAA,CAAwB,GAAA,EAAK,UAAA,GAAa,OAAA,CAAQ,WAAA;;iBAoBxD,kBAAA,CAAmB,OAA+B,WAAb,WAAW;;;AL/C5B;AAAA;;;;iBK0Dd,iBAAA,CACpB,GAAA,EAAK,UAAA,EACL,OAAA,YAAmB,WAAA,KAClB,OAAA,CAAQ,UAAA;;;;;;;;iBC1EW,uBAAA,CACpB,GAAA,EAAK,UAAA,EACL,MAAA,WACC,OAAA,CAAQ,YAAA"}
1
+ {"version":3,"file":"index-C3VyZLjf.d.mts","names":[],"sources":["../src/behaviors/with-overrides.ts","../src/behaviors/workflowable.ts","../src/capabilities.ts","../src/diff/types.ts","../src/errors.ts","../src/locale-registry.ts","../src/message-registry.ts"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as LocaleDirection, c as LockInfo, d as TranslationReport, f as TranslationStatus, i as LocaleConfig, l as RoutingStrategy, m as VersionableOptions, n as DraftEntry, o as LocaleEntry, p as VersionEntry, r as I18nConfig, t as ContentConfig, u as TranslationFieldStatus } from "./types-B3K2p9t8.mjs";
2
2
  import { n as getVersionableOptions, r as versionable } from "./versionable-ByZgfpp0.mjs";
3
- import { C as OverrideDirective, D as withOverrides, E as hasWithOverrides, S as workflowable, T as WithOverridesFields, _ as WORKFLOW_TRANSITIONS, a as DraftNotFoundError, b as WorkflowableFields, c as VersionNotFoundError, d as DiffSegment, f as FieldDiff, g as WORKFLOW_STATES, h as entitySupports, i as resolveI18nConfig, l as BlockDiffEntry, m as EntityCapability, n as enabledLocaleCodes, o as EntityCapabilityError, p as PropDiff, r as resolveEffectiveLocales, s as InvalidLocaleError, t as resolveMessageOverrides, u as BlockDiffStatus, v as WorkflowState, w as OverridesMap, x as isAllowedTransition, y as WorkflowTransitionError } from "./index-C2G4IJck.mjs";
4
- import { a as ROUTING_STRATEGIES, i as Locale, n as prefixDefaultLocaleForStrategy, o as i18nSettings, r as Message, t as localePrefixForStrategy } from "./routing-strategy-D3Qowm3D.mjs";
5
- export { type BlockDiffEntry, type BlockDiffStatus, type ContentConfig, type DiffSegment, type DraftEntry, DraftNotFoundError, type EntityCapability, EntityCapabilityError, type FieldDiff, type I18nConfig, InvalidLocaleError, Locale, type LocaleConfig, type LocaleDirection, type LocaleEntry, type LockInfo, Message, type OverrideDirective, type OverridesMap, type PropDiff, ROUTING_STRATEGIES, type RoutingStrategy, type TranslationFieldStatus, type TranslationReport, type TranslationStatus, type VersionEntry, VersionNotFoundError, type VersionableOptions, WORKFLOW_STATES, WORKFLOW_TRANSITIONS, type WithOverridesFields, type WorkflowState, WorkflowTransitionError, type WorkflowableFields, enabledLocaleCodes, entitySupports, getVersionableOptions, hasWithOverrides, i18nSettings, isAllowedTransition, localePrefixForStrategy, prefixDefaultLocaleForStrategy, resolveEffectiveLocales, resolveI18nConfig, resolveMessageOverrides, versionable, withOverrides, workflowable };
3
+ import { A as withOverrides, C as WorkflowableFields, D as OverridesMap, E as OverrideDirective, O as WithOverridesFields, S as WorkflowTransitionError, T as workflowable, _ as EntityCapability, a as resolveI18nConfig, b as WORKFLOW_TRANSITIONS, c as DraftNotFoundError, d as VersionNotFoundError, f as BlockDiffEntry, g as PropDiff, h as FieldDiff, i as resolveEffectiveLocales, k as hasWithOverrides, l as EntityCapabilityError, m as DiffSegment, n as SiteI18n, o as resolveSiteI18n, p as BlockDiffStatus, r as enabledLocaleCodes, s as resolveSiteLocales, t as resolveMessageOverrides, u as InvalidLocaleError, v as entitySupports, w as isAllowedTransition, x as WorkflowState, y as WORKFLOW_STATES } from "./index-C3VyZLjf.mjs";
4
+ import { a as ROUTING_STRATEGIES, i as Locale, n as prefixDefaultLocaleForStrategy, o as i18nSettings, r as Message, t as localePrefixForStrategy } from "./routing-strategy-DV7KGarX.mjs";
5
+ export { type BlockDiffEntry, type BlockDiffStatus, type ContentConfig, type DiffSegment, type DraftEntry, DraftNotFoundError, type EntityCapability, EntityCapabilityError, type FieldDiff, type I18nConfig, InvalidLocaleError, Locale, type LocaleConfig, type LocaleDirection, type LocaleEntry, type LockInfo, Message, type OverrideDirective, type OverridesMap, type PropDiff, ROUTING_STRATEGIES, type RoutingStrategy, type SiteI18n, type TranslationFieldStatus, type TranslationReport, type TranslationStatus, type VersionEntry, VersionNotFoundError, type VersionableOptions, WORKFLOW_STATES, WORKFLOW_TRANSITIONS, type WithOverridesFields, type WorkflowState, WorkflowTransitionError, type WorkflowableFields, enabledLocaleCodes, entitySupports, getVersionableOptions, hasWithOverrides, i18nSettings, isAllowedTransition, localePrefixForStrategy, prefixDefaultLocaleForStrategy, resolveEffectiveLocales, resolveI18nConfig, resolveMessageOverrides, resolveSiteI18n, resolveSiteLocales, versionable, withOverrides, workflowable };