@murumets-ee/content 0.23.2 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks/index.d.mts +1 -1
- package/dist/blocks/index.mjs +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.mjs +1 -1
- package/dist/{define-block-D-7OpIn5.mjs → define-block-DpMPEEdA.mjs} +1 -1
- package/dist/{define-block-D-7OpIn5.mjs.map → define-block-DpMPEEdA.mjs.map} +1 -1
- package/dist/entities/index.d.mts +123 -0
- package/dist/entities/index.d.mts.map +1 -0
- package/dist/entities/index.mjs +2 -0
- package/dist/entities/index.mjs.map +1 -0
- package/dist/{errors-kkf8Ue1H.mjs → errors-CVBrIU1K.mjs} +1 -1
- package/dist/{errors-kkf8Ue1H.mjs.map → errors-CVBrIU1K.mjs.map} +1 -1
- package/dist/{index-B4kqLg-c.d.mts → index-CB0fdMs1.d.mts} +38 -4
- package/dist/index-CB0fdMs1.d.mts.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/locale-utils-DIcmuLIr.mjs +2 -0
- package/dist/{locale-utils-BMRVJwvU.mjs.map → locale-utils-DIcmuLIr.mjs.map} +1 -1
- package/dist/lock-service.d.mts +1 -1
- package/dist/lock-service.mjs +1 -1
- package/dist/{locks-BRtmYqgS.mjs → locks-BCHUb-yC.mjs} +1 -1
- package/dist/{locks-BRtmYqgS.mjs.map → locks-BCHUb-yC.mjs.map} +1 -1
- package/dist/{plugin-DkmXJ7Qw.mjs → plugin-DbK6FxTd.mjs} +2 -2
- package/dist/{plugin-DkmXJ7Qw.mjs.map → plugin-DbK6FxTd.mjs.map} +1 -1
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/query-client.mjs +1 -1
- package/dist/{types-BCmX1nBL.d.mts → types-CWo2dfEK.d.mts} +1 -1
- package/dist/{types-BCmX1nBL.d.mts.map → types-CWo2dfEK.d.mts.map} +1 -1
- package/dist/workflowable-aGtQhPRc.mjs +2 -0
- package/dist/workflowable-aGtQhPRc.mjs.map +1 -0
- package/package.json +9 -5
- package/dist/index-B4kqLg-c.d.mts.map +0 -1
- package/dist/locale-utils-BMRVJwvU.mjs +0 -2
package/dist/blocks/index.d.mts
CHANGED
package/dist/blocks/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"../define-block-
|
|
1
|
+
import{t as e}from"../define-block-DpMPEEdA.mjs";import{field as t}from"@murumets-ee/entity";const n=e({slug:`code`,label:`Code`,fields:{code:t.text({required:!0}),language:t.select({options:[`javascript`,`typescript`,`python`,`html`,`css`,`json`,`bash`,`sql`,`other`]})}}),r=e({slug:`cta`,label:`Call to Action`,fields:{heading:t.text({required:!0,translatable:!0}),description:t.text({translatable:!0}),buttonText:t.text({required:!0,translatable:!0}),buttonLink:t.text({required:!0})}}),i=e({slug:`gallery`,label:`Gallery`,fields:{images:t.reference({entity:`media`,cardinality:`many`}),columns:t.select({options:[`2`,`3`,`4`]})}}),a=e({slug:`hero`,label:`Hero Section`,fields:{heading:t.text({required:!0,translatable:!0}),subheading:t.text({translatable:!0}),background:t.media({accept:[`image/*`]}),alignment:t.select({options:[`left`,`center`,`right`]})}}),o=e({slug:`image`,label:`Image`,fields:{image:t.media({accept:[`image/*`],required:!0}),alt:t.text({translatable:!0}),caption:t.text({translatable:!0})}}),s=e({slug:`quote`,label:`Quote`,fields:{quote:t.text({required:!0,translatable:!0}),attribution:t.text({translatable:!0})}}),c=e({slug:`richtext`,label:`Rich Text`,fields:{content:t.richtext({translatable:!0})}}),l=e({slug:`video`,label:`Video`,fields:{url:t.text({required:!0}),poster:t.media({accept:[`image/*`]}),caption:t.text({translatable:!0})}}),u=[c,o,a,i,r,l,s,n];export{r as CTABlock,n as CodeBlock,i as GalleryBlock,a as HeroBlock,o as ImageBlock,s as QuoteBlock,c as RichTextBlock,l as VideoBlock,u as commonBlocks};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/client.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { d as TranslationStatus, f as VersionEntry, r as DraftEntry, s as LocalePublishStatus, u as TranslationReport } from "./types-
|
|
2
|
-
import { o as FieldDiff } from "./index-
|
|
1
|
+
import { d as TranslationStatus, f as VersionEntry, r as DraftEntry, s as LocalePublishStatus, u as TranslationReport } from "./types-CWo2dfEK.mjs";
|
|
2
|
+
import { o as FieldDiff } from "./index-CB0fdMs1.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 as InferEntityDTO$1, InferUpdateInput } from "@murumets-ee/entity/admin";
|
package/dist/client.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./versionable-B0bP2Gib.mjs";import{t}from"./errors-kkf8Ue1H.mjs";import{n,r}from"./plugin-DkmXJ7Qw.mjs";import{t as i}from"./locale-utils-BMRVJwvU.mjs";import"server-only";import{getApp as a}from"@murumets-ee/core";import{createAdminClient as o}from"@murumets-ee/core/clients";import{schemaRegistry as s}from"@murumets-ee/db";import{and as c,eq as l,inArray as u}from"drizzle-orm";function d(e){return e.match(/\S+|\s+/g)??[]}function f(e,t){if(e===t)return e?[{type:`equal`,value:e}]:[];if(!e)return[{type:`add`,value:t}];if(!t)return[{type:`remove`,value:e}];let n=d(e),r=d(t),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=1;e<=i;e++){let t=o[e],i=o[e-1];if(!(!t||!i))for(let o=1;o<=a;o++)n[e-1]===r[o-1]?t[o]=(i[o-1]??0)+1:t[o]=Math.max(i[o]??0,t[o-1]??0)}let s=[],c=i,l=a,u=[];for(;c>0||l>0;){let e=c>0?n[c-1]:void 0,t=l>0?r[l-1]:void 0;if(c>0&&l>0&&e!==void 0&&e===t)u.push({type:`equal`,value:e}),c--,l--;else if(l>0&&t!==void 0&&(c===0||(o[c]?.[l-1]??0)>=(o[c-1]?.[l]??0)))u.push({type:`add`,value:t}),l--;else if(e!==void 0)u.push({type:`remove`,value:e}),c--;else break}u.reverse();for(let e of u){let t=s[s.length-1];t&&t.type===e.type?t.value+=e.value:s.push({...e})}return s}function p(e){return e.replace(/<[^>]*>/g,` `).replace(/\s+/g,` `).trim()}function m(e){return e==null?``:e instanceof Date?e.toISOString():typeof e==`object`?JSON.stringify(e):String(e)}function ee(e,t,n,r){if(JSON.stringify(t)===JSON.stringify(n))return null;switch(r?.type??g(t,n)){case`text`:case`slug`:return{field:e,type:`text`,from:t,to:n,segments:f(m(t),m(n))};case`richtext`:return{field:e,type:`richtext`,from:t,to:n,segments:f(p(m(t)),p(m(n)))};case`number`:return{field:e,type:`number`,from:t,to:n};case`boolean`:return{field:e,type:`boolean`,from:t,to:n};case`date`:return{field:e,type:`date`,from:t,to:n};case`select`:return{field:e,type:`select`,from:t,to:n};case`media`:return{field:e,type:`media`,from:t,to:n};case`json`:return{field:e,type:`json`,from:t,to:n};case`blocks`:return{field:e,type:`blocks`,from:t,to:n};default:return{field:e,type:`unknown`,from:t,to:n}}}function h(e,t,n){let r=new Set([...Object.keys(e),...Object.keys(t)]),i=[];for(let a of r){if(a===`id`||a===`createdAt`||a===`updatedAt`)continue;let r=ee(a,e[a],t[a],n?.[a]);r&&i.push(r)}return i}function g(e,t){let n=e??t;return typeof n==`number`?`number`:typeof n==`boolean`?`boolean`:n instanceof Date?`date`:typeof n==`string`?/^\d{4}-\d{2}-\d{2}/.test(n)?`date`:`text`:Array.isArray(n)?n.length>0&&typeof n[0]==`object`&&n[0]!==null&&`_block`in n[0]?`blocks`:`json`:typeof n==`object`?`json`:`unknown`}function _(e,t){if(typeof e==`string`||typeof t==`string`)return f(m(e),m(t))}function v(e){return typeof e==`object`&&!!e&&`_block`in e&&`_id`in e&&typeof e._block==`string`&&typeof e._id==`string`}function y(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let i of n){if(i===`_block`||i===`_id`)continue;let n=e[i],a=t[i];JSON.stringify(n)!==JSON.stringify(a)&&r.push({field:i,from:n,to:a,segments:_(n,a)})}return r}function b(e,t){let n=e.filter(v),r=t.filter(v),i=new Map,a=new Map;for(let e=0;e<n.length;e++){let t=n[e];t&&i.set(t._id,{block:t,index:e})}for(let e=0;e<r.length;e++){let t=r[e];t&&a.set(t._id,{block:t,index:e})}let o=[],s=new Set;for(let[e,{block:t,index:n}]of a){s.add(e);let r=i.get(e);if(!r){o.push({blockId:e,blockType:t._block,status:`added`,toIndex:n});continue}let a=r.block,c=r.index,l=y(a,t),u=c!==n;l.length===0&&!u?o.push({blockId:e,blockType:t._block,status:`unchanged`,fromIndex:c,toIndex:n}):l.length>0?o.push({blockId:e,blockType:t._block,status:`changed`,fromIndex:c,toIndex:n,propDiffs:l}):o.push({blockId:e,blockType:t._block,status:`moved`,fromIndex:c,toIndex:n})}for(let[e,{block:t,index:n}]of i)s.has(e)||o.push({blockId:e,blockType:t._block,status:`removed`,fromIndex:n});return o}function x(e,t){return{entity:e.entity,admin:e.admin,db:e.admin.getDb(),versionableOptions:t,logger:e.logger}}function S(e){let{drafts:t}=r(e.entity.name);return t?t.makeClient(e.db):null}function C(e){let{versions:t}=r(e.entity.name);return t?t.makeClient(e.db):null}function w(e){let{localeStatus:t}=r(e.entity.name);return t?t.makeClient(e.db):null}const T=new Set([`id`,`createdAt`,`createdBy`,`updatedAt`,`updatedBy`]);function E(e,t){let n=e.allFields,r={};for(let e of Object.keys(t)){if(T.has(e)||e.startsWith(`_`)||!Object.hasOwn(n,e))continue;let i=n[e];i&&i.internal===!0||(r[e]=t[e])}return r}function D(e,t){return JSON.stringify(e)===JSON.stringify(t)}function O(e,t){let n={};for(let r of Object.keys(t))r===`id`||r.startsWith(`_`)||D(e[r],t[r])||(n[r]=t[r]);return n}async function k(e,t,n){n?.locale!==void 0&&i(n.locale);let r=C(e);return r?await r.findMany({where:{entityId:t,locale:n?.locale??`_`},orderBy:[{column:`version`,dir:`desc`}],...n?.limit===void 0?{}:{limit:n.limit},...n?.offset===void 0?{}:{offset:n.offset}}):[]}async function A(e,t,n,r=`_`){i(r);let a=C(e);return a?await a.findOne({entityId:t,version:n,locale:r})??null:null}async function j(e,t,n,r,a=`_`){i(a);let[o,s]=await Promise.all([A(e,t,n,a),A(e,t,r,a)]);if(!o||!s)return null;let c=e.entity.allFields,l=h(o.data,s.data,c);for(let e of l)e.type===`blocks`&&Array.isArray(e.from)&&Array.isArray(e.to)&&(e.blockDiffs=b(e.from,e.to));return{diffs:l,fromVersion:o,toVersion:s}}async function M(e,n,r){if(!C(e))throw new t(e.entity.name,`versions`);let i=await A(e,n,r);if(!i)throw Error(`Version ${r} not found for entity ${n}`);let a=i.data,o=E(e.entity,a);return e.admin.update(n,o)}async function N(e,t,n=`_`){if(i(n),!e.versionableOptions)return;let r=C(e);if(!r)return;let a=e.versionableOptions.maxPerDoc,o=await r.findMany({where:{entityId:t,locale:n},orderBy:[{column:`version`,dir:`desc`}],limit:a});if(o.length<a)return;let s=o.map(e=>e.id);await r.deleteMany({entityId:t,locale:n,id:{notIn:s}})}async function P(e,t,n,r,a,o=`_`,s){i(o);let c=C(e);if(!c)return;let[l]=await c.aggregate({select:{maxVersion:{fn:`max`,column:`version`}},where:{entityId:t,locale:o}}),u=Number(l?.maxVersion??0)+1,d=n.status??null;if(a){let e=await c.findOne({entityId:t,isAutosave:!0,version:u,locale:o});if(e)return await c.update({id:e.id},{data:n,delta:r,status:d,createdAt:new Date}),u}return await c.insert({entityId:t,version:u,locale:o,data:n,delta:r,status:d,createdBy:s?.id??null,createdByName:s?.name??null,isAutosave:a}),u}async function F(e,t,n,r,a,o){if(a!==void 0&&i(a),!e.versionableOptions)return;let s=a??`_`,c=await P(e,t,n,r?O(r,n):null,!1,s,o);c!==void 0&&s===`_`&&await I(e,t,c),await N(e,t,s)}async function I(e,t,n){let r=s.get(e.entity.name);r&&await e.db.update(r).set({_version:n}).where(l(r.id,t))}function L(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 R(e,n,r,a=`_`,o){i(a);let s=S(e);if(!s)throw new t(e.entity.name,`drafts`);let c=new Date,l=o?.id??``,u=o?.name??null;return L(await s.upsert({entityId:n,locale:a,data:r,createdBy:l,createdByName:u,createdAt:c,updatedAt:c},{target:[`entityId`,`locale`],set:{data:r,updatedAt:c}}))}async function z(e,t,n=`_`){i(n);let r=S(e);if(!r)return null;let a=await r.findOne({entityId:t,locale:n});return a?L(a):null}async function B(e,t,n=`_`){i(n);let r=S(e);return r?(await r.deleteMany({entityId:t,locale:n})).length>0:!1}async function V(e,n,r=`_`,a){if(i(r),!S(e))throw new t(e.entity.name,`drafts`);let o=await z(e,n,r);if(!o)throw Error(`No draft found for entity ${n} locale ${r}`);let s=null;e.versionableOptions&&(s=await e.admin.findById(n));let c=o.data,l=E(e.entity,c),u=await e.admin.update(n,l);return await B(e,n,r),e.versionableOptions&&await F(e,n,u,s,r,a),u}async function H(e,t,n=`_`){i(n);let r=S(e);return r?r.exists({entityId:t,locale:n}):!1}async function U(e,t=`_`){i(t);let n=S(e);return n?n.count({locale:t}):0}async function W(e,t,n=`_`){i(n);let r=new Set;if(t.length===0)return r;let a=S(e);if(!a)return r;let o=await a.findMany({where:{entityId:{in:t},locale:n}});for(let e of o)r.add(e.entityId);return r}async function G(e,t,r=`_`){i(r);let a=await z(e,t,r);if(!a)return null;let o=n().defaultLocale,s=r===`_`?{defaultLocale:o}:{locale:r,defaultLocale:o},c=await e.admin.findById(t,s);if(!c)return null;let l=e.entity.allFields,u=c,d=a.data,f={};for(let e of Object.keys(d))f[e]=u[e];let p=h(f,d,l);for(let e of p)e.type===`blocks`&&Array.isArray(e.from)&&Array.isArray(e.to)&&(e.blockDiffs=b(e.from,e.to));return{diffs:p,current:c,draft:a}}async function K(e,t,n){i(n.from);let r=Array.isArray(n.to)?n.to:[n.to];for(let e of r)i(e);let a=s.get(`${e.entity.name}_layout`);if(!a)return;let o=await e.db.select().from(a).where(c(l(a.entityId,t),l(a.locale,n.from))).orderBy(a.sortOrder);if(o.length!==0){for(let n of r){await e.db.delete(a).where(c(l(a.entityId,t),l(a.locale,n)));for(let t of o)await e.db.insert(a).values({entityId:t.entityId,fieldName:t.fieldName,blockType:t.blockType,sortOrder:t.sortOrder,data:t.data,locale:n})}e.logger?.debug({entityId:t,from:n.from,to:r},`Layout copied between locales`)}}async function q(e,t){let n=(await Y(e,[t])).get(t);return n?Object.values(n):[]}async function J(e,r,a,o){if(i(a),a===n().defaultLocale){let t={status:o};o===`published`&&(t.publishedAt=new Date),await e.admin.update(r,t);return}let s=w(e);if(!s)throw new t(e.entity.name,`localeStatus`);let c=o===`published`?new Date:null;await s.upsert({entityId:r,locale:a,status:o,publishedAt:c},{target:[`entityId`,`locale`],set:{status:o,publishedAt:c}})}async function Y(e,t){let n=new Map;if(t.length===0)return n;let r=w(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 X(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 Z(e,t){return(await Q(e,[t])).get(t)??{}}async function Q(e,t){let r=new Map;if(t.length===0)return r;let i=n(),a=Object.entries(e.entity.allFields).filter(([e,t])=>t.translatable).map(([e])=>e);if(a.length===0)return r;let o=s.get(`${e.entity.name}_translations`);if(!o)return r;let c=await e.db.select().from(o).where(u(o.entityId,t)),l=new Map;for(let e of c){let t=e,n=t.entityId,r=t.locale,i={};for(let e of a)i[e]=t[e];let o=l.get(n);o||(o=new Map,l.set(n,o)),o.set(r,i)}for(let e of t)r.set(e,X(a,l.get(e),i.locales,i.defaultLocale));return r}async function te(e,t){let r=n(),i=t?.locale??r.locales.find(e=>e.code!==r.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 Q(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 $=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 P(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=O(n,r),i=await P(this.ctx,e,r,t,!1);if(i!==void 0)return await I(this.ctx,e,i),await N(this.ctx,e),{...r,_version:i};await N(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)}copyLayout(e,t){return K(this.ctx,e,t)}getVersions(e,t){return k(this.ctx,e,t)}getVersion(e,t,n){return A(this.ctx,e,t,n)}compareVersions(e,t,n,r){return j(this.ctx,e,t,n,r)}restoreVersion(e,t){return M(this.ctx,e,t)}pruneVersions(e,t){return N(this.ctx,e,t)}createVersionSnapshot(e,t,n,r,i){return F(this.ctx,e,t,n,r,i)}getTranslationStatus(e){return Z(this.ctx,e)}getTranslationStatusBatch(e){return Q(this.ctx,e)}getTranslationReport(e){return te(this.ctx,e)}getLocaleStatus(e){return q(this.ctx,e)}setLocaleStatus(e,t,n){return J(this.ctx,e,t,n)}getLocaleStatusBatch(e){return Y(this.ctx,e)}saveDraft(e,t,n,r){return R(this.ctx,e,t,n,r)}getDraft(e,t){return z(this.ctx,e,t)}deleteDraft(e,t){return B(this.ctx,e,t)}publishDraft(e,t,n){return V(this.ctx,e,t,n)}hasDraft(e,t){return H(this.ctx,e,t)}countDrafts(e){return U(this.ctx,e)}hasDraftBatch(e,t){return W(this.ctx,e,t)}compareDraftWithCurrent(e,t){return G(this.ctx,e,t)}};function ne(e){return new $({admin:o(e),entity:e,logger:a().logger.child({content:e.name})})}export{$ as ContentClient,ne as createContentClient};
|
|
1
|
+
import{t as e}from"./versionable-B0bP2Gib.mjs";import{t}from"./errors-CVBrIU1K.mjs";import{n,r}from"./plugin-DbK6FxTd.mjs";import{t as i}from"./locale-utils-DIcmuLIr.mjs";import"server-only";import{getApp as a}from"@murumets-ee/core";import{createAdminClient as o}from"@murumets-ee/core/clients";import{schemaRegistry as s}from"@murumets-ee/db";import{and as c,eq as l,inArray as u}from"drizzle-orm";function d(e){return e.match(/\S+|\s+/g)??[]}function f(e,t){if(e===t)return e?[{type:`equal`,value:e}]:[];if(!e)return[{type:`add`,value:t}];if(!t)return[{type:`remove`,value:e}];let n=d(e),r=d(t),i=n.length,a=r.length,o=Array.from({length:i+1},()=>Array(a+1).fill(0));for(let e=1;e<=i;e++){let t=o[e],i=o[e-1];if(!(!t||!i))for(let o=1;o<=a;o++)n[e-1]===r[o-1]?t[o]=(i[o-1]??0)+1:t[o]=Math.max(i[o]??0,t[o-1]??0)}let s=[],c=i,l=a,u=[];for(;c>0||l>0;){let e=c>0?n[c-1]:void 0,t=l>0?r[l-1]:void 0;if(c>0&&l>0&&e!==void 0&&e===t)u.push({type:`equal`,value:e}),c--,l--;else if(l>0&&t!==void 0&&(c===0||(o[c]?.[l-1]??0)>=(o[c-1]?.[l]??0)))u.push({type:`add`,value:t}),l--;else if(e!==void 0)u.push({type:`remove`,value:e}),c--;else break}u.reverse();for(let e of u){let t=s[s.length-1];t&&t.type===e.type?t.value+=e.value:s.push({...e})}return s}function p(e){return e.replace(/<[^>]*>/g,` `).replace(/\s+/g,` `).trim()}function m(e){return e==null?``:e instanceof Date?e.toISOString():typeof e==`object`?JSON.stringify(e):String(e)}function ee(e,t,n,r){if(JSON.stringify(t)===JSON.stringify(n))return null;switch(r?.type??g(t,n)){case`text`:case`slug`:return{field:e,type:`text`,from:t,to:n,segments:f(m(t),m(n))};case`richtext`:return{field:e,type:`richtext`,from:t,to:n,segments:f(p(m(t)),p(m(n)))};case`number`:return{field:e,type:`number`,from:t,to:n};case`boolean`:return{field:e,type:`boolean`,from:t,to:n};case`date`:return{field:e,type:`date`,from:t,to:n};case`select`:return{field:e,type:`select`,from:t,to:n};case`media`:return{field:e,type:`media`,from:t,to:n};case`json`:return{field:e,type:`json`,from:t,to:n};case`blocks`:return{field:e,type:`blocks`,from:t,to:n};default:return{field:e,type:`unknown`,from:t,to:n}}}function h(e,t,n){let r=new Set([...Object.keys(e),...Object.keys(t)]),i=[];for(let a of r){if(a===`id`||a===`createdAt`||a===`updatedAt`)continue;let r=ee(a,e[a],t[a],n?.[a]);r&&i.push(r)}return i}function g(e,t){let n=e??t;return typeof n==`number`?`number`:typeof n==`boolean`?`boolean`:n instanceof Date?`date`:typeof n==`string`?/^\d{4}-\d{2}-\d{2}/.test(n)?`date`:`text`:Array.isArray(n)?n.length>0&&typeof n[0]==`object`&&n[0]!==null&&`_block`in n[0]?`blocks`:`json`:typeof n==`object`?`json`:`unknown`}function _(e,t){if(typeof e==`string`||typeof t==`string`)return f(m(e),m(t))}function v(e){return typeof e==`object`&&!!e&&`_block`in e&&`_id`in e&&typeof e._block==`string`&&typeof e._id==`string`}function y(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let i of n){if(i===`_block`||i===`_id`)continue;let n=e[i],a=t[i];JSON.stringify(n)!==JSON.stringify(a)&&r.push({field:i,from:n,to:a,segments:_(n,a)})}return r}function b(e,t){let n=e.filter(v),r=t.filter(v),i=new Map,a=new Map;for(let e=0;e<n.length;e++){let t=n[e];t&&i.set(t._id,{block:t,index:e})}for(let e=0;e<r.length;e++){let t=r[e];t&&a.set(t._id,{block:t,index:e})}let o=[],s=new Set;for(let[e,{block:t,index:n}]of a){s.add(e);let r=i.get(e);if(!r){o.push({blockId:e,blockType:t._block,status:`added`,toIndex:n});continue}let a=r.block,c=r.index,l=y(a,t),u=c!==n;l.length===0&&!u?o.push({blockId:e,blockType:t._block,status:`unchanged`,fromIndex:c,toIndex:n}):l.length>0?o.push({blockId:e,blockType:t._block,status:`changed`,fromIndex:c,toIndex:n,propDiffs:l}):o.push({blockId:e,blockType:t._block,status:`moved`,fromIndex:c,toIndex:n})}for(let[e,{block:t,index:n}]of i)s.has(e)||o.push({blockId:e,blockType:t._block,status:`removed`,fromIndex:n});return o}function x(e,t){return{entity:e.entity,admin:e.admin,db:e.admin.getDb(),versionableOptions:t,logger:e.logger}}function S(e){let{drafts:t}=r(e.entity.name);return t?t.makeClient(e.db):null}function C(e){let{versions:t}=r(e.entity.name);return t?t.makeClient(e.db):null}function w(e){let{localeStatus:t}=r(e.entity.name);return t?t.makeClient(e.db):null}const T=new Set([`id`,`createdAt`,`createdBy`,`updatedAt`,`updatedBy`]);function E(e,t){let n=e.allFields,r={};for(let e of Object.keys(t)){if(T.has(e)||e.startsWith(`_`)||!Object.hasOwn(n,e))continue;let i=n[e];i&&i.internal===!0||(r[e]=t[e])}return r}function D(e,t){return JSON.stringify(e)===JSON.stringify(t)}function O(e,t){let n={};for(let r of Object.keys(t))r===`id`||r.startsWith(`_`)||D(e[r],t[r])||(n[r]=t[r]);return n}async function k(e,t,n){n?.locale!==void 0&&i(n.locale);let r=C(e);return r?await r.findMany({where:{entityId:t,locale:n?.locale??`_`},orderBy:[{column:`version`,dir:`desc`}],...n?.limit===void 0?{}:{limit:n.limit},...n?.offset===void 0?{}:{offset:n.offset}}):[]}async function A(e,t,n,r=`_`){i(r);let a=C(e);return a?await a.findOne({entityId:t,version:n,locale:r})??null:null}async function j(e,t,n,r,a=`_`){i(a);let[o,s]=await Promise.all([A(e,t,n,a),A(e,t,r,a)]);if(!o||!s)return null;let c=e.entity.allFields,l=h(o.data,s.data,c);for(let e of l)e.type===`blocks`&&Array.isArray(e.from)&&Array.isArray(e.to)&&(e.blockDiffs=b(e.from,e.to));return{diffs:l,fromVersion:o,toVersion:s}}async function M(e,n,r){if(!C(e))throw new t(e.entity.name,`versions`);let i=await A(e,n,r);if(!i)throw Error(`Version ${r} not found for entity ${n}`);let a=i.data,o=E(e.entity,a);return e.admin.update(n,o)}async function N(e,t,n=`_`){if(i(n),!e.versionableOptions)return;let r=C(e);if(!r)return;let a=e.versionableOptions.maxPerDoc,o=await r.findMany({where:{entityId:t,locale:n},orderBy:[{column:`version`,dir:`desc`}],limit:a});if(o.length<a)return;let s=o.map(e=>e.id);await r.deleteMany({entityId:t,locale:n,id:{notIn:s}})}async function P(e,t,n,r,a,o=`_`,s){i(o);let c=C(e);if(!c)return;let[l]=await c.aggregate({select:{maxVersion:{fn:`max`,column:`version`}},where:{entityId:t,locale:o}}),u=Number(l?.maxVersion??0)+1,d=n.status??null;if(a){let e=await c.findOne({entityId:t,isAutosave:!0,version:u,locale:o});if(e)return await c.update({id:e.id},{data:n,delta:r,status:d,createdAt:new Date}),u}return await c.insert({entityId:t,version:u,locale:o,data:n,delta:r,status:d,createdBy:s?.id??null,createdByName:s?.name??null,isAutosave:a}),u}async function F(e,t,n,r,a,o){if(a!==void 0&&i(a),!e.versionableOptions)return;let s=a??`_`,c=await P(e,t,n,r?O(r,n):null,!1,s,o);c!==void 0&&s===`_`&&await I(e,t,c),await N(e,t,s)}async function I(e,t,n){let r=s.get(e.entity.name);r&&await e.db.update(r).set({_version:n}).where(l(r.id,t))}function L(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 R(e,n,r,a=`_`,o){i(a);let s=S(e);if(!s)throw new t(e.entity.name,`drafts`);let c=new Date,l=o?.id??``,u=o?.name??null;return L(await s.upsert({entityId:n,locale:a,data:r,createdBy:l,createdByName:u,createdAt:c,updatedAt:c},{target:[`entityId`,`locale`],set:{data:r,updatedAt:c}}))}async function z(e,t,n=`_`){i(n);let r=S(e);if(!r)return null;let a=await r.findOne({entityId:t,locale:n});return a?L(a):null}async function B(e,t,n=`_`){i(n);let r=S(e);return r?(await r.deleteMany({entityId:t,locale:n})).length>0:!1}async function V(e,n,r=`_`,a){if(i(r),!S(e))throw new t(e.entity.name,`drafts`);let o=await z(e,n,r);if(!o)throw Error(`No draft found for entity ${n} locale ${r}`);let s=null;e.versionableOptions&&(s=await e.admin.findById(n));let c=o.data,l=E(e.entity,c),u=await e.admin.update(n,l);return await B(e,n,r),e.versionableOptions&&await F(e,n,u,s,r,a),u}async function H(e,t,n=`_`){i(n);let r=S(e);return r?r.exists({entityId:t,locale:n}):!1}async function U(e,t=`_`){i(t);let n=S(e);return n?n.count({locale:t}):0}async function W(e,t,n=`_`){i(n);let r=new Set;if(t.length===0)return r;let a=S(e);if(!a)return r;let o=await a.findMany({where:{entityId:{in:t},locale:n}});for(let e of o)r.add(e.entityId);return r}async function G(e,t,r=`_`){i(r);let a=await z(e,t,r);if(!a)return null;let o=n().defaultLocale,s=r===`_`?{defaultLocale:o}:{locale:r,defaultLocale:o},c=await e.admin.findById(t,s);if(!c)return null;let l=e.entity.allFields,u=c,d=a.data,f={};for(let e of Object.keys(d))f[e]=u[e];let p=h(f,d,l);for(let e of p)e.type===`blocks`&&Array.isArray(e.from)&&Array.isArray(e.to)&&(e.blockDiffs=b(e.from,e.to));return{diffs:p,current:c,draft:a}}async function K(e,t,n){i(n.from);let r=Array.isArray(n.to)?n.to:[n.to];for(let e of r)i(e);let a=s.get(`${e.entity.name}_layout`);if(!a)return;let o=await e.db.select().from(a).where(c(l(a.entityId,t),l(a.locale,n.from))).orderBy(a.sortOrder);if(o.length!==0){for(let n of r){await e.db.delete(a).where(c(l(a.entityId,t),l(a.locale,n)));for(let t of o)await e.db.insert(a).values({entityId:t.entityId,fieldName:t.fieldName,blockType:t.blockType,sortOrder:t.sortOrder,data:t.data,locale:n})}e.logger?.debug({entityId:t,from:n.from,to:r},`Layout copied between locales`)}}async function q(e,t){let n=(await Y(e,[t])).get(t);return n?Object.values(n):[]}async function J(e,r,a,o){if(i(a),a===n().defaultLocale){let t={status:o};o===`published`&&(t.publishedAt=new Date),await e.admin.update(r,t);return}let s=w(e);if(!s)throw new t(e.entity.name,`localeStatus`);let c=o===`published`?new Date:null;await s.upsert({entityId:r,locale:a,status:o,publishedAt:c},{target:[`entityId`,`locale`],set:{status:o,publishedAt:c}})}async function Y(e,t){let n=new Map;if(t.length===0)return n;let r=w(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 X(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 Z(e,t){return(await Q(e,[t])).get(t)??{}}async function Q(e,t){let r=new Map;if(t.length===0)return r;let i=n(),a=Object.entries(e.entity.allFields).filter(([e,t])=>t.translatable).map(([e])=>e);if(a.length===0)return r;let o=s.get(`${e.entity.name}_translations`);if(!o)return r;let c=await e.db.select().from(o).where(u(o.entityId,t)),l=new Map;for(let e of c){let t=e,n=t.entityId,r=t.locale,i={};for(let e of a)i[e]=t[e];let o=l.get(n);o||(o=new Map,l.set(n,o)),o.set(r,i)}for(let e of t)r.set(e,X(a,l.get(e),i.locales,i.defaultLocale));return r}async function te(e,t){let r=n(),i=t?.locale??r.locales.find(e=>e.code!==r.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 Q(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 $=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 P(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=O(n,r),i=await P(this.ctx,e,r,t,!1);if(i!==void 0)return await I(this.ctx,e,i),await N(this.ctx,e),{...r,_version:i};await N(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)}copyLayout(e,t){return K(this.ctx,e,t)}getVersions(e,t){return k(this.ctx,e,t)}getVersion(e,t,n){return A(this.ctx,e,t,n)}compareVersions(e,t,n,r){return j(this.ctx,e,t,n,r)}restoreVersion(e,t){return M(this.ctx,e,t)}pruneVersions(e,t){return N(this.ctx,e,t)}createVersionSnapshot(e,t,n,r,i){return F(this.ctx,e,t,n,r,i)}getTranslationStatus(e){return Z(this.ctx,e)}getTranslationStatusBatch(e){return Q(this.ctx,e)}getTranslationReport(e){return te(this.ctx,e)}getLocaleStatus(e){return q(this.ctx,e)}setLocaleStatus(e,t,n){return J(this.ctx,e,t,n)}getLocaleStatusBatch(e){return Y(this.ctx,e)}saveDraft(e,t,n,r){return R(this.ctx,e,t,n,r)}getDraft(e,t){return z(this.ctx,e,t)}deleteDraft(e,t){return B(this.ctx,e,t)}publishDraft(e,t,n){return V(this.ctx,e,t,n)}hasDraft(e,t){return H(this.ctx,e,t)}countDrafts(e){return U(this.ctx,e)}hasDraftBatch(e,t){return W(this.ctx,e,t)}compareDraftWithCurrent(e,t){return G(this.ctx,e,t)}};function ne(e){return new $({admin:o(e),entity:e,logger:a().logger.child({content:e.name})})}export{$ as ContentClient,ne as createContentClient};
|
|
2
2
|
//# sourceMappingURL=client.mjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
function e(e){if(!e.slug||!e.slug.match(/^[a-z][a-z0-9-]*$/))throw Error(`Block slug must be lowercase alphanumeric with hyphens, got: "${e.slug}"`);if(!e.label)throw Error(`Block label is required for slug: "${e.slug}"`);if(!e.fields||Object.keys(e.fields).length===0)throw Error(`Block "${e.slug}" must have at least one field`);return{slug:e.slug,label:e.label,fields:e.fields}}export{e as t};
|
|
2
|
-
//# sourceMappingURL=define-block-
|
|
2
|
+
//# sourceMappingURL=define-block-DpMPEEdA.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-block-
|
|
1
|
+
{"version":3,"file":"define-block-DpMPEEdA.mjs","names":[],"sources":["../src/define-block.ts"],"sourcesContent":["/**\n * Block definition API\n *\n * defineBlock() creates a reusable content block with typed fields.\n * Uses `const F` generic to preserve literal field types for inference.\n *\n * Blocks are NOT entities — they lack independent identity, CRUD, and access control.\n * They exist only as embedded data within a parent entity's blocks field.\n *\n * @example\n * const HeroBlock = defineBlock({\n * slug: 'hero',\n * label: 'Hero Section',\n * fields: {\n * heading: field.text({ required: true, translatable: true }),\n * background: field.media({ accept: ['image/*'] }),\n * },\n * })\n */\n\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport type { BlockDefinition } from './types.js'\n\nexport function defineBlock<const F extends Record<string, FieldConfig>>(definition: {\n slug: string\n label: string\n fields: F\n}): BlockDefinition<F> {\n if (!definition.slug || !definition.slug.match(/^[a-z][a-z0-9-]*$/)) {\n throw new Error(\n `Block slug must be lowercase alphanumeric with hyphens, got: \"${definition.slug}\"`,\n )\n }\n\n if (!definition.label) {\n throw new Error(`Block label is required for slug: \"${definition.slug}\"`)\n }\n\n if (!definition.fields || Object.keys(definition.fields).length === 0) {\n throw new Error(`Block \"${definition.slug}\" must have at least one field`)\n }\n\n return {\n slug: definition.slug,\n label: definition.label,\n fields: definition.fields,\n }\n}\n"],"mappings":"AAuBA,SAAgB,EAAyD,EAIlD,CACrB,GAAI,CAAC,EAAW,MAAQ,CAAC,EAAW,KAAK,MAAM,oBAAoB,CACjE,MAAU,MACR,iEAAiE,EAAW,KAAK,GAClF,CAGH,GAAI,CAAC,EAAW,MACd,MAAU,MAAM,sCAAsC,EAAW,KAAK,GAAG,CAG3E,GAAI,CAAC,EAAW,QAAU,OAAO,KAAK,EAAW,OAAO,CAAC,SAAW,EAClE,MAAU,MAAM,UAAU,EAAW,KAAK,gCAAgC,CAG5E,MAAO,CACL,KAAM,EAAW,KACjB,MAAO,EAAW,MAClB,OAAQ,EAAW,OACpB"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { C as VersionableFields, h as WorkflowableFields } from "../index-CB0fdMs1.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/entities/site-layout.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `site_layout` — the singleton document entity that holds site-wide
|
|
6
|
+
* shared content (header, footer, optional before/after-content regions).
|
|
7
|
+
*
|
|
8
|
+
* Phase 05 §2 defines this as a `kind: 'singleton'` entity with one
|
|
9
|
+
* versionable row per (site, locale). The render-time composer
|
|
10
|
+
* (`composeMultiScopeDocument`) joins `site_layout` blocks with the
|
|
11
|
+
* active page's blocks before applying overrides; the result is the
|
|
12
|
+
* page's true rendered shape.
|
|
13
|
+
*
|
|
14
|
+
* The entity ships as a factory function so apps wire it into their
|
|
15
|
+
* `entities` array without us hard-coding the per-app `access` rules:
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* // apps/.../entities/site-layout.ts
|
|
20
|
+
* import { siteLayoutEntity } from '@murumets-ee/content/entities'
|
|
21
|
+
*
|
|
22
|
+
* export const SiteLayout = siteLayoutEntity()
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Plugin contributions (PR 12): the admin route is auto-mounted by the
|
|
26
|
+
* blocks plugin when an entity declares the marker symbol. Permissions
|
|
27
|
+
* — `site_layout:view` (editor), `site_layout:update` (admin),
|
|
28
|
+
* `site_layout:publish` (admin) — flow from the `access` map below;
|
|
29
|
+
* admins can override via per-customer permission catalog.
|
|
30
|
+
*
|
|
31
|
+
* Behaviors layered onto the entity:
|
|
32
|
+
*
|
|
33
|
+
* - `auditable()` — created/updated stamps
|
|
34
|
+
* - `publishable()` — draft / published lifecycle (publishing site_layout
|
|
35
|
+
* affects every page that doesn't override it)
|
|
36
|
+
* - `versionable({ maxPerDoc: 50 })` — per-locale history; longer
|
|
37
|
+
* retention than pages because site-layout edits are higher-stakes
|
|
38
|
+
* - `workflowable()` — same approval gates as pages (so the same
|
|
39
|
+
* review surface applies)
|
|
40
|
+
* - `withOpLog({ retentionDays: 30 })` — longer op retention than
|
|
41
|
+
* pages; site-layout undo windows tend to be longer because changes
|
|
42
|
+
* are less frequent + higher-impact
|
|
43
|
+
*
|
|
44
|
+
* NOTE on singleton enforcement: `kind: 'singleton'` is metadata-only
|
|
45
|
+
* at the framework level — there's no runtime "at most one row" gate.
|
|
46
|
+
* For v1 PR 12 we rely on (a) seed/wizard creating the one row and
|
|
47
|
+
* (b) the admin UI hiding "Create new" for singleton entities. A
|
|
48
|
+
* future hardening PR will add generic runtime enforcement to
|
|
49
|
+
* `AdminClient.create`.
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* Optional overrides apps can apply when calling the factory. The
|
|
53
|
+
* defaults match phase 05 §7 — editor reads, admin writes / publishes.
|
|
54
|
+
*/
|
|
55
|
+
interface SiteLayoutEntityOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Roles allowed to read site_layout. Default `'editor'`. The admin
|
|
58
|
+
* sidebar still gates on `site_layout:view`, but lowering this to
|
|
59
|
+
* `'public'` lets headless API consumers query without auth (rare).
|
|
60
|
+
*/
|
|
61
|
+
viewRole?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Roles allowed to mutate site_layout. Default `'admin'` — high
|
|
64
|
+
* blast radius. Override to `'editor'` for trusted teams; the per-
|
|
65
|
+
* customer permission catalog (#357) is the right configuration
|
|
66
|
+
* surface for per-deployment relaxation.
|
|
67
|
+
*/
|
|
68
|
+
updateRole?: string;
|
|
69
|
+
/** Roles allowed to publish. Default `'admin'`. */
|
|
70
|
+
publishRole?: string;
|
|
71
|
+
/** Override the version-retention cap. Default 50 versions per (site, locale). */
|
|
72
|
+
maxVersions?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build the `site_layout` entity. Apps add the returned entity to their
|
|
76
|
+
* `defineLumiConfig({ entities: [...] })` list; the blocks plugin
|
|
77
|
+
* auto-mounts the admin route + emits permission catalog entries.
|
|
78
|
+
*
|
|
79
|
+
* Returns a fresh entity per call — multiple calls in the same process
|
|
80
|
+
* produce structurally-identical but referentially-distinct entities.
|
|
81
|
+
* That's fine in practice; the framework deduplicates by entity name at
|
|
82
|
+
* plugin merge time.
|
|
83
|
+
*
|
|
84
|
+
* The return type is intentionally INFERRED (no annotation) so the
|
|
85
|
+
* `'site_layout'` name stays as a string literal in the caller's
|
|
86
|
+
* `Entity<…, 'site_layout', …>` view. Annotating it as bare `Entity`
|
|
87
|
+
* would widen N to `string`, which propagates to the resolved-
|
|
88
|
+
* permissions union and re-introduces a `${string}:view` template
|
|
89
|
+
* (caught by the wide-template guards in
|
|
90
|
+
* `apps/admin-playground/scripts/typed-permissions-demo.ts`).
|
|
91
|
+
*/
|
|
92
|
+
declare function siteLayoutEntity(options?: SiteLayoutEntityOptions): import("@murumets-ee/entity").Entity<{
|
|
93
|
+
id: import("@murumets-ee/entity").IdField;
|
|
94
|
+
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & VersionableFields & WorkflowableFields & {
|
|
95
|
+
slug: import("@murumets-ee/entity").TextField & {
|
|
96
|
+
readonly required: true;
|
|
97
|
+
readonly default: "default";
|
|
98
|
+
readonly indexed: true;
|
|
99
|
+
};
|
|
100
|
+
label: import("@murumets-ee/entity").TextField & {
|
|
101
|
+
readonly translatable: true;
|
|
102
|
+
};
|
|
103
|
+
headerBlocks: import("@murumets-ee/entity").JsonField & {
|
|
104
|
+
readonly default: () => never[];
|
|
105
|
+
};
|
|
106
|
+
footerBlocks: import("@murumets-ee/entity").JsonField & {
|
|
107
|
+
readonly default: () => never[];
|
|
108
|
+
};
|
|
109
|
+
beforeContentBlocks: import("@murumets-ee/entity").JsonField & {
|
|
110
|
+
readonly default: () => never[];
|
|
111
|
+
};
|
|
112
|
+
afterContentBlocks: import("@murumets-ee/entity").JsonField & {
|
|
113
|
+
readonly default: () => never[];
|
|
114
|
+
};
|
|
115
|
+
siteName: import("@murumets-ee/entity").TextField & {
|
|
116
|
+
readonly translatable: true;
|
|
117
|
+
};
|
|
118
|
+
siteLogo: import("@murumets-ee/entity").MediaField & Partial<import("@murumets-ee/entity").MediaField>;
|
|
119
|
+
siteFavicon: import("@murumets-ee/entity").MediaField & Partial<import("@murumets-ee/entity").MediaField>;
|
|
120
|
+
}, "site_layout", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<VersionableFields, string>, import("@murumets-ee/entity").Behavior<WorkflowableFields, string>]>;
|
|
121
|
+
//#endregion
|
|
122
|
+
export { type SiteLayoutEntityOptions, siteLayoutEntity };
|
|
123
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/entities/site-layout.ts"],"mappings":";;;;;;AAyDA;;;;;;;;;;AAsCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAtCiB,uBAAA;;;;;;EAMf,QAAA;;;;;;;EAOA,UAAA;;EAEA,WAAA;;EAEA,WAAA;AAAA;;;;;;;;;;;;;;;;;;;iBAqBc,gBAAA,CAAiB,OAAA,GAAU,uBAAA,iCAAuB,MAAA;;sGAAA,iBAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{n as e}from"../versionable-B0bP2Gib.mjs";import{a as t}from"../workflowable-aGtQhPRc.mjs";import{auditable as n,defineEntity as r,field as i,publishable as a}from"@murumets-ee/entity";function o(o){let s=o?.viewRole??`editor`,c=o?.updateRole??`admin`,l=o?.publishRole??`admin`,u=o?.maxVersions??50;return r({name:`site_layout`,kind:`singleton`,fields:{slug:i.text({required:!0,default:`default`,indexed:!0}),label:i.text({translatable:!0}),headerBlocks:i.json({default:()=>[]}),footerBlocks:i.json({default:()=>[]}),beforeContentBlocks:i.json({default:()=>[]}),afterContentBlocks:i.json({default:()=>[]}),siteName:i.text({translatable:!0}),siteLogo:i.media(),siteFavicon:i.media()},behaviors:[n(),a(),e({maxPerDoc:u}),t()],scope:`global`,access:{view:s,create:l,update:c,delete:l},admin:{icon:`layout`,description:`Shared header / footer / pre / post-content blocks rendered on every page that does not override them.`,rootFields:[`slug`,`label`]}})}export{o as siteLayoutEntity};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/entities/site-layout.ts"],"sourcesContent":["/**\n * `site_layout` — the singleton document entity that holds site-wide\n * shared content (header, footer, optional before/after-content regions).\n *\n * Phase 05 §2 defines this as a `kind: 'singleton'` entity with one\n * versionable row per (site, locale). The render-time composer\n * (`composeMultiScopeDocument`) joins `site_layout` blocks with the\n * active page's blocks before applying overrides; the result is the\n * page's true rendered shape.\n *\n * The entity ships as a factory function so apps wire it into their\n * `entities` array without us hard-coding the per-app `access` rules:\n *\n * @example\n * ```ts\n * // apps/.../entities/site-layout.ts\n * import { siteLayoutEntity } from '@murumets-ee/content/entities'\n *\n * export const SiteLayout = siteLayoutEntity()\n * ```\n *\n * Plugin contributions (PR 12): the admin route is auto-mounted by the\n * blocks plugin when an entity declares the marker symbol. Permissions\n * — `site_layout:view` (editor), `site_layout:update` (admin),\n * `site_layout:publish` (admin) — flow from the `access` map below;\n * admins can override via per-customer permission catalog.\n *\n * Behaviors layered onto the entity:\n *\n * - `auditable()` — created/updated stamps\n * - `publishable()` — draft / published lifecycle (publishing site_layout\n * affects every page that doesn't override it)\n * - `versionable({ maxPerDoc: 50 })` — per-locale history; longer\n * retention than pages because site-layout edits are higher-stakes\n * - `workflowable()` — same approval gates as pages (so the same\n * review surface applies)\n * - `withOpLog({ retentionDays: 30 })` — longer op retention than\n * pages; site-layout undo windows tend to be longer because changes\n * are less frequent + higher-impact\n *\n * NOTE on singleton enforcement: `kind: 'singleton'` is metadata-only\n * at the framework level — there's no runtime \"at most one row\" gate.\n * For v1 PR 12 we rely on (a) seed/wizard creating the one row and\n * (b) the admin UI hiding \"Create new\" for singleton entities. A\n * future hardening PR will add generic runtime enforcement to\n * `AdminClient.create`.\n */\n\nimport { auditable, publishable } from '@murumets-ee/entity'\nimport { defineEntity, field } from '@murumets-ee/entity'\nimport { versionable } from '../behaviors/versionable.js'\nimport { workflowable } from '../behaviors/workflowable.js'\n\n/**\n * Optional overrides apps can apply when calling the factory. The\n * defaults match phase 05 §7 — editor reads, admin writes / publishes.\n */\nexport interface SiteLayoutEntityOptions {\n /**\n * Roles allowed to read site_layout. Default `'editor'`. The admin\n * sidebar still gates on `site_layout:view`, but lowering this to\n * `'public'` lets headless API consumers query without auth (rare).\n */\n viewRole?: string\n /**\n * Roles allowed to mutate site_layout. Default `'admin'` — high\n * blast radius. Override to `'editor'` for trusted teams; the per-\n * customer permission catalog (#357) is the right configuration\n * surface for per-deployment relaxation.\n */\n updateRole?: string\n /** Roles allowed to publish. Default `'admin'`. */\n publishRole?: string\n /** Override the version-retention cap. Default 50 versions per (site, locale). */\n maxVersions?: number\n}\n\n/**\n * Build the `site_layout` entity. Apps add the returned entity to their\n * `defineLumiConfig({ entities: [...] })` list; the blocks plugin\n * auto-mounts the admin route + emits permission catalog entries.\n *\n * Returns a fresh entity per call — multiple calls in the same process\n * produce structurally-identical but referentially-distinct entities.\n * That's fine in practice; the framework deduplicates by entity name at\n * plugin merge time.\n *\n * The return type is intentionally INFERRED (no annotation) so the\n * `'site_layout'` name stays as a string literal in the caller's\n * `Entity<…, 'site_layout', …>` view. Annotating it as bare `Entity`\n * would widen N to `string`, which propagates to the resolved-\n * permissions union and re-introduces a `${string}:view` template\n * (caught by the wide-template guards in\n * `apps/admin-playground/scripts/typed-permissions-demo.ts`).\n */\nexport function siteLayoutEntity(options?: SiteLayoutEntityOptions) {\n const viewRole = options?.viewRole ?? 'editor'\n const updateRole = options?.updateRole ?? 'admin'\n const publishRole = options?.publishRole ?? 'admin'\n const maxVersions = options?.maxVersions ?? 50\n\n return defineEntity({\n name: 'site_layout',\n kind: 'singleton',\n fields: {\n // Singleton-friendly key — apps may pre-seed a row with a known id\n // (e.g. 'default') so the admin route + composer can find it\n // without scanning. Translatable: false so the slot itself\n // doesn't fork per-locale (the BLOCKS inside each region fork\n // per-locale via versionable's chain, but the slot id is global).\n slug: field.text({\n required: true,\n default: 'default',\n indexed: true,\n }),\n label: field.text({ translatable: true }),\n // Shared content regions — each is its own block array. Validated\n // by the blocks plugin's `validateBlockTree` hook (PR 4).\n headerBlocks: field.json({ default: () => [] }),\n footerBlocks: field.json({ default: () => [] }),\n // Optional pre/post-content regions per phase 05 §2.\n beforeContentBlocks: field.json({ default: () => [] }),\n afterContentBlocks: field.json({ default: () => [] }),\n // Site metadata that some blocks might reference.\n siteName: field.text({ translatable: true }),\n siteLogo: field.media(),\n siteFavicon: field.media(),\n },\n behaviors: [\n auditable(),\n publishable(),\n versionable({ maxPerDoc: maxVersions }),\n workflowable(),\n // withOpLog ships from `@murumets-ee/blocks/server` — apps that\n // want the block-editor surface for site_layout should layer it\n // onto the entity at app-config time (matching how the playground\n // wires page-with-blocks). Keeping it out of the factory keeps the\n // content package's import direction one-way (content doesn't\n // depend on blocks).\n ],\n scope: 'global',\n access: {\n view: viewRole,\n // Singleton — apps usually seed the row; `create` admin gating\n // prevents accidental \"second row\" but stays open for the seed\n // path (which runs as the system role).\n create: publishRole,\n update: updateRole,\n delete: publishRole,\n },\n admin: {\n icon: 'layout',\n description:\n 'Shared header / footer / pre / post-content blocks rendered on every page that does not override them.',\n rootFields: ['slug', 'label'],\n },\n })\n}\n"],"mappings":"+LA+FA,SAAgB,EAAiB,EAAmC,CAClE,IAAM,EAAW,GAAS,UAAY,SAChC,EAAa,GAAS,YAAc,QACpC,EAAc,GAAS,aAAe,QACtC,EAAc,GAAS,aAAe,GAE5C,OAAO,EAAa,CAClB,KAAM,cACN,KAAM,YACN,OAAQ,CAMN,KAAM,EAAM,KAAK,CACf,SAAU,GACV,QAAS,UACT,QAAS,GACV,CAAC,CACF,MAAO,EAAM,KAAK,CAAE,aAAc,GAAM,CAAC,CAGzC,aAAc,EAAM,KAAK,CAAE,YAAe,EAAE,CAAE,CAAC,CAC/C,aAAc,EAAM,KAAK,CAAE,YAAe,EAAE,CAAE,CAAC,CAE/C,oBAAqB,EAAM,KAAK,CAAE,YAAe,EAAE,CAAE,CAAC,CACtD,mBAAoB,EAAM,KAAK,CAAE,YAAe,EAAE,CAAE,CAAC,CAErD,SAAU,EAAM,KAAK,CAAE,aAAc,GAAM,CAAC,CAC5C,SAAU,EAAM,OAAO,CACvB,YAAa,EAAM,OAAO,CAC3B,CACD,UAAW,CACT,GAAW,CACX,GAAa,CACb,EAAY,CAAE,UAAW,EAAa,CAAC,CACvC,GAAc,CAOf,CACD,MAAO,SACP,OAAQ,CACN,KAAM,EAIN,OAAQ,EACR,OAAQ,EACR,OAAQ,EACT,CACD,MAAO,CACL,KAAM,SACN,YACE,yGACF,WAAY,CAAC,OAAQ,QAAQ,CAC9B,CACF,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
var e=class extends Error{entityName;capability;constructor(e,t,n){super(n??`Entity "${e}" does not support ${t}. Add the corresponding behavior to defineEntity({ behaviors: [...] }).`),this.name=`EntityCapabilityError`,this.entityName=e,this.capability=t}},t=class extends Error{locale;allowed;constructor(e,t){super(`Invalid locale "${e}". Configured locales: [${t.join(`, `)}]. (The "_" sentinel is also accepted for default-locale storage.)`),this.name=`InvalidLocaleError`,this.locale=e,this.allowed=t}};export{t as n,e as t};
|
|
2
|
-
//# sourceMappingURL=errors-
|
|
2
|
+
//# sourceMappingURL=errors-CVBrIU1K.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors-
|
|
1
|
+
{"version":3,"file":"errors-CVBrIU1K.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Domain errors for ContentClient + LockService.\n */\n\n/**\n * Thrown when a caller invokes an entity-capability-specific method on an\n * entity that doesn't have the corresponding behavior — e.g. `saveDraft`\n * on a non-publishable entity, `setLocaleStatus` on an entity without\n * publishable+translatable, `restoreVersion` on a non-versionable entity.\n *\n * Distinct from generic `Error` so admin-ui's API handler can map it to\n * 400 Bad Request (caller's fault) rather than 500 (server bug).\n */\nexport class EntityCapabilityError extends Error {\n readonly entityName: string\n readonly capability: 'drafts' | 'versions' | 'localeStatus' | 'translations'\n\n constructor(\n entityName: string,\n capability: EntityCapabilityError['capability'],\n message?: string,\n ) {\n super(\n message ??\n `Entity \"${entityName}\" does not support ${capability}. ` +\n `Add the corresponding behavior to defineEntity({ behaviors: [...] }).`,\n )\n this.name = 'EntityCapabilityError'\n this.entityName = entityName\n this.capability = capability\n }\n}\n\n/**\n * Thrown when a locale string passed at runtime is not configured in the\n * content plugin. The plugin validates `defaultLocale` at init time, but\n * the per-call `locale` parameter on every read/write is not naturally\n * checked — `validateLocale()` is the boundary check.\n *\n * Allows the `'_'` sentinel (used internally for default-locale rows in\n * versions/drafts/locale_status tables) so callers can pass it through.\n */\nexport class InvalidLocaleError extends Error {\n readonly locale: string\n readonly allowed: readonly string[]\n\n constructor(locale: string, allowed: readonly string[]) {\n super(\n `Invalid locale \"${locale}\". Configured locales: [${allowed.join(', ')}]. ` +\n `(The \"_\" sentinel is also accepted for default-locale storage.)`,\n )\n this.name = 'InvalidLocaleError'\n this.locale = locale\n this.allowed = allowed\n }\n}\n"],"mappings":"AAaA,IAAa,EAAb,cAA2C,KAAM,CAC/C,WACA,WAEA,YACE,EACA,EACA,EACA,CACA,MACE,GACE,WAAW,EAAW,qBAAqB,EAAW,yEAEzD,CACD,KAAK,KAAO,wBACZ,KAAK,WAAa,EAClB,KAAK,WAAa,IAaT,EAAb,cAAwC,KAAM,CAC5C,OACA,QAEA,YAAY,EAAgB,EAA4B,CACtD,MACE,mBAAmB,EAAO,0BAA0B,EAAQ,KAAK,KAAK,CAAC,oEAExE,CACD,KAAK,KAAO,qBACZ,KAAK,OAAS,EACd,KAAK,QAAU"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { p as VersionableOptions, t as BlockDefinition } from "./types-
|
|
2
|
-
import { Behavior, DateField, Entity, FieldConfig, NumberField, SelectField, TextField } from "@murumets-ee/entity";
|
|
1
|
+
import { p as VersionableOptions, t as BlockDefinition } from "./types-CWo2dfEK.mjs";
|
|
2
|
+
import { Behavior, DateField, Entity, FieldConfig, JsonField, NumberField, SelectField, TextField } from "@murumets-ee/entity";
|
|
3
3
|
|
|
4
4
|
//#region src/behaviors/versionable.d.ts
|
|
5
5
|
type VersionableFields = {
|
|
@@ -14,6 +14,40 @@ declare function versionable(options?: VersionableOptions): Behavior<Versionable
|
|
|
14
14
|
*/
|
|
15
15
|
declare function getVersionableOptions(behaviors?: Behavior[]): Required<VersionableOptions> | null;
|
|
16
16
|
//#endregion
|
|
17
|
+
//#region src/behaviors/with-overrides.d.ts
|
|
18
|
+
/**
|
|
19
|
+
* Override-directive shape. Mirrored from `@murumets-ee/blocks` to avoid
|
|
20
|
+
* a circular dep (content → blocks → content); content stores the
|
|
21
|
+
* persisted JSON, blocks owns the resolver. Keep the two shapes in sync.
|
|
22
|
+
*/
|
|
23
|
+
type OverrideDirective = {
|
|
24
|
+
kind: 'hide';
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'patch';
|
|
27
|
+
props?: Record<string, unknown>;
|
|
28
|
+
traits?: Record<string, Record<string, unknown>>;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'replace';
|
|
31
|
+
block: Record<string, unknown>;
|
|
32
|
+
};
|
|
33
|
+
type OverridesMap = Record<string, OverrideDirective>;
|
|
34
|
+
type WithOverridesFields = {
|
|
35
|
+
_overrides: JsonField;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Mark an entity as participating in the page-scoped override resolver.
|
|
39
|
+
* See module-level JSDoc for the full contract.
|
|
40
|
+
*/
|
|
41
|
+
declare function withOverrides(): Behavior<WithOverridesFields, 'withOverrides'>;
|
|
42
|
+
/**
|
|
43
|
+
* Check whether an entity declares `withOverrides()`. Returns `true`
|
|
44
|
+
* when the entity's behaviors list contains a behavior tagged with
|
|
45
|
+
* the marker symbol. Used by the boot orphan-sweep + admin UI gating.
|
|
46
|
+
*/
|
|
47
|
+
declare function hasWithOverrides(behaviors: ReadonlyArray<{
|
|
48
|
+
name: string;
|
|
49
|
+
} & Record<symbol, unknown>> | undefined): boolean;
|
|
50
|
+
//#endregion
|
|
17
51
|
//#region src/behaviors/workflowable.d.ts
|
|
18
52
|
declare const WORKFLOW_STATES: readonly ["draft", "in_review", "approved", "rejected", "published"];
|
|
19
53
|
type WorkflowState = (typeof WORKFLOW_STATES)[number];
|
|
@@ -144,5 +178,5 @@ declare class InvalidLocaleError extends Error {
|
|
|
144
178
|
constructor(locale: string, allowed: readonly string[]);
|
|
145
179
|
}
|
|
146
180
|
//#endregion
|
|
147
|
-
export { workflowable as _, DiffSegment as a, defineBlock as c, WORKFLOW_STATES as d, WORKFLOW_TRANSITIONS as f, isAllowedTransition as g, WorkflowableFields as h, BlockDiffStatus as i, EntityCapability as l, WorkflowTransitionError as m, InvalidLocaleError as n, FieldDiff as o, WorkflowState as p, BlockDiffEntry as r, PropDiff as s, EntityCapabilityError as t, entitySupports as u,
|
|
148
|
-
//# sourceMappingURL=index-
|
|
181
|
+
export { VersionableFields as C, withOverrides as S, versionable as T, workflowable as _, DiffSegment as a, WithOverridesFields as b, defineBlock as c, WORKFLOW_STATES as d, WORKFLOW_TRANSITIONS as f, isAllowedTransition as g, WorkflowableFields as h, BlockDiffStatus as i, EntityCapability as l, WorkflowTransitionError as m, InvalidLocaleError as n, FieldDiff as o, WorkflowState as p, BlockDiffEntry as r, PropDiff as s, EntityCapabilityError as t, entitySupports as u, OverrideDirective as v, getVersionableOptions as w, hasWithOverrides as x, OverridesMap as y };
|
|
182
|
+
//# sourceMappingURL=index-CB0fdMs1.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-CB0fdMs1.d.mts","names":[],"sources":["../src/behaviors/versionable.ts","../src/behaviors/with-overrides.ts","../src/behaviors/workflowable.ts","../src/capabilities.ts","../src/define-block.ts","../src/diff/types.ts","../src/errors.ts"],"mappings":";;;;KAmBY,iBAAA;EACV,QAAA,EAAU,WAAA;IAAgB,QAAA;EAAA;EAC1B,YAAA,EAAc,SAAA;AAAA;AAAA,iBAQA,WAAA,CAAY,OAAA,GAAU,kBAAA,GAAqB,QAAA,CAAS,iBAAA;;;;iBAqDpD,qBAAA,CAAsB,SAAA,GAAY,QAAA,KAAa,QAAA,CAAS,kBAAA;;;;ACrCV;;;;KAQzD,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,MAAA,SAAe,iBAAA;AAAA,KAEvB,mBAAA;EAcV,UAAA,EAAY,SAAA;AAAA;;;;AAlBsB;iBAsLpB,aAAA,CAAA,GAAiB,QAAA,CAAS,mBAAA;;;;AAlL1C;;iBA8NgB,gBAAA,CACd,SAAA,EAAW,aAAA;EAAgB,IAAA;AAAA,IAAiB,MAAA;;;cCjPjC,eAAA;AAAA,KACD,aAAA,WAAwB,eAAA;;;;;;;;cASvB,oBAAA;EAAA;;;;;;;;;iBAWG,mBAAA,CAAoB,IAAA,EAAM,aAAA,EAAe,EAAA,EAAI,aAAA;;;;;cAUhD,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,QAAA,CAAS,kBAAA;;;KC7E7B,gBAAA;;;;;;iBAOI,cAAA,CAAe,MAAA,EAAQ,MAAA,EAAQ,UAAA,EAAY,gBAAA;;;iBCN3C,WAAA,iBAA4B,MAAA,SAAe,WAAA,EAAA,CAAc,UAAA;EACvE,IAAA;EACA,KAAA;EACA,MAAA,EAAQ,CAAA;AAAA,IACN,eAAA,CAAgB,CAAA;;;;;;;UCtBH,WAAA;EACf,IAAA;EACA,KAAA;AAAA;;UAIe,SAAA;EACf,KAAA;EACA,IAAA;EAWA,IAAA;EACA,EAAA;ELJuB;EKMvB,QAAA,GAAW,WAAA;ELEc;EKAzB,UAAA,GAAa,cAAA;AAAA;;KAIH,eAAA;;UAGK,cAAA;EACf,OAAA;EACA,SAAA;EACA,MAAA,EAAQ,eAAA;EACR,SAAA;EACA,OAAA;ELZmF;EKcnF,SAAA,GAAY,QAAA;AAAA;;UAIG,QAAA;EACf,KAAA;EACA,IAAA;EACA,EAAA;ELgCqE;EK9BrE,QAAA,GAAW,WAAA;AAAA;;;;;;;ALjCb;;;;;;;;cMNa,qBAAA,SAA8B,KAAA;EAAA,SAChC,UAAA;EAAA,SACA,UAAA;cAGP,UAAA,UACA,UAAA,EAAY,qBAAA,gBACZ,OAAA;AAAA;;;;;;;;;;cAsBS,kBAAA,SAA2B,KAAA;EAAA,SAC7B,MAAA;EAAA,SACA,OAAA;cAEG,MAAA,UAAgB,OAAA;AAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as InferBlocksUnion, c as LockInfo, d as TranslationStatus, f as VersionEntry, i as InferBlockData, l as TranslationFieldStatus, n as ContentConfig, o as LocaleConfig, p as VersionableOptions, r as DraftEntry, t as BlockDefinition, u as TranslationReport } from "./types-
|
|
2
|
-
import { _ as workflowable, a as DiffSegment, c as defineBlock, d as WORKFLOW_STATES, f as WORKFLOW_TRANSITIONS, g as isAllowedTransition, h as WorkflowableFields, i as BlockDiffStatus, l as EntityCapability, m as WorkflowTransitionError, n as InvalidLocaleError, o as FieldDiff, p as WorkflowState, r as BlockDiffEntry, s as PropDiff, t as EntityCapabilityError, u as entitySupports, v as getVersionableOptions, y as
|
|
3
|
-
export { type BlockDefinition, type BlockDiffEntry, type BlockDiffStatus, type ContentConfig, type DiffSegment, type DraftEntry, type EntityCapability, EntityCapabilityError, type FieldDiff, type InferBlockData, type InferBlocksUnion, InvalidLocaleError, type LocaleConfig, type LockInfo, type PropDiff, type TranslationFieldStatus, type TranslationReport, type TranslationStatus, type VersionEntry, type VersionableOptions, WORKFLOW_STATES, WORKFLOW_TRANSITIONS, type WorkflowState, WorkflowTransitionError, type WorkflowableFields, defineBlock, entitySupports, getVersionableOptions, isAllowedTransition, versionable, workflowable };
|
|
1
|
+
import { a as InferBlocksUnion, c as LockInfo, d as TranslationStatus, f as VersionEntry, i as InferBlockData, l as TranslationFieldStatus, n as ContentConfig, o as LocaleConfig, p as VersionableOptions, r as DraftEntry, t as BlockDefinition, u as TranslationReport } from "./types-CWo2dfEK.mjs";
|
|
2
|
+
import { S as withOverrides, T as versionable, _ as workflowable, a as DiffSegment, b as WithOverridesFields, c as defineBlock, d as WORKFLOW_STATES, f as WORKFLOW_TRANSITIONS, g as isAllowedTransition, h as WorkflowableFields, i as BlockDiffStatus, l as EntityCapability, m as WorkflowTransitionError, n as InvalidLocaleError, o as FieldDiff, p as WorkflowState, r as BlockDiffEntry, s as PropDiff, t as EntityCapabilityError, u as entitySupports, v as OverrideDirective, w as getVersionableOptions, x as hasWithOverrides, y as OverridesMap } from "./index-CB0fdMs1.mjs";
|
|
3
|
+
export { type BlockDefinition, type BlockDiffEntry, type BlockDiffStatus, type ContentConfig, type DiffSegment, type DraftEntry, type EntityCapability, EntityCapabilityError, type FieldDiff, type InferBlockData, type InferBlocksUnion, InvalidLocaleError, type LocaleConfig, type LockInfo, type OverrideDirective, type OverridesMap, type PropDiff, type TranslationFieldStatus, type TranslationReport, type TranslationStatus, type VersionEntry, type VersionableOptions, WORKFLOW_STATES, WORKFLOW_TRANSITIONS, type WithOverridesFields, type WorkflowState, WorkflowTransitionError, type WorkflowableFields, defineBlock, entitySupports, getVersionableOptions, hasWithOverrides, isAllowedTransition, versionable, withOverrides, workflowable };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{n as e,t}from"./versionable-B0bP2Gib.mjs";import{t as
|
|
1
|
+
import{n as e,t}from"./versionable-B0bP2Gib.mjs";import{a as n,i as r,n as i,r as a,t as o}from"./workflowable-aGtQhPRc.mjs";import{t as s}from"./define-block-DpMPEEdA.mjs";import{n as c,t as l}from"./errors-CVBrIU1K.mjs";import{field as u}from"@murumets-ee/entity";const d=new Set([`hide`,`patch`,`replace`]),f=Symbol.for(`@murumets-ee/content:withOverrides`),p=new Set([`__proto__`,`constructor`,`prototype`]);function m(e,t,n=0){if(e!=null&&!(typeof e!=`object`||Array.isArray(e)))for(let r of Object.keys(e)){if(p.has(r))throw Error(`withOverrides: reserved key '${r}' at ${t} is not allowed.`);n<3&&m(e[r],`${t}.${r}`,n+1)}}function h(e){if(e==null)return{};if(typeof e!=`object`||Array.isArray(e))throw Error(`withOverrides: _overrides must be a plain object map.`);let t=e,n=Object.keys(t);if(n.length>100)throw Error(`withOverrides: _overrides has ${n.length} entries; cap is 100.`);let r={};for(let e of n){if(p.has(e))throw Error(`withOverrides: reserved key '${e}' is not allowed.`);let n=t[e];if(typeof n!=`object`||!n||Array.isArray(n))throw Error(`withOverrides: _overrides['${e}'] must be a directive object.`);let i=n.kind;if(typeof i!=`string`||!d.has(i))throw Error(`withOverrides: _overrides['${e}'].kind must be one of [hide, patch, replace]; got ${String(i)}.`);if(i===`patch`){let t=n;g(t.props,`_overrides['${e}'].props`),g(t.traits,`_overrides['${e}'].traits`)}if(i===`replace`){let t=n.block;if(typeof t!=`object`||!t||Array.isArray(t)||typeof t.id!=`string`)throw Error(`withOverrides: _overrides['${e}'] replace payload must include an object 'block' with a string 'id'.`)}m(n,`_overrides['${e}']`),r[e]=n}return r}function g(e,t){if(e!==void 0&&(typeof e!=`object`||!e||Array.isArray(e)))throw Error(`withOverrides: ${t} must be an object.`)}function _(){return{name:`withOverrides`,fields:{_overrides:u.json({default:()=>({}),internal:!0})},hooks:{beforeCreate:async e=>(e._overrides===void 0?e._overrides={}:e._overrides=h(e._overrides),e),beforeUpdate:async(e,t)=>(t._overrides===void 0||(t._overrides=h(t._overrides)),t)},[f]:!0}}function v(e){return e?e.some(e=>!!e?.[f]):!1}function y(e,t){switch(t){case`drafts`:return b(e,`publishable`);case`versions`:return b(e,`versionable`);case`localeStatus`:return b(e,`publishable`)&&x(e);case`translations`:return x(e)}}function b(e,t){return e.behaviors?.some(e=>e.name===t)??!1}function x(e){let t=e.allFields;for(let e of Object.keys(t)){let n=t[e];if(n&&n.translatable===!0)return!0}return!1}export{l as EntityCapabilityError,c as InvalidLocaleError,o as WORKFLOW_STATES,i as WORKFLOW_TRANSITIONS,a as WorkflowTransitionError,s as defineBlock,y as entitySupports,t as getVersionableOptions,v as hasWithOverrides,r as isAllowedTransition,e as versionable,_ as withOverrides,n as workflowable};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/behaviors/workflowable.ts","../src/capabilities.ts"],"sourcesContent":["/**\n * Workflowable behavior — editorial workflow with approval gates.\n *\n * Adds workflow status tracking fields to an entity and enforces the\n * documented transition state machine in `beforeUpdate`. Works alongside\n * `publishable()` — the workflow status tracks the editorial pipeline,\n * while publishable's `status` remains the public-facing state.\n *\n * Workflow state machine:\n *\n * draft ─submit─► in_review ─approve─► approved ─publish─► published\n * │ ▲ │\n * │ └────resubmit─────┐ │\n * └─reject─► rejected ─┘ │\n * │\n * unpublish ◄──────────────┘\n * (back to draft)\n *\n * Internal-write only: every workflow field is declared `internal: true`,\n * so the HTTP PATCH surface strips them from caller input. Workflow\n * transitions are performed by trusted server-side routes via\n * `AdminClient.updateInternal()` after authorizing the action with the\n * permission checker. The `beforeUpdate` hook validates that the\n * proposed transition is in the allowed table.\n *\n * Permission gating lives in the route handler (admin-ui workflow\n * routes), not in this behavior — by the time the hook fires, the\n * route has already authorized via `checkPermission`. The hook is\n * the structural defense: even a bug in the route layer cannot push\n * a workflow row into an illegal state.\n *\n * UI integration is tracked in GitHub issue #124. Until the workflow\n * UX ships, the behavior can still be added to entities — it'll simply\n * seed `_workflowStatus = 'draft'` on create and reject anyone trying\n * to write the field via PATCH.\n */\n\nimport type {\n Behavior,\n BehaviorContext,\n DateField,\n SelectField,\n TextField,\n} from '@murumets-ee/entity'\nimport { field } from '@murumets-ee/entity'\n\nexport const WORKFLOW_STATES = ['draft', 'in_review', 'approved', 'rejected', 'published'] as const\nexport type WorkflowState = (typeof WORKFLOW_STATES)[number]\n\n/**\n * Allowed transitions table. Each key is the *current* workflow status;\n * the value is the set of statuses it can move to.\n *\n * Encoded as a const-asserted record so a typo in the source produces\n * a compile error rather than a silent no-match at runtime.\n */\nexport const WORKFLOW_TRANSITIONS = {\n draft: ['in_review'],\n in_review: ['approved', 'rejected'],\n approved: ['published', 'in_review'],\n rejected: ['in_review', 'draft'],\n published: ['draft'],\n} as const satisfies Record<WorkflowState, readonly WorkflowState[]>\n\n/**\n * Is `to` an allowed transition from `from`? Pure, no I/O.\n */\nexport function isAllowedTransition(from: WorkflowState, to: WorkflowState): boolean {\n if (from === to) return true // no-op self-transition is fine\n const allowed = WORKFLOW_TRANSITIONS[from] as readonly WorkflowState[] | undefined\n return allowed?.includes(to) ?? false\n}\n\n/**\n * Thrown when `beforeUpdate` rejects a workflow transition. Distinct\n * subclass so admin-ui's API handler can map it to HTTP 400.\n */\nexport class WorkflowTransitionError extends Error {\n readonly from: WorkflowState\n readonly to: WorkflowState\n\n constructor(from: WorkflowState, to: WorkflowState) {\n super(\n `Invalid workflow transition: ${from} → ${to}. ` +\n `Allowed transitions from \"${from}\": [${(WORKFLOW_TRANSITIONS[from] ?? []).join(', ')}].`,\n )\n this.name = 'WorkflowTransitionError'\n this.from = from\n this.to = to\n }\n}\n\nexport type WorkflowableFields = {\n _workflowStatus: SelectField & { required: true }\n _workflowAssignee: TextField\n _workflowNote: TextField\n _workflowUpdatedAt: DateField\n}\n\nexport function workflowable(): Behavior<WorkflowableFields> {\n return {\n name: 'workflowable',\n fields: {\n _workflowStatus: field.select({\n options: WORKFLOW_STATES as unknown as string[],\n default: 'draft',\n indexed: true,\n required: true,\n internal: true,\n }) as SelectField & { required: true },\n _workflowAssignee: field.text({ required: false, internal: true }),\n _workflowNote: field.text({ required: false, internal: true }),\n _workflowUpdatedAt: field.date({ internal: true }),\n },\n hooks: {\n beforeCreate: async (data) => {\n data._workflowStatus = 'draft'\n data._workflowUpdatedAt = new Date()\n return data\n },\n beforeUpdate: async (\n _id: string,\n data: Record<string, unknown>,\n ctx?: BehaviorContext,\n ): Promise<Record<string, unknown>> => {\n // Only validate when the patch tries to change the workflow status.\n // Updates that don't touch _workflowStatus pass through (the field's\n // `internal: true` already prevents it from sneaking in via PATCH;\n // only updateInternal callers reach this branch with a value).\n const proposed = data._workflowStatus\n if (proposed === undefined) return data\n\n if (!WORKFLOW_STATES.includes(proposed as WorkflowState)) {\n throw new WorkflowTransitionError('draft', proposed as WorkflowState)\n }\n\n // Need the current row to validate the from→to transition. The\n // lazy loader is provided by AdminClient — present on update,\n // absent on create.\n const current = await ctx?.loadCurrent?.()\n const fromState = (current?._workflowStatus as WorkflowState | undefined) ?? 'draft'\n const toState = proposed as WorkflowState\n\n if (!isAllowedTransition(fromState, toState)) {\n throw new WorkflowTransitionError(fromState, toState)\n }\n\n data._workflowUpdatedAt = new Date()\n return data\n },\n },\n }\n}\n","/**\n * Per-entity content capability gates.\n *\n * The content plugin attaches different infrastructure tables to an\n * entity based on which behaviors it has:\n * - `publishable()` → drafts table\n * - `versionable()` → versions table\n * - `publishable() + translatable fields` → locale_status table\n * - any translatable field → translations table (also written by the\n * entity package; included for symmetric capability checks)\n *\n * Calling a capability-specific ContentClient method (`saveDraft`,\n * `publishDraft`, `setLocaleStatus`, `restoreVersion`, ...) on an\n * entity that doesn't have the required behavior is a programming\n * error. ContentClient throws `EntityCapabilityError` on writes and\n * silently returns falsy on reads. Callers that want to gate ahead —\n * \"should I show the draft button on this entity row?\" — use\n * `entitySupports()` instead of catching the error.\n */\n\nimport type { Entity, FieldConfig } from '@murumets-ee/entity'\n\nexport type EntityCapability = 'drafts' | 'versions' | 'localeStatus' | 'translations'\n\n/**\n * Does this entity support the given content capability? Pure check\n * against the entity's declared behaviors and field translatability —\n * no plugin lookup, no DB call.\n */\nexport function entitySupports(entity: Entity, capability: EntityCapability): boolean {\n switch (capability) {\n case 'drafts':\n return hasBehavior(entity, 'publishable')\n case 'versions':\n return hasBehavior(entity, 'versionable')\n case 'localeStatus':\n return hasBehavior(entity, 'publishable') && hasTranslatableField(entity)\n case 'translations':\n return hasTranslatableField(entity)\n }\n}\n\nfunction hasBehavior(entity: Entity, name: string): boolean {\n return entity.behaviors?.some((b) => b.name === name) ?? false\n}\n\nfunction hasTranslatableField(entity: Entity): boolean {\n const fields = entity.allFields as Record<string, FieldConfig>\n for (const key of Object.keys(fields)) {\n const f = fields[key]\n if (f && (f as { translatable?: boolean }).translatable === true) return true\n }\n return false\n}\n"],"mappings":"8LA8CA,MAAa,EAAkB,CAAC,QAAS,YAAa,WAAY,WAAY,YAAY,CAU7E,EAAuB,CAClC,MAAO,CAAC,YAAY,CACpB,UAAW,CAAC,WAAY,WAAW,CACnC,SAAU,CAAC,YAAa,YAAY,CACpC,SAAU,CAAC,YAAa,QAAQ,CAChC,UAAW,CAAC,QAAQ,CACrB,CAKD,SAAgB,EAAoB,EAAqB,EAA4B,CAGnF,OAFI,IAAS,EAAW,GACR,EAAqB,IACrB,SAAS,EAAG,EAAI,GAOlC,IAAa,EAAb,cAA6C,KAAM,CACjD,KACA,GAEA,YAAY,EAAqB,EAAmB,CAClD,MACE,gCAAgC,EAAK,KAAK,EAAG,8BACd,EAAK,OAAO,EAAqB,IAAS,EAAE,EAAE,KAAK,KAAK,CAAC,IACzF,CACD,KAAK,KAAO,0BACZ,KAAK,KAAO,EACZ,KAAK,GAAK,IAWd,SAAgB,GAA6C,CAC3D,MAAO,CACL,KAAM,eACN,OAAQ,CACN,gBAAiB,EAAM,OAAO,CAC5B,QAAS,EACT,QAAS,QACT,QAAS,GACT,SAAU,GACV,SAAU,GACX,CAAC,CACF,kBAAmB,EAAM,KAAK,CAAE,SAAU,GAAO,SAAU,GAAM,CAAC,CAClE,cAAe,EAAM,KAAK,CAAE,SAAU,GAAO,SAAU,GAAM,CAAC,CAC9D,mBAAoB,EAAM,KAAK,CAAE,SAAU,GAAM,CAAC,CACnD,CACD,MAAO,CACL,aAAc,KAAO,KACnB,EAAK,gBAAkB,QACvB,EAAK,mBAAqB,IAAI,KACvB,GAET,aAAc,MACZ,EACA,EACA,IACqC,CAKrC,IAAM,EAAW,EAAK,gBACtB,GAAI,IAAa,IAAA,GAAW,OAAO,EAEnC,GAAI,CAAC,EAAgB,SAAS,EAA0B,CACtD,MAAM,IAAI,EAAwB,QAAS,EAA0B,CAOvE,IAAM,GAAa,MADG,GAAK,eAAe,GACd,iBAAiD,QACvE,EAAU,EAEhB,GAAI,CAAC,EAAoB,EAAW,EAAQ,CAC1C,MAAM,IAAI,EAAwB,EAAW,EAAQ,CAIvD,MADA,GAAK,mBAAqB,IAAI,KACvB,GAEV,CACF,CC1HH,SAAgB,EAAe,EAAgB,EAAuC,CACpF,OAAQ,EAAR,CACE,IAAK,SACH,OAAO,EAAY,EAAQ,cAAc,CAC3C,IAAK,WACH,OAAO,EAAY,EAAQ,cAAc,CAC3C,IAAK,eACH,OAAO,EAAY,EAAQ,cAAc,EAAI,EAAqB,EAAO,CAC3E,IAAK,eACH,OAAO,EAAqB,EAAO,EAIzC,SAAS,EAAY,EAAgB,EAAuB,CAC1D,OAAO,EAAO,WAAW,KAAM,GAAM,EAAE,OAAS,EAAK,EAAI,GAG3D,SAAS,EAAqB,EAAyB,CACrD,IAAM,EAAS,EAAO,UACtB,IAAK,IAAM,KAAO,OAAO,KAAK,EAAO,CAAE,CACrC,IAAM,EAAI,EAAO,GACjB,GAAI,GAAM,EAAiC,eAAiB,GAAM,MAAO,GAE3E,MAAO"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/behaviors/with-overrides.ts","../src/capabilities.ts"],"sourcesContent":["/**\n * `withOverrides()` — opt-in behavior wiring a document entity into the\n * page-scoped override resolver shipped by phase 05.\n *\n * Adds a single internal field `_overrides: Record<string, OverrideDirective>`\n * to the host entity, defaulted to an empty object. The composition step\n * (`composeMultiScopeDocument` in `@murumets-ee/blocks/server`) reads this\n * field and calls `applyOverrides()` AFTER the multi-scope tree has been\n * assembled.\n *\n * Apps opt-in:\n *\n * @example\n * ```ts\n * defineEntity({\n * name: 'page',\n * fields: { ... },\n * behaviors: [\n * publishable(),\n * versionable({ maxPerDoc: 30 }),\n * workflowable(),\n * withOpLog({ retentionDays: 7 }),\n * withOverrides(), // ← enables page-level overrides on scoped blocks\n * ],\n * })\n * ```\n *\n * Why a behavior (not a plain field):\n * - Discoverability — `entitySupports(entity, 'overrides')` lets the boot\n * orphan-sweep find every overrides-eligible entity without hard-coding\n * a list.\n * - Validation — the behavior's `beforeUpdate` hook validates the shape\n * of the `_overrides` map (Zod-equivalent runtime check) so a malformed\n * PATCH body can't poison the field.\n * - Defensive defaults — `_overrides: {}` is auto-seeded on `beforeCreate`\n * so consumers always see a value, never `undefined`.\n *\n * `internal: true` on the field keeps it out of HTTP PATCH input — the\n * `doc:scope-override:set` op is the only path that can mutate it (writes\n * are routed through `AdminClient.updateInternal()` after permission\n * gating).\n *\n * See `plan/blocks/05-scoping-overrides.md` §5 + `progress/findings/F009`.\n */\n\nimport type { Behavior, JsonField } from '@murumets-ee/entity'\nimport { field } from '@murumets-ee/entity'\n\n/**\n * Override-directive shape. Mirrored from `@murumets-ee/blocks` to avoid\n * a circular dep (content → blocks → content); content stores the\n * persisted JSON, blocks owns the resolver. Keep the two shapes in sync.\n */\ntype OverrideDirective =\n | { kind: 'hide' }\n | {\n kind: 'patch'\n props?: Record<string, unknown>\n traits?: Record<string, Record<string, unknown>>\n }\n | { kind: 'replace'; block: Record<string, unknown> }\n\ntype OverridesMap = Record<string, OverrideDirective>\n\nexport type WithOverridesFields = {\n // `required: false` at the type level — the `beforeCreate` hook\n // seeds the field with `{}` when callers omit it, so making the\n // INPUT optional is more ergonomic than threading `_overrides: {}`\n // through every consumer's create-input.\n //\n // NOTE: the DB column is NULLABLE in the generated migration\n // (`_overrides jsonb`) — the JsonField `default` is a runtime\n // function the entity layer calls on create, NOT a SQL DEFAULT\n // clause the migration generator sees. Readers MUST treat NULL\n // as equivalent to `{}` (see `composeMultiScopeDocument`'s\n // `readOverridesMap` for the canonical pattern). A row inserted\n // via raw SQL or migration that bypasses the entity layer can\n // leave NULL; that's safe.\n _overrides: JsonField\n}\n\n/**\n * Allowed directive kinds — closed set. Unknown discriminators are rejected\n * at the behavior boundary so a malformed PATCH (or future plugin)\n * can't write opaque shapes that the resolver silently ignores.\n */\nconst ALLOWED_KINDS: ReadonlySet<string> = new Set(['hide', 'patch', 'replace'])\n\n/**\n * Maximum number of override entries per document. Bounded to keep the\n * page row's JSON column compact + the rendering hot path predictable.\n * Per phase 05 §9: pages overriding more than ~50 scoped blocks is a\n * smell (consider a `route_layout` instead).\n */\nconst MAX_OVERRIDES_PER_DOC = 100\n\n/** Marker symbol so introspection can pick out the behavior reliably. */\nconst WITH_OVERRIDES_MARKER = Symbol.for('@murumets-ee/content:withOverrides')\n\ninterface WithOverridesMarker {\n [WITH_OVERRIDES_MARKER]: true\n}\n\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype'])\n\n/**\n * Walk a plain-object value's OWN keys and reject any that match a\n * prototype-pollution vector. JSON.parse can create own properties\n * with these names; the dispatcher already filters at the route +\n * op-handler boundary, but the behavior is the third layer of\n * defense (any persistence path that bypasses the dispatcher — DB\n * seed, raw SQL migration, future internal API — lands here on the\n * next update).\n *\n * The depth cap of 3 covers every documented over-the-wire shape:\n *\n * - patch.props.__proto__ → depth 1\n * - patch.traits.spacing.__proto__ → depth 2\n * - replace.block.props.__proto__ → depth 2\n * - replace.block.traits.spacing.__proto__ → depth 3\n *\n * (Counting from the directive root at depth 0. The recursion CHECKS\n * keys at the entered depth, then recurses while `depth < 3`, so the\n * deepest check happens at depth=3.)\n *\n * Beyond that depth the structure is field VALUES which are caller-\n * payload (block field type / trait field type) — those are the\n * field-type validator's job, not the override-map gate.\n *\n * `path` is appended to the thrown error for triage.\n */\nfunction assertNoForbiddenKeys(value: unknown, path: string, depth = 0): void {\n if (value === null || value === undefined) return\n if (typeof value !== 'object' || Array.isArray(value)) return\n for (const key of Object.keys(value as Record<string, unknown>)) {\n if (FORBIDDEN_KEYS.has(key)) {\n throw new Error(`withOverrides: reserved key '${key}' at ${path} is not allowed.`)\n }\n if (depth < 3) {\n assertNoForbiddenKeys((value as Record<string, unknown>)[key], `${path}.${key}`, depth + 1)\n }\n }\n}\n\n/**\n * Validate an override-map payload. Throws when malformed; returns the\n * (defensively-cloned, sanitised) value when ok. Called from the\n * `beforeUpdate` hook — `internal: true` blocks PATCH-side input but\n * the `doc:scope-override:set` op writes via `updateInternal` and so\n * still flows through here.\n *\n * Defense layered: outer scope-block-id keys + each directive's\n * recursive nested keys (patch.props / patch.traits / replace.block.props\n * / replace.block.traits) all walk through `assertNoForbiddenKeys`. The\n * dispatcher's own handler validates the same set; this is the\n * defense-in-depth replay at the persistence boundary.\n */\nfunction validateOverridesMap(value: unknown): OverridesMap {\n if (value === null || value === undefined) return {}\n if (typeof value !== 'object' || Array.isArray(value)) {\n throw new Error('withOverrides: _overrides must be a plain object map.')\n }\n const map = value as Record<string, unknown>\n const keys = Object.keys(map)\n if (keys.length > MAX_OVERRIDES_PER_DOC) {\n throw new Error(\n `withOverrides: _overrides has ${keys.length} entries; cap is ${MAX_OVERRIDES_PER_DOC}.`,\n )\n }\n const sanitised: OverridesMap = {}\n for (const key of keys) {\n // Skip prototype-pollution vectors at the OUTER (scopeBlockId)\n // key boundary first.\n if (FORBIDDEN_KEYS.has(key)) {\n throw new Error(`withOverrides: reserved key '${key}' is not allowed.`)\n }\n const directive = map[key]\n if (\n directive === null ||\n directive === undefined ||\n typeof directive !== 'object' ||\n Array.isArray(directive)\n ) {\n throw new Error(`withOverrides: _overrides['${key}'] must be a directive object.`)\n }\n const kind = (directive as { kind?: unknown }).kind\n if (typeof kind !== 'string' || !ALLOWED_KINDS.has(kind)) {\n throw new Error(\n `withOverrides: _overrides['${key}'].kind must be one of [hide, patch, replace]; got ${String(\n kind,\n )}.`,\n )\n }\n // Kind-specific shape enforcement (CR bot major) — `kind` alone\n // isn't enough; a malformed `{ kind: 'replace' }` (no block) or\n // `{ kind: 'patch', props: 123 }` would otherwise pass the gate\n // and land invalid data in persistence. Mirror the dispatcher\n // handler's shape checks at this third defense layer.\n if (kind === 'patch') {\n const d = directive as { props?: unknown; traits?: unknown }\n assertPlainObjectOrUndefined(d.props, `_overrides['${key}'].props`)\n assertPlainObjectOrUndefined(d.traits, `_overrides['${key}'].traits`)\n }\n if (kind === 'replace') {\n const block = (directive as { block?: unknown }).block\n if (\n block === null ||\n block === undefined ||\n typeof block !== 'object' ||\n Array.isArray(block) ||\n typeof (block as { id?: unknown }).id !== 'string'\n ) {\n throw new Error(\n `withOverrides: _overrides['${key}'] replace payload must include an object 'block' with a string 'id'.`,\n )\n }\n }\n // Recurse into the directive payload — patch.props / patch.traits\n // / replace.block.props / replace.block.traits — and reject any\n // forbidden own-property keys at every level.\n assertNoForbiddenKeys(directive, `_overrides['${key}']`)\n sanitised[key] = directive as OverrideDirective\n }\n return sanitised\n}\n\n/**\n * Reject a directive sub-field that's present but not a plain object\n * (array / primitive / null). `undefined` is allowed — the field is\n * optional. Mirrors the dispatcher handler's `assertPlainObjectOrUndefined`.\n */\nfunction assertPlainObjectOrUndefined(value: unknown, label: string): void {\n if (value === undefined) return\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n throw new Error(`withOverrides: ${label} must be an object.`)\n }\n}\n\n/**\n * Mark an entity as participating in the page-scoped override resolver.\n * See module-level JSDoc for the full contract.\n */\nexport function withOverrides(): Behavior<WithOverridesFields, 'withOverrides'> {\n const behavior: Behavior<WithOverridesFields, 'withOverrides'> & WithOverridesMarker = {\n name: 'withOverrides',\n fields: {\n // `internal: true` blocks PATCH-input poisoning (the\n // `doc:scope-override:set` op writes via updateInternal which\n // bypasses the input filter). `default: () => ({})` seeds the\n // value on every create call, so callers can omit `_overrides`\n // entirely (no `required: true` needed at the input level).\n _overrides: field.json({\n default: () => ({}),\n internal: true,\n }),\n },\n hooks: {\n beforeCreate: async (data) => {\n // Seed the empty map on create so the column is never null in DB.\n // Caller-supplied `_overrides` on create is unusual (overrides\n // are typically dispatched op-by-op) but if they DO pass one,\n // validate it.\n if (data._overrides === undefined) {\n data._overrides = {}\n } else {\n data._overrides = validateOverridesMap(data._overrides)\n }\n return data\n },\n beforeUpdate: async (_id, data) => {\n // Only validate when the patch tries to write `_overrides`.\n if (data._overrides === undefined) return data\n data._overrides = validateOverridesMap(data._overrides)\n return data\n },\n },\n [WITH_OVERRIDES_MARKER]: true,\n }\n return behavior\n}\n\n/**\n * Check whether an entity declares `withOverrides()`. Returns `true`\n * when the entity's behaviors list contains a behavior tagged with\n * the marker symbol. Used by the boot orphan-sweep + admin UI gating.\n */\nexport function hasWithOverrides(\n behaviors: ReadonlyArray<{ name: string } & Record<symbol, unknown>> | undefined,\n): boolean {\n if (!behaviors) return false\n return behaviors.some((b) => Boolean(b?.[WITH_OVERRIDES_MARKER]))\n}\n\n/**\n * Export the directive type alias for downstream consumers that want\n * to type-narrow without depending on `@murumets-ee/blocks`. The\n * resolver lives in blocks; the persisted shape lives here.\n */\nexport type { OverrideDirective, OverridesMap }\n","/**\n * Per-entity content capability gates.\n *\n * The content plugin attaches different infrastructure tables to an\n * entity based on which behaviors it has:\n * - `publishable()` → drafts table\n * - `versionable()` → versions table\n * - `publishable() + translatable fields` → locale_status table\n * - any translatable field → translations table (also written by the\n * entity package; included for symmetric capability checks)\n *\n * Calling a capability-specific ContentClient method (`saveDraft`,\n * `publishDraft`, `setLocaleStatus`, `restoreVersion`, ...) on an\n * entity that doesn't have the required behavior is a programming\n * error. ContentClient throws `EntityCapabilityError` on writes and\n * silently returns falsy on reads. Callers that want to gate ahead —\n * \"should I show the draft button on this entity row?\" — use\n * `entitySupports()` instead of catching the error.\n */\n\nimport type { Entity, FieldConfig } from '@murumets-ee/entity'\n\nexport type EntityCapability = 'drafts' | 'versions' | 'localeStatus' | 'translations'\n\n/**\n * Does this entity support the given content capability? Pure check\n * against the entity's declared behaviors and field translatability —\n * no plugin lookup, no DB call.\n */\nexport function entitySupports(entity: Entity, capability: EntityCapability): boolean {\n switch (capability) {\n case 'drafts':\n return hasBehavior(entity, 'publishable')\n case 'versions':\n return hasBehavior(entity, 'versionable')\n case 'localeStatus':\n return hasBehavior(entity, 'publishable') && hasTranslatableField(entity)\n case 'translations':\n return hasTranslatableField(entity)\n }\n}\n\nfunction hasBehavior(entity: Entity, name: string): boolean {\n return entity.behaviors?.some((b) => b.name === name) ?? false\n}\n\nfunction hasTranslatableField(entity: Entity): boolean {\n const fields = entity.allFields as Record<string, FieldConfig>\n for (const key of Object.keys(fields)) {\n const f = fields[key]\n if (f && (f as { translatable?: boolean }).translatable === true) return true\n }\n return false\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{n as e}from"./errors-CVBrIU1K.mjs";import{n as t}from"./plugin-DbK6FxTd.mjs";function n(n){if(n===`_`)return;let r=t();for(let e of r.locales)if(e.code===n)return;throw new e(n,r.locales.map(e=>e.code))}export{n as t};
|
|
2
|
+
//# sourceMappingURL=locale-utils-DIcmuLIr.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locale-utils-
|
|
1
|
+
{"version":3,"file":"locale-utils-DIcmuLIr.mjs","names":[],"sources":["../src/locale-utils.ts"],"sourcesContent":["/**\n * Locale validation helpers.\n *\n * Every ContentClient and LockService method that accepts a `locale`\n * parameter must run it through `validateLocale()` before passing it to\n * the database. Drizzle parameterizes queries so SQL injection isn't a\n * concern, but unvalidated locale strings:\n * - bypass the configured locale set (typo'd `'frr'` writes a row that\n * never resolves on read),\n * - silently truncate at the column's `varchar(10)` boundary (e.g.\n * `'pt-BR-traditional'` → `'pt-BR-trad'`, breaking later equality),\n * - pollute drafts/versions/locale_status tables with rows that no\n * UI ever queries.\n *\n * The `'_'` sentinel is allowed because it's the conventional value for\n * default-locale rows in versions/drafts (where every row needs a\n * non-null `locale` for unique-constraint composition).\n */\n\nimport { InvalidLocaleError } from './errors.js'\nimport { getContentConfig } from './plugin.js'\n\n/** The internal sentinel for \"default locale\" used in version/draft storage. */\nexport const DEFAULT_LOCALE_SENTINEL = '_'\n\n/**\n * Throw `InvalidLocaleError` if `locale` is neither the `'_'` sentinel nor\n * one of the configured plugin locale codes.\n */\nexport function validateLocale(locale: string): void {\n if (locale === DEFAULT_LOCALE_SENTINEL) return\n const config = getContentConfig()\n for (const l of config.locales) {\n if (l.code === locale) return\n }\n throw new InvalidLocaleError(\n locale,\n config.locales.map((l) => l.code),\n )\n}\n"],"mappings":"oFA6BA,SAAgB,EAAe,EAAsB,CACnD,GAAI,IAAA,IAAoC,OACxC,IAAM,EAAS,GAAkB,CACjC,IAAK,IAAM,KAAK,EAAO,QACrB,GAAI,EAAE,OAAS,EAAQ,OAEzB,MAAM,IAAI,EACR,EACA,EAAO,QAAQ,IAAK,GAAM,EAAE,KAAK,CAClC"}
|
package/dist/lock-service.d.mts
CHANGED
package/dist/lock-service.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./locks-
|
|
1
|
+
import{t as e}from"./locks-BCHUb-yC.mjs";import"server-only";var t=class{client;constructor(t){this.client=e.makeClient(t.app.db.readWrite)}async acquireLock(e,t,n,r){let i=new Date,a=new Date(i.getTime()+300*1e3),o=await this.client.tryClaim({entityType:e,entityId:t,locale:n,lockedBy:r.id,lockedByName:r.name??null,lockedAt:i,expiresAt:a},{target:[`entityType`,`entityId`,`locale`],replaceWhen:{$or:[{expiresAt:{lt:i}},{lockedBy:r.id}]}});return o.acquired?{acquired:!0}:{acquired:!1,lock:{lockedBy:o.row.lockedBy,lockedByName:o.row.lockedByName??null,lockedAt:o.row.lockedAt,expiresAt:o.row.expiresAt}}}async renewLock(e,t,n,r){let i=new Date,a=new Date(i.getTime()+300*1e3);return await this.client.update({entityType:e,entityId:t,locale:n,lockedBy:r,expiresAt:{gte:i}},{expiresAt:a})!==null}async releaseLock(e,t,n,r){return await this.client.delete({entityType:e,entityId:t,locale:n,lockedBy:r})!==null}async forceReleaseLock(e,t,n){return await this.client.delete({entityType:e,entityId:t,locale:n})!==null}async getLock(e,t,n){let r=new Date,i=await this.client.findOne({entityType:e,entityId:t,locale:n,expiresAt:{gte:r}});return i?{lockedBy:i.lockedBy,lockedByName:i.lockedByName??null,lockedAt:i.lockedAt,expiresAt:i.expiresAt}:null}};export{t as LockService};
|
|
2
2
|
//# sourceMappingURL=lock-service.mjs.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import{column as e,defineTable as t}from"@murumets-ee/db";const n=t({name:`toolkit_content_locks`,columns:{id:e.uuid({primaryKey:!0,defaultRandom:!0}),entityType:e.varchar({length:100,notNull:!0,pgName:`entity_type`}),entityId:e.varchar({length:255,notNull:!0,pgName:`entity_id`}),locale:e.varchar({length:10,notNull:!0,default:`_`}),lockedBy:e.varchar({length:255,notNull:!0,pgName:`locked_by`}),lockedByName:e.varchar({length:255,pgName:`locked_by_name`}),lockedAt:e.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`locked_at`}),expiresAt:e.timestamp({notNull:!0,withTimezone:!0,pgName:`expires_at`})},unique:[{on:[`entityType`,`entityId`,`locale`]}]}),r=n.table;export{r as n,n as t};
|
|
2
|
-
//# sourceMappingURL=locks-
|
|
2
|
+
//# sourceMappingURL=locks-BCHUb-yC.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locks-
|
|
1
|
+
{"version":3,"file":"locks-BCHUb-yC.mjs","names":[],"sources":["../src/schema/locks.ts"],"sourcesContent":["/**\n * Content lock table schema\n *\n * Single shared table (not per-entity) for pessimistic content locking.\n * Prevents concurrent edit conflicts by allowing only one editor at a time\n * per entity+locale combination.\n *\n * Locks expire after 5 minutes and must be renewed via heartbeat.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\nexport const contentLocksTable = defineTable({\n name: 'toolkit_content_locks',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityType: column.varchar({ length: 100, notNull: true, pgName: 'entity_type' }),\n entityId: column.varchar({ length: 255, notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n lockedBy: column.varchar({ length: 255, notNull: true, pgName: 'locked_by' }),\n lockedByName: column.varchar({ length: 255, pgName: 'locked_by_name' }),\n lockedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'locked_at',\n }),\n expiresAt: column.timestamp({\n notNull: true,\n withTimezone: true,\n pgName: 'expires_at',\n }),\n },\n unique: [{ on: ['entityType', 'entityId', 'locale'] }],\n})\n\n/** Backwards-compatible export — existing imports of the raw pgTable still work. */\nexport const toolkitContentLocks = contentLocksTable.table\n"],"mappings":"0DAYA,MAAa,EAAoB,EAAY,CAC3C,KAAM,wBACN,QAAS,CACP,GAAI,EAAO,KAAK,CAAE,WAAY,GAAM,cAAe,GAAM,CAAC,CAC1D,WAAY,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,OAAQ,cAAe,CAAC,CACjF,SAAU,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,OAAQ,YAAa,CAAC,CAC7E,OAAQ,EAAO,QAAQ,CAAE,OAAQ,GAAI,QAAS,GAAM,QAAS,IAAK,CAAC,CACnE,SAAU,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,OAAQ,YAAa,CAAC,CAC7E,aAAc,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,iBAAkB,CAAC,CACvE,SAAU,EAAO,UAAU,CACzB,QAAS,GACT,WAAY,GACZ,aAAc,GACd,OAAQ,YACT,CAAC,CACF,UAAW,EAAO,UAAU,CAC1B,QAAS,GACT,aAAc,GACd,OAAQ,aACT,CAAC,CACH,CACD,OAAQ,CAAC,CAAE,GAAI,CAAC,aAAc,WAAY,SAAS,CAAE,CAAC,CACvD,CAAC,CAGW,EAAsB,EAAkB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{n as e}from"./locks-
|
|
2
|
-
//# sourceMappingURL=plugin-
|
|
1
|
+
import{n as e}from"./locks-BCHUb-yC.mjs";import{generateLayoutSchema as t,generateLayoutTranslationSchema as n}from"@murumets-ee/entity";import{definePlugin as r}from"@murumets-ee/core";import{column as i,defineTable as a,schemaRegistry as o}from"@murumets-ee/db";const s=new Map;function c(e,t){let n=s.get(e)??{};s.set(e,{...n,...t})}function l(e){return s.get(e)??{}}function u(e){return a({name:`${e.name}_drafts`,columns:{id:i.uuid({primaryKey:!0,defaultRandom:!0}),entityId:i.uuid({notNull:!0,pgName:`entity_id`}),locale:i.varchar({length:10,notNull:!0,default:`_`}),data:i.jsonb({notNull:!0}),createdBy:i.varchar({length:255,notNull:!0,pgName:`created_by`}),createdByName:i.varchar({length:255,pgName:`created_by_name`}),createdAt:i.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),updatedAt:i.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityId`,`locale`]}]})}function d(e){return a({name:`${e.name}_locale_status`,columns:{id:i.uuid({primaryKey:!0,defaultRandom:!0}),entityId:i.uuid({notNull:!0,pgName:`entity_id`}),locale:i.varchar({length:10,notNull:!0}),status:i.varchar({length:20,notNull:!0,default:`draft`}),publishedAt:i.timestamp({withTimezone:!0,pgName:`published_at`})},unique:[{on:[`entityId`,`locale`]}]})}function f(e){return a({name:`${e.name}_versions`,columns:{id:i.uuid({primaryKey:!0,defaultRandom:!0}),entityId:i.uuid({notNull:!0,pgName:`entity_id`}),version:i.integer({notNull:!0}),locale:i.varchar({length:10,notNull:!0,default:`_`}),data:i.jsonb({notNull:!0}),delta:i.jsonb(),status:i.varchar({length:20}),createdBy:i.varchar({length:255,pgName:`created_by`}),createdByName:i.varchar({length:255,pgName:`created_by_name`}),createdAt:i.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),isAutosave:i.boolean({notNull:!0,default:!1,pgName:`is_autosave`})},unique:[{on:[`entityId`,`version`,`locale`]}]})}let p=null;function m(){if(!p)throw Error(`@murumets-ee/content plugin not initialized. Add content() to your plugins array.`);return p}function h(i){if(!i.locales||i.locales.length===0)throw Error(`content() requires at least one locale`);if(!i.defaultLocale)throw Error(`content() requires a defaultLocale`);if(!i.locales.some(e=>e.code===i.defaultLocale))throw Error(`defaultLocale "${i.defaultLocale}" not found in locales: [${i.locales.map(e=>e.code).join(`, `)}]`);return r({name:`@murumets-ee/content`,server:{tables:{toolkitContentLocks:e},init:async e=>{p=i;let r=0,a=0,s=0,l=0;for(let[i,p]of e.entities){if(p.behaviors?.some(e=>e.name===`versionable`)){let e=`${i}_versions`;if(!o.has(e)){let t=f(p);o.register(e,t.table),c(i,{versions:t})}r++}let e=p.allFields;if(Object.values(e).some(e=>e.type===`blocks`)){let e=`${i}_layout`;if(!o.has(e)){let n=t(p);n&&o.register(e,n)}let r=`${i}_layout_translations`;if(!o.has(r)){let e=n(p);e&&o.register(r,e)}a++}let m=p.behaviors?.some(e=>e.name===`publishable`),h=Object.values(e).some(e=>e.translatable);if(m&&h){let e=`${i}_locale_status`;if(!o.has(e)){let t=d(p);o.register(e,t.table),c(i,{localeStatus:t})}s++}if(m){let e=`${i}_drafts`;if(!o.has(e)){let t=u(p);o.register(e,t.table),c(i,{drafts:t})}l++}}e.logger.info({locales:i.locales.map(e=>e.code),defaultLocale:i.defaultLocale,versionableEntities:r,blockEntities:a,localeStatusEntities:s,draftEntities:l},`Content plugin initialized`)}}})}export{m as n,l as r,h as t};
|
|
2
|
+
//# sourceMappingURL=plugin-DbK6FxTd.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-DkmXJ7Qw.mjs","names":[],"sources":["../src/schema/registry.ts","../src/schema/drafts.ts","../src/schema/locale-status.ts","../src/schema/versions.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Per-entity content table registry.\n *\n * Maps an entity name to its content infrastructure tables — drafts,\n * versions, locale_status. Each entry stores the `DefinedTable` (from\n * `defineTable`), which exposes a typed `.makeClient(db)` for CRUD via\n * `TableClient` and a raw `.table` for legacy callers.\n *\n * The content plugin populates this registry at `init` time; ContentClient\n * reads from it lazily on first use. Tables that don't apply to an entity\n * (non-publishable entities have no drafts, etc.) are simply absent.\n */\n\nimport type { DraftsTable } from './drafts.js'\nimport type { LocaleStatusTable } from './locale-status.js'\nimport type { VersionsTable } from './versions.js'\n\n/**\n * The full set of optional content infra tables attached to an entity.\n * Every field is optional — entities without the relevant behavior don't\n * get the corresponding table.\n */\nexport interface ContentTables {\n /** `{entity}_drafts` — present when the entity has `publishable()`. */\n drafts?: DraftsTable\n /** `{entity}_versions` — present when the entity has `versionable()`. */\n versions?: VersionsTable\n /** `{entity}_locale_status` — present when the entity is publishable + has translatable fields. */\n localeStatus?: LocaleStatusTable\n}\n\n// Module-level map. Single source of truth for ContentClient lookups.\nconst tables = new Map<string, ContentTables>()\n\n/**\n * Register content tables for an entity. Called from `content().init()`.\n *\n * Subsequent calls for the same entity name MERGE the provided patch into\n * the existing entry. This lets the plugin add tables one at a time (drafts,\n * then versions, then locale_status) without the caller needing to build the\n * whole object up front.\n */\nexport function registerContentTables(entityName: string, patch: ContentTables): void {\n const existing = tables.get(entityName) ?? {}\n tables.set(entityName, { ...existing, ...patch })\n}\n\n/**\n * Get the content tables for an entity. Returns an empty object (rather\n * than undefined) when the entity has no registered tables — that\n * simplifies the common ContentClient lookup idiom:\n *\n * const { drafts } = getContentTables(entity.name)\n * if (!drafts) return null\n */\nexport function getContentTables(entityName: string): ContentTables {\n return tables.get(entityName) ?? {}\n}\n\n/**\n * Clear the registry. **Test-only.** Throws in production.\n */\nexport function clearContentTables(): void {\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'clearContentTables() is not allowed in production — this is a test-only utility.',\n )\n }\n tables.clear()\n}\n","/**\n * Draft overlay table schema generator\n *\n * Generates a {entity}_drafts table for entities with the publishable() behavior.\n * Stores a mutable work-in-progress snapshot that non-publishers edit instead of\n * writing directly to the live published row.\n *\n * One draft per entity+locale. Drafts are upserted, discarded, or promoted to live.\n *\n * Uses `defineTable` so ContentClient can operate on drafts through a typed\n * TableClient (see registry.ts). The migration pipeline uses the\n * entity-package generator (which emits FK references) — this one produces a\n * runtime-only table shape for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a publishable entity's drafts table.\n *\n * Schema:\n * {entity}_drafts (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_',\n * data jsonb NOT NULL, -- full entity snapshot\n * created_by varchar(255) NOT NULL, -- user ID (varchar, NOT uuid!)\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * updated_at timestamptz NOT NULL DEFAULT now(),\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateDraftsTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_drafts`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n createdBy: column.varchar({ length: 255, notNull: true, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type DraftsTable = ReturnType<typeof generateDraftsTable>\n","/**\n * Per-locale publish status table schema generator\n *\n * Generates a {entity}_locale_status table for entities that have both\n * publishable() behavior AND translatable fields. This allows different\n * locales to have independent publish statuses.\n *\n * Fallback rule: no row for a locale = inherit base entity status.\n *\n * Uses `defineTable` so ContentClient can operate on rows through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references); this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a publishable + translatable entity's\n * locale status table.\n *\n * Schema:\n * {entity}_locale_status (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL,\n * status varchar(20) NOT NULL DEFAULT 'draft',\n * published_at timestamptz,\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateLocaleStatusTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_locale_status`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true }),\n status: column.varchar({ length: 20, notNull: true, default: 'draft' }),\n publishedAt: column.timestamp({ withTimezone: true, pgName: 'published_at' }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type LocaleStatusTable = ReturnType<typeof generateLocaleStatusTable>\n","/**\n * Version history table schema generator\n *\n * Generates a {entity}_versions table for entities with the versionable() behavior.\n * Each version stores a full snapshot + delta (changed fields only).\n *\n * Uses `defineTable` so ContentClient can operate on versions through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references) — this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a versionable entity's versions table.\n *\n * Schema:\n * {entity}_versions (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * version integer NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_', -- locale this snapshot belongs to\n * data jsonb NOT NULL, -- full snapshot (locale-merged view)\n * delta jsonb, -- only changed fields\n * status varchar(20), -- 'draft' | 'published' at time of save\n * created_by varchar(255),\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * is_autosave boolean NOT NULL DEFAULT false,\n * UNIQUE(entity_id, version, locale)\n * )\n *\n * Each locale has its own version sequence. The default locale uses '_'\n * as the locale value (distinguishable from actual locale codes).\n */\nexport function generateVersionsTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_versions`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n version: column.integer({ notNull: true }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n delta: column.jsonb<Record<string, unknown>>(),\n status: column.varchar({ length: 20 }),\n createdBy: column.varchar({ length: 255, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n isAutosave: column.boolean({ notNull: true, default: false, pgName: 'is_autosave' }),\n },\n unique: [{ on: ['entityId', 'version', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type VersionsTable = ReturnType<typeof generateVersionsTable>\n","/**\n * Content plugin — registers locale configuration and version schemas.\n *\n * Usage:\n * import { content } from '@murumets-ee/content/plugin'\n *\n * export default defineLumiConfig({\n * plugins: [\n * content({\n * locales: [\n * { code: 'en', label: 'English' },\n * { code: 'de', label: 'German' },\n * ],\n * defaultLocale: 'en',\n * }),\n * ],\n * })\n */\n\nimport { definePlugin } from '@murumets-ee/core'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { generateLayoutSchema, generateLayoutTranslationSchema } from '@murumets-ee/entity'\nimport { generateDraftsTable } from './schema/drafts.js'\nimport { generateLocaleStatusTable } from './schema/locale-status.js'\nimport { toolkitContentLocks } from './schema/locks.js'\nimport { registerContentTables } from './schema/registry.js'\nimport { generateVersionsTable } from './schema/versions.js'\nimport type { ContentConfig } from './types.js'\n\nlet _contentConfig: ContentConfig | null = null\n\n/**\n * Get the content configuration. Throws if plugin not initialized.\n */\nexport function getContentConfig(): ContentConfig {\n if (!_contentConfig) {\n throw new Error(\n '@murumets-ee/content plugin not initialized. Add content() to your plugins array.',\n )\n }\n return _contentConfig\n}\n\n/**\n * Content plugin factory.\n */\nexport function content(config: ContentConfig) {\n // Validate config\n if (!config.locales || config.locales.length === 0) {\n throw new Error('content() requires at least one locale')\n }\n\n if (!config.defaultLocale) {\n throw new Error('content() requires a defaultLocale')\n }\n\n const defaultExists = config.locales.some((l) => l.code === config.defaultLocale)\n if (!defaultExists) {\n throw new Error(\n `defaultLocale \"${config.defaultLocale}\" not found in locales: [${config.locales.map((l) => l.code).join(', ')}]`,\n )\n }\n\n return definePlugin({\n name: '@murumets-ee/content',\n server: {\n init: async (app) => {\n // Store config globally\n _contentConfig = config\n\n let versionableCount = 0\n let blocksCount = 0\n let localeStatusCount = 0\n let draftsCount = 0\n\n // Register shared locks table (once, outside entity loop)\n if (!schemaRegistry.has('toolkit_content_locks')) {\n schemaRegistry.register('toolkit_content_locks', toolkitContentLocks)\n }\n\n for (const [name, entity] of app.entities) {\n // Register version tables for versionable entities\n const hasVersionable = entity.behaviors?.some(\n (b: { name: string }) => b.name === 'versionable',\n )\n if (hasVersionable) {\n const versionsTableName = `${name}_versions`\n if (!schemaRegistry.has(versionsTableName)) {\n const versions = generateVersionsTable(entity)\n schemaRegistry.register(versionsTableName, versions.table)\n registerContentTables(name, { versions })\n }\n versionableCount++\n }\n\n // Register layout tables for entities with blocks fields\n const allFields = entity.allFields as Record<\n string,\n { type: string; translatable?: boolean }\n >\n const hasBlocks = Object.values(allFields).some((f) => f.type === 'blocks')\n if (hasBlocks) {\n const layoutTableName = `${name}_layout`\n if (!schemaRegistry.has(layoutTableName)) {\n const layoutSchema = generateLayoutSchema(entity)\n if (layoutSchema) {\n schemaRegistry.register(layoutTableName, layoutSchema)\n }\n }\n\n const layoutTransTableName = `${name}_layout_translations`\n if (!schemaRegistry.has(layoutTransTableName)) {\n const layoutTransSchema = generateLayoutTranslationSchema(entity)\n if (layoutTransSchema) {\n schemaRegistry.register(layoutTransTableName, layoutTransSchema)\n }\n }\n\n blocksCount++\n }\n\n // Register locale_status tables for publishable + translatable entities\n const isPublishable = entity.behaviors?.some(\n (b: { name: string }) => b.name === 'publishable',\n )\n const hasTranslatable = Object.values(allFields).some((f) => f.translatable)\n if (isPublishable && hasTranslatable) {\n const statusTableName = `${name}_locale_status`\n if (!schemaRegistry.has(statusTableName)) {\n const localeStatus = generateLocaleStatusTable(entity)\n schemaRegistry.register(statusTableName, localeStatus.table)\n registerContentTables(name, { localeStatus })\n }\n localeStatusCount++\n }\n\n // Register drafts table for publishable entities\n if (isPublishable) {\n const draftsTableName = `${name}_drafts`\n if (!schemaRegistry.has(draftsTableName)) {\n const drafts = generateDraftsTable(entity)\n schemaRegistry.register(draftsTableName, drafts.table)\n registerContentTables(name, { drafts })\n }\n draftsCount++\n }\n }\n\n app.logger.info(\n {\n locales: config.locales.map((l) => l.code),\n defaultLocale: config.defaultLocale,\n versionableEntities: versionableCount,\n blockEntities: blocksCount,\n localeStatusEntities: localeStatusCount,\n draftEntities: draftsCount,\n },\n 'Content plugin initialized',\n )\n },\n },\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin-DbK6FxTd.mjs","names":[],"sources":["../src/schema/registry.ts","../src/schema/drafts.ts","../src/schema/locale-status.ts","../src/schema/versions.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Per-entity content table registry.\n *\n * Maps an entity name to its content infrastructure tables — drafts,\n * versions, locale_status. Each entry stores the `DefinedTable` (from\n * `defineTable`), which exposes a typed `.makeClient(db)` for CRUD via\n * `TableClient` and a raw `.table` for legacy callers.\n *\n * The content plugin populates this registry at `init` time; ContentClient\n * reads from it lazily on first use. Tables that don't apply to an entity\n * (non-publishable entities have no drafts, etc.) are simply absent.\n */\n\nimport type { DraftsTable } from './drafts.js'\nimport type { LocaleStatusTable } from './locale-status.js'\nimport type { VersionsTable } from './versions.js'\n\n/**\n * The full set of optional content infra tables attached to an entity.\n * Every field is optional — entities without the relevant behavior don't\n * get the corresponding table.\n */\nexport interface ContentTables {\n /** `{entity}_drafts` — present when the entity has `publishable()`. */\n drafts?: DraftsTable\n /** `{entity}_versions` — present when the entity has `versionable()`. */\n versions?: VersionsTable\n /** `{entity}_locale_status` — present when the entity is publishable + has translatable fields. */\n localeStatus?: LocaleStatusTable\n}\n\n// Module-level map. Single source of truth for ContentClient lookups.\nconst tables = new Map<string, ContentTables>()\n\n/**\n * Register content tables for an entity. Called from `content().init()`.\n *\n * Subsequent calls for the same entity name MERGE the provided patch into\n * the existing entry. This lets the plugin add tables one at a time (drafts,\n * then versions, then locale_status) without the caller needing to build the\n * whole object up front.\n */\nexport function registerContentTables(entityName: string, patch: ContentTables): void {\n const existing = tables.get(entityName) ?? {}\n tables.set(entityName, { ...existing, ...patch })\n}\n\n/**\n * Get the content tables for an entity. Returns an empty object (rather\n * than undefined) when the entity has no registered tables — that\n * simplifies the common ContentClient lookup idiom:\n *\n * const { drafts } = getContentTables(entity.name)\n * if (!drafts) return null\n */\nexport function getContentTables(entityName: string): ContentTables {\n return tables.get(entityName) ?? {}\n}\n\n/**\n * Clear the registry. **Test-only.** Throws in production.\n */\nexport function clearContentTables(): void {\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'clearContentTables() is not allowed in production — this is a test-only utility.',\n )\n }\n tables.clear()\n}\n","/**\n * Draft overlay table schema generator\n *\n * Generates a {entity}_drafts table for entities with the publishable() behavior.\n * Stores a mutable work-in-progress snapshot that non-publishers edit instead of\n * writing directly to the live published row.\n *\n * One draft per entity+locale. Drafts are upserted, discarded, or promoted to live.\n *\n * Uses `defineTable` so ContentClient can operate on drafts through a typed\n * TableClient (see registry.ts). The migration pipeline uses the\n * entity-package generator (which emits FK references) — this one produces a\n * runtime-only table shape for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a publishable entity's drafts table.\n *\n * Schema:\n * {entity}_drafts (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_',\n * data jsonb NOT NULL, -- full entity snapshot\n * created_by varchar(255) NOT NULL, -- user ID (varchar, NOT uuid!)\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * updated_at timestamptz NOT NULL DEFAULT now(),\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateDraftsTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_drafts`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n createdBy: column.varchar({ length: 255, notNull: true, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type DraftsTable = ReturnType<typeof generateDraftsTable>\n","/**\n * Per-locale publish status table schema generator\n *\n * Generates a {entity}_locale_status table for entities that have both\n * publishable() behavior AND translatable fields. This allows different\n * locales to have independent publish statuses.\n *\n * Fallback rule: no row for a locale = inherit base entity status.\n *\n * Uses `defineTable` so ContentClient can operate on rows through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references); this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a publishable + translatable entity's\n * locale status table.\n *\n * Schema:\n * {entity}_locale_status (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL,\n * status varchar(20) NOT NULL DEFAULT 'draft',\n * published_at timestamptz,\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateLocaleStatusTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_locale_status`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true }),\n status: column.varchar({ length: 20, notNull: true, default: 'draft' }),\n publishedAt: column.timestamp({ withTimezone: true, pgName: 'published_at' }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type LocaleStatusTable = ReturnType<typeof generateLocaleStatusTable>\n","/**\n * Version history table schema generator\n *\n * Generates a {entity}_versions table for entities with the versionable() behavior.\n * Each version stores a full snapshot + delta (changed fields only).\n *\n * Uses `defineTable` so ContentClient can operate on versions through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references) — this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a versionable entity's versions table.\n *\n * Schema:\n * {entity}_versions (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * version integer NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_', -- locale this snapshot belongs to\n * data jsonb NOT NULL, -- full snapshot (locale-merged view)\n * delta jsonb, -- only changed fields\n * status varchar(20), -- 'draft' | 'published' at time of save\n * created_by varchar(255),\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * is_autosave boolean NOT NULL DEFAULT false,\n * UNIQUE(entity_id, version, locale)\n * )\n *\n * Each locale has its own version sequence. The default locale uses '_'\n * as the locale value (distinguishable from actual locale codes).\n */\nexport function generateVersionsTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_versions`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n version: column.integer({ notNull: true }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n delta: column.jsonb<Record<string, unknown>>(),\n status: column.varchar({ length: 20 }),\n createdBy: column.varchar({ length: 255, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n isAutosave: column.boolean({ notNull: true, default: false, pgName: 'is_autosave' }),\n },\n unique: [{ on: ['entityId', 'version', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type VersionsTable = ReturnType<typeof generateVersionsTable>\n","/**\n * Content plugin — registers locale configuration and version schemas.\n *\n * Usage:\n * import { content } from '@murumets-ee/content/plugin'\n *\n * export default defineLumiConfig({\n * plugins: [\n * content({\n * locales: [\n * { code: 'en', label: 'English' },\n * { code: 'de', label: 'German' },\n * ],\n * defaultLocale: 'en',\n * }),\n * ],\n * })\n */\n\nimport { definePlugin } from '@murumets-ee/core'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { generateLayoutSchema, generateLayoutTranslationSchema } from '@murumets-ee/entity'\nimport { generateDraftsTable } from './schema/drafts.js'\nimport { generateLocaleStatusTable } from './schema/locale-status.js'\nimport { toolkitContentLocks } from './schema/locks.js'\nimport { registerContentTables } from './schema/registry.js'\nimport { generateVersionsTable } from './schema/versions.js'\nimport type { ContentConfig } from './types.js'\n\nlet _contentConfig: ContentConfig | null = null\n\n/**\n * Get the content configuration. Throws if plugin not initialized.\n */\nexport function getContentConfig(): ContentConfig {\n if (!_contentConfig) {\n throw new Error(\n '@murumets-ee/content plugin not initialized. Add content() to your plugins array.',\n )\n }\n return _contentConfig\n}\n\n/**\n * Content plugin factory.\n */\nexport function content(config: ContentConfig) {\n // Validate config\n if (!config.locales || config.locales.length === 0) {\n throw new Error('content() requires at least one locale')\n }\n\n if (!config.defaultLocale) {\n throw new Error('content() requires a defaultLocale')\n }\n\n const defaultExists = config.locales.some((l) => l.code === config.defaultLocale)\n if (!defaultExists) {\n throw new Error(\n `defaultLocale \"${config.defaultLocale}\" not found in locales: [${config.locales.map((l) => l.code).join(', ')}]`,\n )\n }\n\n return definePlugin({\n name: '@murumets-ee/content',\n server: {\n // The shared locks table is registered declaratively via the\n // framework's merge step (#469). The per-entity tables below\n // (versions / layout / drafts / locale_status) are generated\n // dynamically from `app.entities` and stay imperatively\n // registered — they're not known at plugin-construct time.\n tables: { toolkitContentLocks },\n init: async (app) => {\n // Store config globally\n _contentConfig = config\n\n let versionableCount = 0\n let blocksCount = 0\n let localeStatusCount = 0\n let draftsCount = 0\n\n for (const [name, entity] of app.entities) {\n // Register version tables for versionable entities\n const hasVersionable = entity.behaviors?.some(\n (b: { name: string }) => b.name === 'versionable',\n )\n if (hasVersionable) {\n const versionsTableName = `${name}_versions`\n if (!schemaRegistry.has(versionsTableName)) {\n const versions = generateVersionsTable(entity)\n schemaRegistry.register(versionsTableName, versions.table)\n registerContentTables(name, { versions })\n }\n versionableCount++\n }\n\n // Register layout tables for entities with blocks fields\n const allFields = entity.allFields as Record<\n string,\n { type: string; translatable?: boolean }\n >\n const hasBlocks = Object.values(allFields).some((f) => f.type === 'blocks')\n if (hasBlocks) {\n const layoutTableName = `${name}_layout`\n if (!schemaRegistry.has(layoutTableName)) {\n const layoutSchema = generateLayoutSchema(entity)\n if (layoutSchema) {\n schemaRegistry.register(layoutTableName, layoutSchema)\n }\n }\n\n const layoutTransTableName = `${name}_layout_translations`\n if (!schemaRegistry.has(layoutTransTableName)) {\n const layoutTransSchema = generateLayoutTranslationSchema(entity)\n if (layoutTransSchema) {\n schemaRegistry.register(layoutTransTableName, layoutTransSchema)\n }\n }\n\n blocksCount++\n }\n\n // Register locale_status tables for publishable + translatable entities\n const isPublishable = entity.behaviors?.some(\n (b: { name: string }) => b.name === 'publishable',\n )\n const hasTranslatable = Object.values(allFields).some((f) => f.translatable)\n if (isPublishable && hasTranslatable) {\n const statusTableName = `${name}_locale_status`\n if (!schemaRegistry.has(statusTableName)) {\n const localeStatus = generateLocaleStatusTable(entity)\n schemaRegistry.register(statusTableName, localeStatus.table)\n registerContentTables(name, { localeStatus })\n }\n localeStatusCount++\n }\n\n // Register drafts table for publishable entities\n if (isPublishable) {\n const draftsTableName = `${name}_drafts`\n if (!schemaRegistry.has(draftsTableName)) {\n const drafts = generateDraftsTable(entity)\n schemaRegistry.register(draftsTableName, drafts.table)\n registerContentTables(name, { drafts })\n }\n draftsCount++\n }\n }\n\n app.logger.info(\n {\n locales: config.locales.map((l) => l.code),\n defaultLocale: config.defaultLocale,\n versionableEntities: versionableCount,\n blockEntities: blocksCount,\n localeStatusEntities: localeStatusCount,\n draftEntities: draftsCount,\n },\n 'Content plugin initialized',\n )\n },\n },\n })\n}\n"],"mappings":"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"}
|
package/dist/plugin.d.mts
CHANGED
package/dist/plugin.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e,t}from"./plugin-
|
|
1
|
+
import{n as e,t}from"./plugin-DbK6FxTd.mjs";export{t as content,e as getContentConfig};
|
package/dist/query-client.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./locale-utils-
|
|
1
|
+
import{t as e}from"./locale-utils-DIcmuLIr.mjs";import"server-only";import{createQueryClient as t}from"@murumets-ee/core/clients";import{schemaRegistry as n}from"@murumets-ee/db";import{eq as r}from"drizzle-orm";var i=class{entity;query;constructor(e){this.entity=e.entity,this.query=e.query}async findById(e,t){return this.query.findById(e,t)}async findBySlug(t,i){i?.locale!==void 0&&e(i.locale);let a=n.get(this.entity.name);return!a||!a.slug?null:(await this.query.findMany({where:r(a.slug,t),limit:1,locale:i?.locale}))[0]??null}async findMany(e){return this.query.findMany(e)}async count(e){return this.query.count(e)}};function a(e){return new i({query:t(e),entity:e})}export{i as ContentQueryClient,a as createContentQueryClient};
|
|
2
2
|
//# sourceMappingURL=query-client.mjs.map
|
|
@@ -92,4 +92,4 @@ interface LockInfo {
|
|
|
92
92
|
}
|
|
93
93
|
//#endregion
|
|
94
94
|
export { InferBlocksUnion as a, LockInfo as c, TranslationStatus as d, VersionEntry as f, InferBlockData as i, TranslationFieldStatus as l, ContentConfig as n, LocaleConfig as o, VersionableOptions as p, DraftEntry as r, LocalePublishStatus as s, BlockDefinition as t, TranslationReport as u };
|
|
95
|
-
//# sourceMappingURL=types-
|
|
95
|
+
//# sourceMappingURL=types-CWo2dfEK.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-
|
|
1
|
+
{"version":3,"file":"types-CWo2dfEK.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;UAeiB,eAAA,WACL,MAAA,SAAe,WAAA,IAAe,MAAA,SAAe,WAAA;EAEvD,IAAA;EACA,KAAA;EACA,MAAA,EAAQ,CAAA;AAAA;;;;;KAWE,cAAA,WAAyB,eAAA,IACnC,CAAA,SAAU,eAAA;EACJ,MAAA,EAAQ,CAAA;EAAW,GAAA;AAAA,IAAgB,IAAA,CAAK,cAAA,CAAe,CAAA;;;AAF/D;;;;;KAYY,gBAAA,oBAAoC,eAAA,oBAClC,CAAA,GAAI,CAAA,CAAE,CAAA,UAAW,eAAA,GAAkB,cAAA,CAAe,CAAA,CAAE,CAAA;AAAA,UAOjD,YAAA;EACf,IAAA;EACA,KAAA;AAAA;AAAA,UAGe,aAAA;EACf,OAAA,EAAS,YAAA;EACT,aAAA;AAAA;AAAA,UAOe,kBAAA;EAhCT;EAkCN,SAAA;EAlCyB;EAoCzB,QAAA;AAAA;AAAA,UAOe,YAAA,eAA2B,MAAA,oBAA0B,MAAA;EACpE,EAAA;EACA,QAAA;EACA,OAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,KAAA,EAAO,OAAA,CAAQ,KAAA;EACf,MAAA;EACA,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,UAAA;AAAA;AAAA,UAae,sBAAA;EACf,QAAA;EACA,KAAA;EACA,UAAA;EACA,OAAA;AAAA;AAAA,UAGe,iBAAA;EAAA,CACd,MAAA,WAAiB,sBAAA;AAAA;AAAA,UAGH,iBAAA;EACf,KAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;AAAA;AAAA,UAOe,mBAAA;EACf,MAAA;EACA,MAAA;EACA,WAAA,EAAa,IAAA;AAAA;AAAA,UAOE,UAAA,eAAyB,MAAA,oBAA0B,MAAA;EAClE,EAAA;EACA,QAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,SAAA,EAAW,IAAA;AAAA;AAAA,UAOI,QAAA;EACf,QAAA;EACA,YAAA;EACA,QAAA,EAAU,IAAA;EACV,SAAA,EAAW,IAAA;AAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{USER_ID_MAX_LENGTH as e,field as t}from"@murumets-ee/entity";const n=[`draft`,`in_review`,`approved`,`rejected`,`published`],r={draft:[`in_review`],in_review:[`approved`,`rejected`],approved:[`published`,`in_review`],rejected:[`in_review`,`draft`],published:[`draft`]};function i(e,t){return e===t?!0:r[e]?.includes(t)??!1}var a=class extends Error{from;to;constructor(e,t){super(`Invalid workflow transition: ${e} → ${t}. Allowed transitions from "${e}": [${(r[e]??[]).join(`, `)}].`),this.name=`WorkflowTransitionError`,this.from=e,this.to=t}};function o(){return{name:`workflowable`,fields:{_workflowStatus:t.select({options:n,default:`draft`,indexed:!0,required:!0,internal:!0}),_workflowAssignee:t.text({required:!1,internal:!0,maxLength:e}),_workflowNote:t.text({required:!1,internal:!0}),_workflowUpdatedAt:t.date({internal:!0})},hooks:{beforeCreate:async e=>(e._workflowStatus=`draft`,e._workflowUpdatedAt=new Date,e),beforeUpdate:async(e,t,r)=>{let o=t._workflowStatus;if(o===void 0)return t;if(!n.includes(o))throw new a(`draft`,o);let s=(await r?.loadCurrent?.())?._workflowStatus??`draft`,c=o;if(!i(s,c))throw new a(s,c);return t._workflowUpdatedAt=new Date,t}}}}export{o as a,i,r as n,a as r,n as t};
|
|
2
|
+
//# sourceMappingURL=workflowable-aGtQhPRc.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflowable-aGtQhPRc.mjs","names":[],"sources":["../src/behaviors/workflowable.ts"],"sourcesContent":["/**\n * Workflowable behavior — editorial workflow with approval gates.\n *\n * Adds workflow status tracking fields to an entity and enforces the\n * documented transition state machine in `beforeUpdate`. Works alongside\n * `publishable()` — the workflow status tracks the editorial pipeline,\n * while publishable's `status` remains the public-facing state.\n *\n * Workflow state machine:\n *\n * draft ─submit─► in_review ─approve─► approved ─publish─► published\n * │ ▲ │\n * │ └────resubmit─────┐ │\n * └─reject─► rejected ─┘ │\n * │\n * unpublish ◄──────────────┘\n * (back to draft)\n *\n * Internal-write only: every workflow field is declared `internal: true`,\n * so the HTTP PATCH surface strips them from caller input. Workflow\n * transitions are performed by trusted server-side routes via\n * `AdminClient.updateInternal()` after authorizing the action with the\n * permission checker. The `beforeUpdate` hook validates that the\n * proposed transition is in the allowed table.\n *\n * Permission gating lives in the route handler (admin-ui workflow\n * routes), not in this behavior — by the time the hook fires, the\n * route has already authorized via `checkPermission`. The hook is\n * the structural defense: even a bug in the route layer cannot push\n * a workflow row into an illegal state.\n *\n * UI integration is tracked in GitHub issue #124. Until the workflow\n * UX ships, the behavior can still be added to entities — it'll simply\n * seed `_workflowStatus = 'draft'` on create and reject anyone trying\n * to write the field via PATCH.\n */\n\nimport type {\n Behavior,\n BehaviorContext,\n DateField,\n SelectField,\n TextField,\n} from '@murumets-ee/entity'\nimport { field, USER_ID_MAX_LENGTH } from '@murumets-ee/entity'\n\nexport const WORKFLOW_STATES = ['draft', 'in_review', 'approved', 'rejected', 'published'] as const\nexport type WorkflowState = (typeof WORKFLOW_STATES)[number]\n\n/**\n * Allowed transitions table. Each key is the *current* workflow status;\n * the value is the set of statuses it can move to.\n *\n * Encoded as a const-asserted record so a typo in the source produces\n * a compile error rather than a silent no-match at runtime.\n */\nexport const WORKFLOW_TRANSITIONS = {\n draft: ['in_review'],\n in_review: ['approved', 'rejected'],\n approved: ['published', 'in_review'],\n rejected: ['in_review', 'draft'],\n published: ['draft'],\n} as const satisfies Record<WorkflowState, readonly WorkflowState[]>\n\n/**\n * Is `to` an allowed transition from `from`? Pure, no I/O.\n */\nexport function isAllowedTransition(from: WorkflowState, to: WorkflowState): boolean {\n if (from === to) return true // no-op self-transition is fine\n const allowed = WORKFLOW_TRANSITIONS[from] as readonly WorkflowState[] | undefined\n return allowed?.includes(to) ?? false\n}\n\n/**\n * Thrown when `beforeUpdate` rejects a workflow transition. Distinct\n * subclass so admin-ui's API handler can map it to HTTP 400.\n */\nexport class WorkflowTransitionError extends Error {\n readonly from: WorkflowState\n readonly to: WorkflowState\n\n constructor(from: WorkflowState, to: WorkflowState) {\n super(\n `Invalid workflow transition: ${from} → ${to}. ` +\n `Allowed transitions from \"${from}\": [${(WORKFLOW_TRANSITIONS[from] ?? []).join(', ')}].`,\n )\n this.name = 'WorkflowTransitionError'\n this.from = from\n this.to = to\n }\n}\n\nexport type WorkflowableFields = {\n _workflowStatus: SelectField & { required: true }\n _workflowAssignee: TextField\n _workflowNote: TextField\n _workflowUpdatedAt: DateField\n}\n\nexport function workflowable(): Behavior<WorkflowableFields> {\n return {\n name: 'workflowable',\n fields: {\n _workflowStatus: field.select({\n options: WORKFLOW_STATES as unknown as string[],\n default: 'draft',\n indexed: true,\n required: true,\n internal: true,\n }) as SelectField & { required: true },\n // `_workflowAssignee` stores a better-auth user ID — varchar(255)\n // per CLAUDE.md §\"User IDs\", emitted via `maxLength: 255` (#430).\n // `_workflowNote` is free-text, so it stays unbounded `text`.\n _workflowAssignee: field.text({\n required: false,\n internal: true,\n maxLength: USER_ID_MAX_LENGTH,\n }),\n _workflowNote: field.text({ required: false, internal: true }),\n _workflowUpdatedAt: field.date({ internal: true }),\n },\n hooks: {\n beforeCreate: async (data) => {\n data._workflowStatus = 'draft'\n data._workflowUpdatedAt = new Date()\n return data\n },\n beforeUpdate: async (\n _id: string,\n data: Record<string, unknown>,\n ctx?: BehaviorContext,\n ): Promise<Record<string, unknown>> => {\n // Only validate when the patch tries to change the workflow status.\n // Updates that don't touch _workflowStatus pass through (the field's\n // `internal: true` already prevents it from sneaking in via PATCH;\n // only updateInternal callers reach this branch with a value).\n const proposed = data._workflowStatus\n if (proposed === undefined) return data\n\n if (!WORKFLOW_STATES.includes(proposed as WorkflowState)) {\n throw new WorkflowTransitionError('draft', proposed as WorkflowState)\n }\n\n // Need the current row to validate the from→to transition. The\n // lazy loader is provided by AdminClient — present on update,\n // absent on create.\n const current = await ctx?.loadCurrent?.()\n const fromState = (current?._workflowStatus as WorkflowState | undefined) ?? 'draft'\n const toState = proposed as WorkflowState\n\n if (!isAllowedTransition(fromState, toState)) {\n throw new WorkflowTransitionError(fromState, toState)\n }\n\n data._workflowUpdatedAt = new Date()\n return data\n },\n },\n }\n}\n"],"mappings":"oEA8CA,MAAa,EAAkB,CAAC,QAAS,YAAa,WAAY,WAAY,YAAY,CAU7E,EAAuB,CAClC,MAAO,CAAC,YAAY,CACpB,UAAW,CAAC,WAAY,WAAW,CACnC,SAAU,CAAC,YAAa,YAAY,CACpC,SAAU,CAAC,YAAa,QAAQ,CAChC,UAAW,CAAC,QAAQ,CACrB,CAKD,SAAgB,EAAoB,EAAqB,EAA4B,CAGnF,OAFI,IAAS,EAAW,GACR,EAAqB,IACrB,SAAS,EAAG,EAAI,GAOlC,IAAa,EAAb,cAA6C,KAAM,CACjD,KACA,GAEA,YAAY,EAAqB,EAAmB,CAClD,MACE,gCAAgC,EAAK,KAAK,EAAG,8BACd,EAAK,OAAO,EAAqB,IAAS,EAAE,EAAE,KAAK,KAAK,CAAC,IACzF,CACD,KAAK,KAAO,0BACZ,KAAK,KAAO,EACZ,KAAK,GAAK,IAWd,SAAgB,GAA6C,CAC3D,MAAO,CACL,KAAM,eACN,OAAQ,CACN,gBAAiB,EAAM,OAAO,CAC5B,QAAS,EACT,QAAS,QACT,QAAS,GACT,SAAU,GACV,SAAU,GACX,CAAC,CAIF,kBAAmB,EAAM,KAAK,CAC5B,SAAU,GACV,SAAU,GACV,UAAW,EACZ,CAAC,CACF,cAAe,EAAM,KAAK,CAAE,SAAU,GAAO,SAAU,GAAM,CAAC,CAC9D,mBAAoB,EAAM,KAAK,CAAE,SAAU,GAAM,CAAC,CACnD,CACD,MAAO,CACL,aAAc,KAAO,KACnB,EAAK,gBAAkB,QACvB,EAAK,mBAAqB,IAAI,KACvB,GAET,aAAc,MACZ,EACA,EACA,IACqC,CAKrC,IAAM,EAAW,EAAK,gBACtB,GAAI,IAAa,IAAA,GAAW,OAAO,EAEnC,GAAI,CAAC,EAAgB,SAAS,EAA0B,CACtD,MAAM,IAAI,EAAwB,QAAS,EAA0B,CAOvE,IAAM,GAAa,MADG,GAAK,eAAe,GACd,iBAAiD,QACvE,EAAU,EAEhB,GAAI,CAAC,EAAoB,EAAW,EAAQ,CAC1C,MAAM,IAAI,EAAwB,EAAW,EAAQ,CAIvD,MADA,GAAK,mBAAqB,IAAI,KACvB,GAEV,CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/content",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"types": "./dist/blocks/index.d.mts",
|
|
25
25
|
"import": "./dist/blocks/index.mjs"
|
|
26
26
|
},
|
|
27
|
+
"./entities": {
|
|
28
|
+
"types": "./dist/entities/index.d.mts",
|
|
29
|
+
"import": "./dist/entities/index.mjs"
|
|
30
|
+
},
|
|
27
31
|
"./lock": {
|
|
28
32
|
"types": "./dist/lock-service.d.mts",
|
|
29
33
|
"import": "./dist/lock-service.mjs"
|
|
@@ -36,10 +40,10 @@
|
|
|
36
40
|
"drizzle-orm": "^0.45.2",
|
|
37
41
|
"server-only": "^0.0.1",
|
|
38
42
|
"zod": "^3.24.1",
|
|
39
|
-
"@murumets-ee/core": "0.
|
|
40
|
-
"@murumets-ee/
|
|
41
|
-
"@murumets-ee/
|
|
42
|
-
"@murumets-ee/logging": "0.
|
|
43
|
+
"@murumets-ee/core": "0.24.1",
|
|
44
|
+
"@murumets-ee/db": "0.24.1",
|
|
45
|
+
"@murumets-ee/entity": "0.24.1",
|
|
46
|
+
"@murumets-ee/logging": "0.24.1"
|
|
43
47
|
},
|
|
44
48
|
"devDependencies": {
|
|
45
49
|
"@types/node": "^20.19.40",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-B4kqLg-c.d.mts","names":[],"sources":["../src/behaviors/versionable.ts","../src/behaviors/workflowable.ts","../src/capabilities.ts","../src/define-block.ts","../src/diff/types.ts","../src/errors.ts"],"mappings":";;;;KAmBY,iBAAA;EACV,QAAA,EAAU,WAAA;IAAgB,QAAA;EAAA;EAC1B,YAAA,EAAc,SAAA;AAAA;AAAA,iBAQA,WAAA,CAAY,OAAA,GAAU,kBAAA,GAAqB,QAAA,CAAS,iBAAA;;;;iBAqDpD,qBAAA,CAAsB,SAAA,GAAY,QAAA,KAAa,QAAA,CAAS,kBAAA;;;cCpC3D,eAAA;AAAA,KACD,aAAA,WAAwB,eAAA;;;;;;;;cASvB,oBAAA;EAAA;;;;;;;;;iBAWG,mBAAA,CAAoB,IAAA,EAAM,aAAA,EAAe,EAAA,EAAI,aAAA;;;;;cAUhD,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,QAAA,CAAS,kBAAA;;;KC7E7B,gBAAA;;;;;;iBAOI,cAAA,CAAe,MAAA,EAAQ,MAAA,EAAQ,UAAA,EAAY,gBAAA;;;iBCN3C,WAAA,iBAA4B,MAAA,SAAe,WAAA,EAAA,CAAc,UAAA;EACvE,IAAA;EACA,KAAA;EACA,MAAA,EAAQ,CAAA;AAAA,IACN,eAAA,CAAgB,CAAA;;;;;;;UCtBH,WAAA;EACf,IAAA;EACA,KAAA;AAAA;;UAIe,SAAA;EACf,KAAA;EACA,IAAA;EAWA,IAAA;EACA,EAAA;EJJuB;EIMvB,QAAA,GAAW,WAAA;EJEc;EIAzB,UAAA,GAAa,cAAA;AAAA;;KAIH,eAAA;;UAGK,cAAA;EACf,OAAA;EACA,SAAA;EACA,MAAA,EAAQ,eAAA;EACR,SAAA;EACA,OAAA;EJZmF;EIcnF,SAAA,GAAY,QAAA;AAAA;;UAIG,QAAA;EACf,KAAA;EACA,IAAA;EACA,EAAA;EJgCqE;EI9BrE,QAAA,GAAW,WAAA;AAAA;;;;;;;AJjCb;;;;;;;;cKNa,qBAAA,SAA8B,KAAA;EAAA,SAChC,UAAA;EAAA,SACA,UAAA;cAGP,UAAA,UACA,UAAA,EAAY,qBAAA,gBACZ,OAAA;AAAA;;;;;;;;;;cAsBS,kBAAA,SAA2B,KAAA;EAAA,SAC7B,MAAA;EAAA,SACA,OAAA;cAEG,MAAA,UAAgB,OAAA;AAAA"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{n as e}from"./errors-kkf8Ue1H.mjs";import{n as t}from"./plugin-DkmXJ7Qw.mjs";function n(n){if(n===`_`)return;let r=t();for(let e of r.locales)if(e.code===n)return;throw new e(n,r.locales.map(e=>e.code))}export{n as t};
|
|
2
|
-
//# sourceMappingURL=locale-utils-BMRVJwvU.mjs.map
|