@murumets-ee/content 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,94 @@
1
+ Elastic License 2.0 (ELv2)
2
+
3
+ URL: https://www.elastic.co/licensing/elastic-license
4
+
5
+ ## Acceptance
6
+
7
+ By using the software, you agree to all of the terms and conditions below.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject
14
+ to the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ You may not provide the software to third parties as a hosted or managed
19
+ service, where the service provides users with access to any substantial set
20
+ of the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other
27
+ notices of the licensor in the software. Any use of the licensor's trademarks
28
+ is subject to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or becomes able to license, to make, have made, use, sell, offer for
34
+ sale, import and have imported the software, in each case subject to the
35
+ limitations and conditions in this license. This license does not cover any
36
+ patent claims that you cause to be infringed by modifications or additions to
37
+ the software. If you or your company make any written claim that the software
38
+ infringes or contributes to infringement of any patent, your patent license
39
+ for the software granted under these terms ends immediately. If your company
40
+ makes such a claim, your patent license ends immediately for work on behalf
41
+ of your company.
42
+
43
+ ## Notices
44
+
45
+ You must ensure that anyone who gets a copy of any part of the software from
46
+ you also gets a copy of these terms.
47
+
48
+ If you modify the software, you must include in any modified copies of the
49
+ software prominent notices stating that you have modified the software.
50
+
51
+ ## No Other Rights
52
+
53
+ These terms do not imply any licenses other than those expressly granted in
54
+ these terms.
55
+
56
+ ## Termination
57
+
58
+ If you use the software in violation of these terms, such use is not licensed,
59
+ and your licenses will automatically terminate. If the licensor provides you
60
+ with a notice of your violation, and you cease all violation of this license
61
+ no later than 30 days after you receive that notice, your licenses will be
62
+ reinstated retroactively. However, if you violate these terms after such
63
+ reinstatement, any additional violation of these terms will cause your
64
+ licenses to terminate automatically and permanently.
65
+
66
+ ## No Liability
67
+
68
+ As far as the law allows, the software comes as is, without any warranty or
69
+ condition, and the licensor will not be liable to you for any damages arising
70
+ out of these terms or the use or nature of the software, under any kind of
71
+ legal claim.
72
+
73
+ ## Definitions
74
+
75
+ The **licensor** is the entity offering these terms, and the **software** is
76
+ the software the licensor makes available under these terms, including any
77
+ portion of it.
78
+
79
+ **you** refers to the individual or entity agreeing to these terms.
80
+
81
+ **your company** is any legal entity, sole proprietorship, or other kind of
82
+ organization that you work for, plus all organizations that have control over,
83
+ are under the control of, or are under common control with that organization.
84
+ **control** means ownership of substantially all the assets of an entity, or
85
+ the power to direct the management and policies of an entity (for example, by
86
+ voting right, contract, or otherwise). Control can be direct or indirect.
87
+
88
+ **your licenses** are all the licenses granted to you for the software under
89
+ these terms.
90
+
91
+ **use** means anything you do with the software requiring one of your
92
+ licenses.
93
+
94
+ **trademark** means trademarks, service marks, and similar rights.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @murumets-ee/content
2
+
3
+ Content management — localization, publishing workflows, versioning, drafts, and translation status.
4
+
5
+ Part of [Lumi CMS](https://github.com/murumets-ee/lumi-cms) — a modular, type-safe CMS toolkit for Node.js.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @murumets-ee/content
11
+ ```
12
+
13
+ ## License
14
+
15
+ [Elastic License 2.0 (ELv2)](../../LICENSE)
@@ -0,0 +1,187 @@
1
+ import { B as BlockDefinition } from '../types-BYeiE1ie.js';
2
+ import * as _murumets_ee_entity from '@murumets-ee/entity';
3
+
4
+ declare const CodeBlock: BlockDefinition<{
5
+ readonly code: _murumets_ee_entity.TextField & {
6
+ readonly required: true;
7
+ };
8
+ readonly language: _murumets_ee_entity.SelectField & {
9
+ options: readonly ["javascript", "typescript", "python", "html", "css", "json", "bash", "sql", "other"];
10
+ } & Omit<Partial<_murumets_ee_entity.SelectField>, "options">;
11
+ }>;
12
+
13
+ declare const CTABlock: BlockDefinition<{
14
+ readonly heading: _murumets_ee_entity.TextField & {
15
+ readonly required: true;
16
+ readonly translatable: true;
17
+ };
18
+ readonly description: _murumets_ee_entity.TextField & {
19
+ readonly translatable: true;
20
+ };
21
+ readonly buttonText: _murumets_ee_entity.TextField & {
22
+ readonly required: true;
23
+ readonly translatable: true;
24
+ };
25
+ readonly buttonLink: _murumets_ee_entity.TextField & {
26
+ readonly required: true;
27
+ };
28
+ }>;
29
+
30
+ declare const GalleryBlock: BlockDefinition<{
31
+ readonly images: _murumets_ee_entity.ReferenceField & {
32
+ cardinality: "many";
33
+ } & Partial<Omit<_murumets_ee_entity.ReferenceField, "entity" | "cardinality">>;
34
+ readonly columns: _murumets_ee_entity.SelectField & {
35
+ options: readonly ["2", "3", "4"];
36
+ } & Omit<Partial<_murumets_ee_entity.SelectField>, "options">;
37
+ }>;
38
+
39
+ declare const HeroBlock: BlockDefinition<{
40
+ readonly heading: _murumets_ee_entity.TextField & {
41
+ readonly required: true;
42
+ readonly translatable: true;
43
+ };
44
+ readonly subheading: _murumets_ee_entity.TextField & {
45
+ readonly translatable: true;
46
+ };
47
+ readonly background: _murumets_ee_entity.MediaField & {
48
+ readonly accept: ["image/*"];
49
+ };
50
+ readonly alignment: _murumets_ee_entity.SelectField & {
51
+ options: readonly ["left", "center", "right"];
52
+ } & Omit<Partial<_murumets_ee_entity.SelectField>, "options">;
53
+ }>;
54
+
55
+ declare const ImageBlock: BlockDefinition<{
56
+ readonly image: _murumets_ee_entity.MediaField & {
57
+ readonly accept: ["image/*"];
58
+ readonly required: true;
59
+ };
60
+ readonly alt: _murumets_ee_entity.TextField & {
61
+ readonly translatable: true;
62
+ };
63
+ readonly caption: _murumets_ee_entity.TextField & {
64
+ readonly translatable: true;
65
+ };
66
+ }>;
67
+
68
+ declare const QuoteBlock: BlockDefinition<{
69
+ readonly quote: _murumets_ee_entity.TextField & {
70
+ readonly required: true;
71
+ readonly translatable: true;
72
+ };
73
+ readonly attribution: _murumets_ee_entity.TextField & {
74
+ readonly translatable: true;
75
+ };
76
+ }>;
77
+
78
+ declare const RichTextBlock: BlockDefinition<{
79
+ readonly content: _murumets_ee_entity.RichTextField & {
80
+ readonly translatable: true;
81
+ };
82
+ }>;
83
+
84
+ declare const VideoBlock: BlockDefinition<{
85
+ readonly url: _murumets_ee_entity.TextField & {
86
+ readonly required: true;
87
+ };
88
+ readonly poster: _murumets_ee_entity.MediaField & {
89
+ readonly accept: ["image/*"];
90
+ };
91
+ readonly caption: _murumets_ee_entity.TextField & {
92
+ readonly translatable: true;
93
+ };
94
+ }>;
95
+
96
+ /**
97
+ * All common blocks shipped with the toolkit.
98
+ * Use as-is or pick individual blocks from the named exports.
99
+ *
100
+ * @example
101
+ * import { commonBlocks } from '@murumets-ee/content/blocks'
102
+ * const Page = defineEntity({
103
+ * name: 'page',
104
+ * fields: {
105
+ * layout: field.blocks({ blocks: [...commonBlocks] }),
106
+ * },
107
+ * })
108
+ */
109
+ declare const commonBlocks: readonly [BlockDefinition<{
110
+ readonly content: _murumets_ee_entity.RichTextField & {
111
+ readonly translatable: true;
112
+ };
113
+ }>, BlockDefinition<{
114
+ readonly image: _murumets_ee_entity.MediaField & {
115
+ readonly accept: ["image/*"];
116
+ readonly required: true;
117
+ };
118
+ readonly alt: _murumets_ee_entity.TextField & {
119
+ readonly translatable: true;
120
+ };
121
+ readonly caption: _murumets_ee_entity.TextField & {
122
+ readonly translatable: true;
123
+ };
124
+ }>, BlockDefinition<{
125
+ readonly heading: _murumets_ee_entity.TextField & {
126
+ readonly required: true;
127
+ readonly translatable: true;
128
+ };
129
+ readonly subheading: _murumets_ee_entity.TextField & {
130
+ readonly translatable: true;
131
+ };
132
+ readonly background: _murumets_ee_entity.MediaField & {
133
+ readonly accept: ["image/*"];
134
+ };
135
+ readonly alignment: _murumets_ee_entity.SelectField & {
136
+ options: readonly ["left", "center", "right"];
137
+ } & Omit<Partial<_murumets_ee_entity.SelectField>, "options">;
138
+ }>, BlockDefinition<{
139
+ readonly images: _murumets_ee_entity.ReferenceField & {
140
+ cardinality: "many";
141
+ } & Partial<Omit<_murumets_ee_entity.ReferenceField, "entity" | "cardinality">>;
142
+ readonly columns: _murumets_ee_entity.SelectField & {
143
+ options: readonly ["2", "3", "4"];
144
+ } & Omit<Partial<_murumets_ee_entity.SelectField>, "options">;
145
+ }>, BlockDefinition<{
146
+ readonly heading: _murumets_ee_entity.TextField & {
147
+ readonly required: true;
148
+ readonly translatable: true;
149
+ };
150
+ readonly description: _murumets_ee_entity.TextField & {
151
+ readonly translatable: true;
152
+ };
153
+ readonly buttonText: _murumets_ee_entity.TextField & {
154
+ readonly required: true;
155
+ readonly translatable: true;
156
+ };
157
+ readonly buttonLink: _murumets_ee_entity.TextField & {
158
+ readonly required: true;
159
+ };
160
+ }>, BlockDefinition<{
161
+ readonly url: _murumets_ee_entity.TextField & {
162
+ readonly required: true;
163
+ };
164
+ readonly poster: _murumets_ee_entity.MediaField & {
165
+ readonly accept: ["image/*"];
166
+ };
167
+ readonly caption: _murumets_ee_entity.TextField & {
168
+ readonly translatable: true;
169
+ };
170
+ }>, BlockDefinition<{
171
+ readonly quote: _murumets_ee_entity.TextField & {
172
+ readonly required: true;
173
+ readonly translatable: true;
174
+ };
175
+ readonly attribution: _murumets_ee_entity.TextField & {
176
+ readonly translatable: true;
177
+ };
178
+ }>, BlockDefinition<{
179
+ readonly code: _murumets_ee_entity.TextField & {
180
+ readonly required: true;
181
+ };
182
+ readonly language: _murumets_ee_entity.SelectField & {
183
+ options: readonly ["javascript", "typescript", "python", "html", "css", "json", "bash", "sql", "other"];
184
+ } & Omit<Partial<_murumets_ee_entity.SelectField>, "options">;
185
+ }>];
186
+
187
+ export { CTABlock, CodeBlock, GalleryBlock, HeroBlock, ImageBlock, QuoteBlock, RichTextBlock, VideoBlock, commonBlocks };
@@ -0,0 +1 @@
1
+ import{a as e}from"../chunk-CULYIGZI.js";import{field as d}from"@murumets-ee/entity";var r=e({slug:"code",label:"Code",fields:{code:d.text({required:!0}),language:d.select({options:["javascript","typescript","python","html","css","json","bash","sql","other"]})}});import{field as t}from"@murumets-ee/entity";var l=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})}});import{field as u}from"@murumets-ee/entity";var i=e({slug:"gallery",label:"Gallery",fields:{images:u.reference({entity:"media",cardinality:"many"}),columns:u.select({options:["2","3","4"]})}});import{field as o}from"@murumets-ee/entity";var a=e({slug:"hero",label:"Hero Section",fields:{heading:o.text({required:!0,translatable:!0}),subheading:o.text({translatable:!0}),background:o.media({accept:["image/*"]}),alignment:o.select({options:["left","center","right"]})}});import{field as c}from"@murumets-ee/entity";var m=e({slug:"image",label:"Image",fields:{image:c.media({accept:["image/*"],required:!0}),alt:c.text({translatable:!0}),caption:c.text({translatable:!0})}});import{field as x}from"@murumets-ee/entity";var n=e({slug:"quote",label:"Quote",fields:{quote:x.text({required:!0,translatable:!0}),attribution:x.text({translatable:!0})}});import{field as k}from"@murumets-ee/entity";var f=e({slug:"richtext",label:"Rich Text",fields:{content:k.richtext({translatable:!0})}});import{field as s}from"@murumets-ee/entity";var p=e({slug:"video",label:"Video",fields:{url:s.text({required:!0}),poster:s.media({accept:["image/*"]}),caption:s.text({translatable:!0})}});var X=[f,m,a,i,l,p,n,r];export{l as CTABlock,r as CodeBlock,i as GalleryBlock,a as HeroBlock,m as ImageBlock,n as QuoteBlock,f as RichTextBlock,p as VideoBlock,X as commonBlocks};
@@ -0,0 +1 @@
1
+ import{field as g}from"@murumets-ee/entity";var x={maxPerDoc:50,autosave:!1};function j(e){let t={...x,...e};return{name:"versionable",fields:{_version:g.number({required:!0,default:1,integer:!0}),_versionedAt:g.date()},hooks:{beforeCreate:async s=>(s._version=1,s._versionedAt=new Date,s),beforeUpdate:async(s,f)=>(f._versionedAt=new Date,f)},_versionableOptions:t}}function F(e){return e?e.find(n=>n.name==="versionable")?._versionableOptions??null:null}function h(e){return e.match(/\S+|\s+/g)??[]}function b(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=h(e),s=h(t),f=n.length,r=s.length,o=Array.from({length:f+1},()=>new Array(r+1).fill(0));for(let c=1;c<=f;c++)for(let u=1;u<=r;u++)n[c-1]===s[u-1]?o[c][u]=o[c-1][u-1]+1:o[c][u]=Math.max(o[c-1][u],o[c][u-1]);let p=[],i=f,l=r,d=[];for(;i>0||l>0;)i>0&&l>0&&n[i-1]===s[l-1]?(d.push({type:"equal",value:n[i-1]}),i--,l--):l>0&&(i===0||o[i][l-1]>=o[i-1][l])?(d.push({type:"add",value:s[l-1]}),l--):(d.push({type:"remove",value:n[i-1]}),i--);d.reverse();for(let c of d){let u=p[p.length-1];u&&u.type===c.type?u.value+=c.value:p.push({...c})}return p}function m(e){return e.replace(/<[^>]*>/g," ").replace(/\s+/g," ").trim()}function a(e){return e==null?"":e instanceof Date?e.toISOString():typeof e=="object"?JSON.stringify(e):String(e)}function D(e,t,n,s){if(JSON.stringify(t)===JSON.stringify(n))return null;switch(s?.type??O(t,n)){case"text":case"slug":{let r=b(a(t),a(n));return{field:e,type:"text",from:t,to:n,segments:r}}case"richtext":{let r=b(m(a(t)),m(a(n)));return{field:e,type:"richtext",from:t,to:n,segments:r}}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 I(e,t,n){let s=new Set([...Object.keys(e),...Object.keys(t)]),f=[];for(let r of s){if(r==="id"||r==="createdAt"||r==="updatedAt")continue;let o=D(r,e[r],t[r],n?.[r]);o&&f.push(o)}return f}function O(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 v(e,t){if(typeof e=="string"||typeof t=="string")return b(a(e),a(t))}function _(e){return typeof e=="object"&&e!==null&&"_block"in e&&"_id"in e&&typeof e._block=="string"&&typeof e._id=="string"}function B(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),s=[];for(let f of n){if(f==="_block"||f==="_id")continue;let r=e[f],o=t[f];JSON.stringify(r)!==JSON.stringify(o)&&s.push({field:f,from:r,to:o,segments:v(r,o)})}return s}function E(e,t){let n=e.filter(_),s=t.filter(_),f=new Map,r=new Map;for(let i=0;i<n.length;i++)f.set(n[i]._id,{block:n[i],index:i});for(let i=0;i<s.length;i++)r.set(s[i]._id,{block:s[i],index:i});let o=[],p=new Set;for(let[i,{block:l,index:d}]of r){p.add(i);let c=f.get(i);if(!c){o.push({blockId:i,blockType:l._block,status:"added",toIndex:d});continue}let u=c.block,y=c.index,k=B(u,l),w=y!==d;k.length===0&&!w?o.push({blockId:i,blockType:l._block,status:"unchanged",fromIndex:y,toIndex:d}):k.length>0?o.push({blockId:i,blockType:l._block,status:"changed",fromIndex:y,toIndex:d,propDiffs:k}):o.push({blockId:i,blockType:l._block,status:"moved",fromIndex:y,toIndex:d})}for(let[i,{block:l,index:d}]of f)p.has(i)||o.push({blockId:i,blockType:l._block,status:"removed",fromIndex:d});return o}export{j as a,F as b,b as c,D as d,I as e,v as f,E as g};
@@ -0,0 +1 @@
1
+ function e(l){if(!l.slug||!l.slug.match(/^[a-z][a-z0-9-]*$/))throw new Error(`Block slug must be lowercase alphanumeric with hyphens, got: "${l.slug}"`);if(!l.label)throw new Error(`Block label is required for slug: "${l.slug}"`);if(!l.fields||Object.keys(l.fields).length===0)throw new Error(`Block "${l.slug}" must have at least one field`);return{slug:l.slug,label:l.label,fields:l.fields}}export{e as a};
@@ -0,0 +1,215 @@
1
+ import { Logger } from '@murumets-ee/core';
2
+ import { Entity, FieldToTS, FieldConfig } from '@murumets-ee/entity';
3
+ import { FindManyOptions } from '@murumets-ee/entity/admin';
4
+ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
5
+ import { F as FieldDiff } from './types-Cr0ifLn_.js';
6
+ import { V as VersionEntry, D as DraftEntry, T as TranslationStatus, a as TranslationReport, L as LocalePublishStatus } from './types-BYeiE1ie.js';
7
+
8
+ /**
9
+ * ContentClient — wraps AdminClient with version history and translation status.
10
+ *
11
+ * Usage:
12
+ * import { createContentClient } from '@murumets-ee/content/client'
13
+ * const pages = createContentClient(Page)
14
+ * await pages.create({ title: 'Hello', layout: [...] })
15
+ */
16
+
17
+ /** Value type that any entity field can hold */
18
+ type EntityFieldValue = FieldToTS<FieldConfig> | null | undefined;
19
+ interface ContentClientConfig {
20
+ entity: Entity;
21
+ db: PostgresJsDatabase;
22
+ logger?: Logger;
23
+ }
24
+ declare class ContentClient {
25
+ private entity;
26
+ private db;
27
+ private logger?;
28
+ private admin;
29
+ private versionableOptions;
30
+ constructor(config: ContentClientConfig);
31
+ private getAdmin;
32
+ create(data: Record<string, EntityFieldValue>): Promise<{
33
+ id: string;
34
+ } & {} & {
35
+ [x: string]: string | number | boolean | Date | Record<string, unknown> | Record<string, unknown>[] | {
36
+ [key: string]: unknown;
37
+ _block: string;
38
+ _id: string;
39
+ }[] | null | undefined;
40
+ }>;
41
+ findById(id: string, options?: {
42
+ locale?: string;
43
+ }): Promise<({
44
+ id: string;
45
+ } & {} & {
46
+ [x: string]: string | number | boolean | Date | Record<string, unknown> | Record<string, unknown>[] | {
47
+ [key: string]: unknown;
48
+ _block: string;
49
+ _id: string;
50
+ }[] | null | undefined;
51
+ }) | null>;
52
+ findMany(options?: FindManyOptions): Promise<({
53
+ id: string;
54
+ } & {} & {
55
+ [x: string]: string | number | boolean | Date | Record<string, unknown> | Record<string, unknown>[] | {
56
+ [key: string]: unknown;
57
+ _block: string;
58
+ _id: string;
59
+ }[] | null | undefined;
60
+ })[]>;
61
+ update(id: string, data: Record<string, EntityFieldValue>): Promise<{
62
+ id: string;
63
+ } & {} & {
64
+ [x: string]: string | number | boolean | Date | Record<string, unknown> | Record<string, unknown>[] | {
65
+ [key: string]: unknown;
66
+ _block: string;
67
+ _id: string;
68
+ }[] | null | undefined;
69
+ }>;
70
+ delete(id: string): Promise<void>;
71
+ saveTranslation(entityId: string, locale: string, translations: Record<string, unknown>): Promise<void>;
72
+ deleteTranslation(entityId: string, locale?: string): Promise<void>;
73
+ /**
74
+ * Copy layout from one locale to another (for localized: true blocks).
75
+ */
76
+ copyLayout(entityId: string, options: {
77
+ from: string;
78
+ to: string | string[];
79
+ }): Promise<void>;
80
+ /**
81
+ * List versions for an entity (newest first), filtered by locale.
82
+ * Uses '_' as the locale value for default-locale snapshots.
83
+ */
84
+ getVersions(entityId: string, options?: {
85
+ limit?: number;
86
+ offset?: number;
87
+ locale?: string;
88
+ }): Promise<VersionEntry[]>;
89
+ /**
90
+ * Get a specific version snapshot.
91
+ */
92
+ getVersion(entityId: string, version: number, locale?: string): Promise<VersionEntry | null>;
93
+ /**
94
+ * Compare two versions and return field-level diff.
95
+ *
96
+ * Returns structured diffs with word-level segments for text fields
97
+ * and structural block diffs for blocks fields.
98
+ */
99
+ compareVersions(entityId: string, v1: number, v2: number, locale?: string): Promise<{
100
+ diffs: FieldDiff[];
101
+ fromVersion: VersionEntry;
102
+ toVersion: VersionEntry;
103
+ } | null>;
104
+ /**
105
+ * Compare the current draft with the live entity (or a specific version).
106
+ * Returns field-level diffs between the current published data and the draft.
107
+ */
108
+ compareDraftWithCurrent(entityId: string, locale?: string): Promise<{
109
+ diffs: FieldDiff[];
110
+ current: Record<string, unknown>;
111
+ draft: DraftEntry;
112
+ } | null>;
113
+ /**
114
+ * Restore an entity to a previous version.
115
+ */
116
+ restoreVersion(entityId: string, version: number): Promise<{
117
+ id: string;
118
+ } & {} & {
119
+ [x: string]: string | number | boolean | Date | Record<string, unknown> | Record<string, unknown>[] | {
120
+ [key: string]: unknown;
121
+ _block: string;
122
+ _id: string;
123
+ }[] | null | undefined;
124
+ }>;
125
+ /**
126
+ * Enforce maxPerDoc retention by deleting oldest versions for a locale.
127
+ */
128
+ pruneVersions(entityId: string, locale?: string): Promise<void>;
129
+ /**
130
+ * Get translation status for an entity — per-locale completion report.
131
+ */
132
+ getTranslationStatus(entityId: string): Promise<TranslationStatus>;
133
+ /**
134
+ * Get translation status for multiple entities in a single query.
135
+ * Returns a map of entityId → TranslationStatus (O(1) DB roundtrips).
136
+ */
137
+ getTranslationStatusBatch(entityIds: string[]): Promise<Map<string, TranslationStatus>>;
138
+ /**
139
+ * Get translation report across all entities of this type.
140
+ */
141
+ getTranslationReport(options?: {
142
+ locale?: string;
143
+ }): Promise<TranslationReport>;
144
+ /**
145
+ * Get per-locale publish status for an entity.
146
+ * Returns status for all non-default locales that have explicit overrides.
147
+ * Missing locale = inherits base entity status.
148
+ */
149
+ getLocaleStatus(entityId: string): Promise<LocalePublishStatus[]>;
150
+ /**
151
+ * Set per-locale publish status. Upserts into locale_status table.
152
+ * When locale === defaultLocale, updates the base entity status instead.
153
+ */
154
+ setLocaleStatus(entityId: string, locale: string, status: 'draft' | 'published'): Promise<void>;
155
+ /**
156
+ * Get per-locale publish status for multiple entities in a single query.
157
+ * Returns a map of entityId → locale → LocalePublishStatus.
158
+ */
159
+ getLocaleStatusBatch(entityIds: string[]): Promise<Map<string, Record<string, LocalePublishStatus>>>;
160
+ /**
161
+ * Save (upsert) a draft for an entity+locale.
162
+ * One draft per entity+locale — overwrites any existing draft.
163
+ */
164
+ saveDraft(entityId: string, data: Record<string, unknown>, locale?: string, user?: {
165
+ id: string;
166
+ name?: string;
167
+ }): Promise<DraftEntry>;
168
+ /**
169
+ * Get the draft for an entity+locale, or null if none exists.
170
+ */
171
+ getDraft(entityId: string, locale?: string): Promise<DraftEntry | null>;
172
+ /**
173
+ * Delete (discard) a draft for an entity+locale.
174
+ */
175
+ deleteDraft(entityId: string, locale?: string): Promise<boolean>;
176
+ /**
177
+ * Publish a draft: apply draft data to the live entity, then delete the draft.
178
+ * Runs in a transaction to ensure atomicity.
179
+ */
180
+ publishDraft(entityId: string, locale?: string, user?: {
181
+ id: string;
182
+ name?: string;
183
+ }): Promise<Record<string, unknown>>;
184
+ /**
185
+ * Quick existence check for list badges — does a draft exist?
186
+ */
187
+ hasDraft(entityId: string, locale?: string): Promise<boolean>;
188
+ /**
189
+ * Check draft existence for multiple entities in one query.
190
+ * Returns a set of entity IDs that have drafts.
191
+ */
192
+ hasDraftBatch(entityIds: string[], locale?: string): Promise<Set<string>>;
193
+ /**
194
+ * Create a version snapshot for an entity after an external update.
195
+ * Use this when the update was performed by AdminClient directly
196
+ * (e.g., the admin API handler) rather than through ContentClient.update().
197
+ *
198
+ * @param locale - The locale being edited. Use undefined or '_' for the default locale.
199
+ * @param user - The user who made the change (id + name for display).
200
+ */
201
+ createVersionSnapshot(entityId: string, currentData: Record<string, unknown>, previousData?: Record<string, unknown> | null, locale?: string, user?: {
202
+ id: string;
203
+ name?: string;
204
+ }): Promise<void>;
205
+ private saveVersionSnapshot;
206
+ private toDraftEntry;
207
+ private calculateDelta;
208
+ }
209
+ /**
210
+ * Factory — creates a ContentClient for an entity.
211
+ * Must be called after createApp().
212
+ */
213
+ declare function createContentClient(entity: Entity): Promise<ContentClient>;
214
+
215
+ export { ContentClient, type ContentClientConfig, createContentClient };
package/dist/client.js ADDED
@@ -0,0 +1 @@
1
+ import{b as v,e as p,g as b}from"./chunk-BVMZMEPV.js";import"server-only";var R=class{entity;db;logger;admin=null;versionableOptions;constructor(t){this.entity=t.entity,this.db=t.db,this.logger=t.logger,this.versionableOptions=v(t.entity.behaviors)}async getAdmin(){if(!this.admin){let{AdminClient:t}=await import("@murumets-ee/entity/admin");this.admin=new t({entity:this.entity,db:this.db,logger:this.logger})}return this.admin}async create(t){let i=await(await this.getAdmin()).create(t);return this.versionableOptions&&await this.saveVersionSnapshot(i.id,i,null,!1),i}async findById(t,e){return(await this.getAdmin()).findById(t,e)}async findMany(t){return(await this.getAdmin()).findMany(t)}async update(t,e){let i=await this.getAdmin(),n=null;this.versionableOptions&&(n=await i.findById(t));let o=await i.update(t,e);if(this.versionableOptions&&n){let s=this.calculateDelta(n,o),r=await this.saveVersionSnapshot(t,o,s,!1);if(r!==void 0){let{schemaRegistry:a}=await import("@murumets-ee/db"),{eq:c}=await import("drizzle-orm"),d=a.get(this.entity.name);d&&(await this.db.update(d).set({_version:r}).where(c(d.id,t)),o._version=r)}await this.pruneVersions(t)}return o}async delete(t){return(await this.getAdmin()).delete(t)}async saveTranslation(t,e,i){return(await this.getAdmin()).saveTranslation(t,e,i)}async deleteTranslation(t,e){return(await this.getAdmin()).deleteTranslation(t,e)}async copyLayout(t,e){let{schemaRegistry:i}=await import("@murumets-ee/db"),{eq:n,and:o}=await import("drizzle-orm"),s=i.get(`${this.entity.name}_layout`);if(!s)return;let r=Array.isArray(e.to)?e.to:[e.to],a=await this.db.select().from(s).where(o(n(s.entityId,t),n(s.locale,e.from))).orderBy(s.sortOrder);if(a.length!==0){for(let c of r){await this.db.delete(s).where(o(n(s.entityId,t),n(s.locale,c)));for(let d of a)await this.db.insert(s).values({entityId:d.entityId,fieldName:d.fieldName,blockType:d.blockType,sortOrder:d.sortOrder,data:d.data,locale:c})}this.logger?.debug({entityId:t,from:e.from,to:r},"Layout copied between locales")}}async getVersions(t,e){let{schemaRegistry:i}=await import("@murumets-ee/db"),{eq:n,and:o,desc:s}=await import("drizzle-orm"),r=i.get(`${this.entity.name}_versions`);if(!r)return[];let a=e?.locale??"_",c=this.db.select().from(r).where(o(n(r.entityId,t),n(r.locale,a))).orderBy(s(r.version)).$dynamic();return e?.limit&&(c=c.limit(e.limit)),e?.offset&&(c=c.offset(e.offset)),c}async getVersion(t,e,i="_"){let{schemaRegistry:n}=await import("@murumets-ee/db"),{eq:o,and:s}=await import("drizzle-orm"),r=n.get(`${this.entity.name}_versions`);return r?(await this.db.select().from(r).where(s(o(r.entityId,t),o(r.version,e),o(r.locale,i))).limit(1))[0]??null:null}async compareVersions(t,e,i,n="_"){let[o,s]=await Promise.all([this.getVersion(t,e,n),this.getVersion(t,i,n)]);if(!o||!s)return null;let r=o.data,a=s.data,c=this.entity.allFields,d=p(r,a,c);for(let l of d)l.type==="blocks"&&Array.isArray(l.from)&&Array.isArray(l.to)&&(l.blockDiffs=b(l.from,l.to));return{diffs:d,fromVersion:o,toVersion:s}}async compareDraftWithCurrent(t,e="_"){let i=await this.getDraft(t,e);if(!i)return null;let n=await this.getAdmin(),{getContentConfig:o}=await import("./plugin.js"),r=o().defaultLocale,a=e==="_"?{defaultLocale:r}:{locale:e,defaultLocale:r},c=await n.findById(t,a);if(!c)return null;let d=this.entity.allFields,l={};for(let f of Object.keys(i.data))l[f]=c[f];let u=p(l,i.data,d);for(let f of u)f.type==="blocks"&&Array.isArray(f.from)&&Array.isArray(f.to)&&(f.blockDiffs=b(f.from,f.to));return{diffs:u,current:c,draft:i}}async restoreVersion(t,e){let i=await this.getVersion(t,e);if(!i)throw new Error(`Version ${e} not found for entity ${t}`);let n=i.data,{id:o,createdAt:s,createdBy:r,_version:a,_versionedAt:c,...d}=n;return(await this.getAdmin()).update(t,d)}async pruneVersions(t,e="_"){if(!this.versionableOptions)return;let{schemaRegistry:i}=await import("@murumets-ee/db"),{eq:n,and:o,desc:s,sql:r}=await import("drizzle-orm"),a=i.get(`${this.entity.name}_versions`);if(!a)return;let c=this.versionableOptions.maxPerDoc,d=await this.db.select({id:a.id}).from(a).where(o(n(a.entityId,t),n(a.locale,e))).orderBy(s(a.version)).limit(c);if(d.length<c)return;let l=d.map(u=>u.id);await this.db.delete(a).where(r`${a.entityId} = ${t} AND ${a.locale} = ${e} AND ${a.id} NOT IN (${r.join(l.map(u=>r`${u}`),r`, `)})`)}async getTranslationStatus(t){let{getContentConfig:e}=await import("./plugin.js"),i=e(),n=Object.entries(this.entity.allFields).filter(([l,u])=>u.translatable).map(([l])=>l);if(n.length===0)return{};let{schemaRegistry:o}=await import("@murumets-ee/db"),{eq:s}=await import("drizzle-orm"),r=o.get(`${this.entity.name}_translations`);if(!r)return{};let a=await this.db.select().from(r).where(s(r.entityId,t)),c=new Map;for(let l of a){let u=l,f={};for(let g of n)f[g]=u[g];c.set(u.locale,f)}let d={};for(let l of i.locales){if(l.code===i.defaultLocale)continue;let u=c.get(l.code)??{},f=[];for(let m of n)(u[m]===void 0||u[m]===null)&&f.push(m);let g=n.length-f.length;d[l.code]={complete:f.length===0,total:n.length,translated:g,missing:f}}return d}async getTranslationStatusBatch(t){let e=new Map;if(t.length===0)return e;let{getContentConfig:i}=await import("./plugin.js"),n=i(),o=Object.entries(this.entity.allFields).filter(([l,u])=>u.translatable).map(([l])=>l);if(o.length===0)return e;let{schemaRegistry:s}=await import("@murumets-ee/db"),{inArray:r}=await import("drizzle-orm"),a=s.get(`${this.entity.name}_translations`);if(!a)return e;let c=await this.db.select().from(a).where(r(a.entityId,t)),d=new Map;for(let l of c){let u=l,f=u.entityId,g=u.locale,m={};for(let h of o)m[h]=u[h];d.has(f)||d.set(f,new Map),d.get(f).set(g,m)}for(let l of t){let u=d.get(l),f={};for(let g of n.locales){if(g.code===n.defaultLocale)continue;let m=u?.get(g.code)??{},h=[];for(let w of o)(m[w]===void 0||m[w]===null)&&h.push(w);let A=o.length-h.length;f[g.code]={complete:h.length===0,total:o.length,translated:A,missing:h}}e.set(l,f)}return e}async getTranslationReport(t){let{getContentConfig:e}=await import("./plugin.js"),i=e(),n=t?.locale??i.locales.find(d=>d.code!==i.defaultLocale)?.code;if(!n)return{total:0,complete:0,partial:0,missing:0};let s=await(await this.getAdmin()).findMany(),r=0,a=0,c=0;for(let d of s){let u=(await this.getTranslationStatus(d.id))[n];!u||u.translated===0?c++:u.complete?r++:a++}return{total:s.length,complete:r,partial:a,missing:c}}async getLocaleStatus(t){let{schemaRegistry:e}=await import("@murumets-ee/db"),{eq:i}=await import("drizzle-orm"),n=e.get(`${this.entity.name}_locale_status`);return n?(await this.db.select().from(n).where(i(n.entityId,t))).map(s=>({locale:s.locale,status:s.status,publishedAt:s.publishedAt??null})):[]}async setLocaleStatus(t,e,i){let{getContentConfig:n}=await import("./plugin.js"),o=n();if(e===o.defaultLocale){let u=await this.getAdmin(),f={status:i};i==="published"&&(f.publishedAt=new Date),await u.update(t,f);return}let{schemaRegistry:s}=await import("@murumets-ee/db"),{eq:r,and:a}=await import("drizzle-orm"),c=s.get(`${this.entity.name}_locale_status`);if(!c)return;let d=i==="published"?new Date:null,[l]=await this.db.select({id:c.id}).from(c).where(a(r(c.entityId,t),r(c.locale,e))).limit(1);l?await this.db.update(c).set({status:i,publishedAt:d}).where(r(c.id,l.id)):await this.db.insert(c).values({entityId:t,locale:e,status:i,publishedAt:d})}async getLocaleStatusBatch(t){let e=new Map;if(t.length===0)return e;let{schemaRegistry:i}=await import("@murumets-ee/db"),{inArray:n}=await import("drizzle-orm"),o=i.get(`${this.entity.name}_locale_status`);if(!o)return e;let s=await this.db.select().from(o).where(n(o.entityId,t));for(let r of s){let a=r.entityId,c=r.locale;e.has(a)||e.set(a,{}),e.get(a)[c]={locale:c,status:r.status,publishedAt:r.publishedAt??null}}return e}async saveDraft(t,e,i="_",n){let{schemaRegistry:o}=await import("@murumets-ee/db"),{eq:s,and:r}=await import("drizzle-orm"),a=o.get(`${this.entity.name}_drafts`);if(!a)throw new Error(`Drafts table not found for entity "${this.entity.name}". Is the content plugin initialized?`);let c=new Date,d=n?.id??"",l=n?.name??null,[u]=await this.db.select().from(a).where(r(s(a.entityId,t),s(a.locale,i))).limit(1);if(u){let[g]=await this.db.update(a).set({data:e,updatedAt:c}).where(s(a.id,u.id)).returning();return this.toDraftEntry(g)}let[f]=await this.db.insert(a).values({entityId:t,locale:i,data:e,createdBy:d,createdByName:l,createdAt:c,updatedAt:c}).returning();return this.toDraftEntry(f)}async getDraft(t,e="_"){let{schemaRegistry:i}=await import("@murumets-ee/db"),{eq:n,and:o}=await import("drizzle-orm"),s=i.get(`${this.entity.name}_drafts`);if(!s)return null;let[r]=await this.db.select().from(s).where(o(n(s.entityId,t),n(s.locale,e))).limit(1);return r?this.toDraftEntry(r):null}async deleteDraft(t,e="_"){let{schemaRegistry:i}=await import("@murumets-ee/db"),{eq:n,and:o}=await import("drizzle-orm"),s=i.get(`${this.entity.name}_drafts`);return s?(await this.db.delete(s).where(o(n(s.entityId,t),n(s.locale,e))).returning({id:s.id})).length>0:!1}async publishDraft(t,e="_",i){let n=await this.getDraft(t,e);if(!n)throw new Error(`No draft found for entity ${t} locale ${e}`);let o=await this.getAdmin(),s=null;this.versionableOptions&&(s=await o.findById(t));let r=n.data,{id:a,createdAt:c,createdBy:d,_version:l,_versionedAt:u,...f}=r,g=await o.update(t,f);return await this.deleteDraft(t,e),this.versionableOptions&&await this.createVersionSnapshot(t,g,s,e,i),g}async hasDraft(t,e="_"){let{schemaRegistry:i}=await import("@murumets-ee/db"),{eq:n,and:o}=await import("drizzle-orm"),s=i.get(`${this.entity.name}_drafts`);if(!s)return!1;let[r]=await this.db.select({id:s.id}).from(s).where(o(n(s.entityId,t),n(s.locale,e))).limit(1);return!!r}async hasDraftBatch(t,e="_"){let i=new Set;if(t.length===0)return i;let{schemaRegistry:n}=await import("@murumets-ee/db"),{inArray:o,eq:s,and:r}=await import("drizzle-orm"),a=n.get(`${this.entity.name}_drafts`);if(!a)return i;let c=await this.db.select({entityId:a.entityId}).from(a).where(r(o(a.entityId,t),s(a.locale,e)));for(let d of c)i.add(d.entityId);return i}async createVersionSnapshot(t,e,i,n,o){if(!this.versionableOptions)return;let s=n??"_",r=i?this.calculateDelta(i,e):null,a=await this.saveVersionSnapshot(t,e,r,!1,s,o);if(a!==void 0&&s==="_"){let{schemaRegistry:c}=await import("@murumets-ee/db"),{eq:d}=await import("drizzle-orm"),l=c.get(this.entity.name);l&&await this.db.update(l).set({_version:a}).where(d(l.id,t))}await this.pruneVersions(t,s)}async saveVersionSnapshot(t,e,i,n,o="_",s){let{schemaRegistry:r}=await import("@murumets-ee/db"),{eq:a,and:c,sql:d}=await import("drizzle-orm"),l=r.get(`${this.entity.name}_versions`);if(!l)return;let[u]=await this.db.select({maxVersion:d`coalesce(max(${l.version}), 0)`}).from(l).where(c(a(l.entityId,t),a(l.locale,o))),f=(u?.maxVersion??0)+1,g=e.status??null;if(n){let[m]=await this.db.select().from(l).where(c(a(l.entityId,t),a(l.isAutosave,!0),a(l.version,f),a(l.locale,o))).limit(1);if(m)return await this.db.update(l).set({data:e,delta:i,status:g,createdAt:new Date}).where(a(l.id,m.id)),f}return await this.db.insert(l).values({entityId:t,version:f,locale:o,data:e,delta:i,status:g,createdBy:s?.id??e.updatedBy??e.createdBy??null,createdByName:s?.name??null,isAutosave:n}),f}toDraftEntry(t){return{id:t.id,entityId:t.entityId,locale:t.locale,data:t.data,createdBy:t.createdBy,createdByName:t.createdByName??null,createdAt:t.createdAt,updatedAt:t.updatedAt}}calculateDelta(t,e){let i={};for(let n of Object.keys(e))n==="id"||n.startsWith("_")||JSON.stringify(t[n])!==JSON.stringify(e[n])&&(i[n]=e[n]);return i}};async function T(y){let{getApp:t}=await import("@murumets-ee/core"),e=t();return new R({entity:y,db:e.db.readWrite,logger:e.logger.child({content:y.name})})}export{R as ContentClient,T as createContentClient};
@@ -0,0 +1 @@
1
+ import{jsonb as o,pgTable as u,timestamp as a,unique as r,uuid as n,varchar as t}from"drizzle-orm/pg-core";function m(l){let d=`${l.name}_drafts`;return u(d,{id:n("id").primaryKey().defaultRandom(),entityId:n("entity_id").notNull(),locale:t("locale",{length:10}).notNull().default("_"),data:o("data").notNull(),createdBy:t("created_by",{length:255}).notNull(),createdByName:t("created_by_name",{length:255}),createdAt:a("created_at",{withTimezone:!0}).notNull().defaultNow(),updatedAt:a("updated_at",{withTimezone:!0}).notNull().defaultNow()},e=>({uniqueEntityLocale:r().on(e.entityId,e.locale)}))}export{m as generateDraftsSchema};
@@ -0,0 +1,142 @@
1
+ import { Behavior, NumberField, DateField, SelectField, TextField, FieldConfig } from '@murumets-ee/entity';
2
+ import { b as VersionableOptions, B as BlockDefinition } from './types-BYeiE1ie.js';
3
+ export { C as ContentConfig, D as DraftEntry, I as InferBlockData, c as InferBlocksUnion, d as LocaleConfig, e as LockInfo, f as TranslationFieldStatus, a as TranslationReport, T as TranslationStatus, V as VersionEntry } from './types-BYeiE1ie.js';
4
+ import { B as BlockDiffEntry, F as FieldDiff, D as DiffSegment } from './types-Cr0ifLn_.js';
5
+ export { a as BlockDiffStatus, P as PropDiff } from './types-Cr0ifLn_.js';
6
+
7
+ /**
8
+ * Versionable behavior — full version history with snapshots.
9
+ *
10
+ * Extends the concept of revisionable() (which only tracks a counter)
11
+ * with actual version snapshots stored in {entity}_versions table.
12
+ *
13
+ * The behavior itself adds fields + hooks for version tracking.
14
+ * Actual snapshot persistence happens in ContentClient (which has DB access).
15
+ *
16
+ * Options:
17
+ * - maxPerDoc: Maximum versions kept per document (default: 50)
18
+ * - autosave: Enable smart autosave (default: false)
19
+ */
20
+
21
+ type VersionableFields = {
22
+ _version: NumberField & {
23
+ required: true;
24
+ };
25
+ _versionedAt: DateField;
26
+ };
27
+ declare function versionable(options?: VersionableOptions): Behavior<VersionableFields>;
28
+ /**
29
+ * Extract versionable options from an entity's behaviors
30
+ */
31
+ declare function getVersionableOptions(behaviors?: Behavior[]): Required<VersionableOptions> | null;
32
+
33
+ /**
34
+ * Workflowable behavior — editorial workflow with approval gates.
35
+ *
36
+ * Adds workflow status tracking fields to an entity. Works alongside
37
+ * `publishable()` — the workflow status tracks the editorial pipeline,
38
+ * while publishable's `status` remains the public-facing state.
39
+ *
40
+ * Key invariant: `status` can only transition to 'published' when
41
+ * `_workflowStatus === 'approved'` (unless the user has direct `publish`
42
+ * permission, which bypasses the workflow).
43
+ *
44
+ * Workflow states:
45
+ * - draft → in_review (submit for review, requires 'update')
46
+ * - in_review → approved (approve, requires 'publish')
47
+ * - in_review → rejected (reject with note, requires 'publish')
48
+ * - approved → published (publish, requires 'publish')
49
+ * - rejected → in_review (resubmit, requires 'update')
50
+ * - published → draft (unpublish, requires 'publish')
51
+ */
52
+
53
+ declare const WORKFLOW_STATES: readonly ["draft", "in_review", "approved", "rejected", "published"];
54
+ type WorkflowState = (typeof WORKFLOW_STATES)[number];
55
+ type WorkflowableFields = {
56
+ _workflowStatus: SelectField & {
57
+ required: true;
58
+ };
59
+ _workflowAssignee: TextField;
60
+ _workflowNote: TextField;
61
+ _workflowUpdatedAt: DateField;
62
+ };
63
+ declare function workflowable(): Behavior<WorkflowableFields>;
64
+
65
+ /**
66
+ * Block definition API
67
+ *
68
+ * defineBlock() creates a reusable content block with typed fields.
69
+ * Uses `const F` generic to preserve literal field types for inference.
70
+ *
71
+ * Blocks are NOT entities — they lack independent identity, CRUD, and access control.
72
+ * They exist only as embedded data within a parent entity's blocks field.
73
+ *
74
+ * @example
75
+ * const HeroBlock = defineBlock({
76
+ * slug: 'hero',
77
+ * label: 'Hero Section',
78
+ * fields: {
79
+ * heading: field.text({ required: true, translatable: true }),
80
+ * background: field.media({ accept: ['image/*'] }),
81
+ * },
82
+ * })
83
+ */
84
+
85
+ declare function defineBlock<const F extends Record<string, FieldConfig>>(definition: {
86
+ slug: string;
87
+ label: string;
88
+ fields: F;
89
+ }): BlockDefinition<F>;
90
+
91
+ /**
92
+ * Structural diff for Puck block arrays.
93
+ *
94
+ * Blocks are identified by `_id`. The algorithm detects:
95
+ * - Added blocks (new _id in target)
96
+ * - Removed blocks (missing _id in target)
97
+ * - Moved blocks (same _id, different position)
98
+ * - Changed blocks (same _id, different prop values)
99
+ * - Unchanged blocks
100
+ */
101
+
102
+ /**
103
+ * Compute structural diff between two block arrays.
104
+ *
105
+ * Each block is identified by its `_id` field. Position changes,
106
+ * additions, removals, and prop-level changes are all detected.
107
+ */
108
+ declare function diffBlocks(fromBlocks: unknown[], toBlocks: unknown[]): BlockDiffEntry[];
109
+
110
+ /**
111
+ * Field-level diff — dispatches to the appropriate diff strategy per field type.
112
+ */
113
+
114
+ /**
115
+ * Compute diff for a single field, using the field's type to determine
116
+ * the best diff strategy.
117
+ */
118
+ declare function diffField(fieldName: string, from: unknown, to: unknown, fieldConfig?: FieldConfig): FieldDiff | null;
119
+ /**
120
+ * Diff all fields between two version snapshots.
121
+ * Pass fieldConfigs to get type-aware diffs; without them, types are inferred.
122
+ */
123
+ declare function diffFields(from: Record<string, unknown>, to: Record<string, unknown>, fieldConfigs?: Record<string, FieldConfig>): FieldDiff[];
124
+ /** Diff two string values and return segments (used by block-diff for string props). */
125
+ declare function diffStringValues(from: unknown, to: unknown): DiffSegment[] | undefined;
126
+
127
+ /**
128
+ * Word-level text diff using a simplified Myers algorithm.
129
+ *
130
+ * Splits text into words (preserving whitespace), computes the longest common
131
+ * subsequence, and returns an array of diff segments.
132
+ */
133
+
134
+ /**
135
+ * Compute word-level diff between two strings.
136
+ *
137
+ * Uses a simple LCS (dynamic programming) approach which is fine for
138
+ * the text sizes we deal with (entity field values, not source code).
139
+ */
140
+ declare function diffText(from: string, to: string): DiffSegment[];
141
+
142
+ export { BlockDefinition, BlockDiffEntry, DiffSegment, FieldDiff, VersionableOptions, WORKFLOW_STATES, type WorkflowState, type WorkflowableFields, defineBlock, diffBlocks, diffField, diffFields, diffStringValues, diffText, getVersionableOptions, versionable, workflowable };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{a as r,b as f,c as l,d as i,e as a,f as n,g as s}from"./chunk-BVMZMEPV.js";import{a as d}from"./chunk-CULYIGZI.js";import{field as e}from"@murumets-ee/entity";var t=["draft","in_review","approved","rejected","published"];function w(){return{name:"workflowable",fields:{_workflowStatus:e.select({options:t,default:"draft",indexed:!0,required:!0}),_workflowAssignee:e.text({required:!1}),_workflowNote:e.text({required:!1}),_workflowUpdatedAt:e.date()},hooks:{beforeCreate:async o=>(o._workflowStatus="draft",o._workflowUpdatedAt=new Date,o)}}}export{t as WORKFLOW_STATES,d as defineBlock,s as diffBlocks,i as diffField,a as diffFields,n as diffStringValues,l as diffText,f as getVersionableOptions,r as versionable,w as workflowable};
@@ -0,0 +1 @@
1
+ import{pgTable as i,timestamp as u,unique as o,uuid as e,varchar as a}from"drizzle-orm/pg-core";function m(l){let n=`${l.name}_locale_status`;return i(n,{id:e("id").primaryKey().defaultRandom(),entityId:e("entity_id").notNull(),locale:a("locale",{length:10}).notNull(),status:a("status",{length:20}).notNull().default("draft"),publishedAt:u("published_at",{withTimezone:!0})},t=>({uniqueEntityLocale:o().on(t.entityId,t.locale)}))}export{m as generateLocaleStatusSchema};
@@ -0,0 +1,61 @@
1
+ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
2
+ import { e as LockInfo } from './types-BYeiE1ie.js';
3
+ import '@murumets-ee/entity';
4
+
5
+ /**
6
+ * Content lock service — pessimistic locking for entity editing.
7
+ *
8
+ * Prevents concurrent edit conflicts by allowing only one editor at a time
9
+ * per entity+locale combination. Locks expire after 5 minutes and must
10
+ * be renewed via heartbeat.
11
+ */
12
+
13
+ interface LockServiceConfig {
14
+ db: PostgresJsDatabase;
15
+ }
16
+ declare class LockService {
17
+ private db;
18
+ constructor(config: LockServiceConfig);
19
+ private getTable;
20
+ /**
21
+ * Acquire a lock on an entity+locale.
22
+ *
23
+ * - Deletes expired locks first
24
+ * - Attempts INSERT; on conflict checks if same user (re-entrant → renew)
25
+ * - Returns { acquired: true } or { acquired: false, lock: LockInfo }
26
+ */
27
+ acquireLock(entityType: string, entityId: string, locale: string, user: {
28
+ id: string;
29
+ name?: string;
30
+ }): Promise<{
31
+ acquired: true;
32
+ } | {
33
+ acquired: false;
34
+ lock: LockInfo;
35
+ }>;
36
+ /**
37
+ * Renew an existing lock (heartbeat).
38
+ * Only succeeds if the caller holds the lock and it hasn't expired.
39
+ */
40
+ renewLock(entityType: string, entityId: string, locale: string, userId: string): Promise<boolean>;
41
+ /**
42
+ * Release a lock. Only the lock holder can release their own lock.
43
+ */
44
+ releaseLock(entityType: string, entityId: string, locale: string, userId: string): Promise<boolean>;
45
+ /**
46
+ * Force-release a lock regardless of who holds it.
47
+ * Admin-only operation — must be audit-logged by the caller.
48
+ */
49
+ forceReleaseLock(entityType: string, entityId: string, locale: string): Promise<boolean>;
50
+ /**
51
+ * Get the current lock for an entity+locale.
52
+ * Returns null if no lock or lock has expired.
53
+ */
54
+ getLock(entityType: string, entityId: string, locale: string): Promise<LockInfo | null>;
55
+ }
56
+ /**
57
+ * Factory — creates a LockService using the app's DB connection.
58
+ */
59
+ declare function createLockService(): Promise<LockService>;
60
+
61
+ export { LockService, type LockServiceConfig, createLockService };
@@ -0,0 +1 @@
1
+ import"server-only";var p=5,g=class{db;constructor(r){this.db=r.db}async getTable(){let{schemaRegistry:r}=await import("@murumets-ee/db");return r.get("toolkit_content_locks")}async acquireLock(r,o,c,s){let{eq:t,and:a,lt:i}=await import("drizzle-orm"),e=await this.getTable();if(!e)return{acquired:!0};let n=new Date,d=new Date(n.getTime()+p*60*1e3);await this.db.delete(e).where(a(t(e.entityType,r),t(e.entityId,o),t(e.locale,c),i(e.expiresAt,n)));let[l]=await this.db.select().from(e).where(a(t(e.entityType,r),t(e.entityId,o),t(e.locale,c))).limit(1);return l?l.lockedBy===s.id?(await this.db.update(e).set({expiresAt:d,lockedAt:n}).where(t(e.id,l.id)),{acquired:!0}):{acquired:!1,lock:{lockedBy:l.lockedBy,lockedByName:l.lockedByName??null,lockedAt:l.lockedAt,expiresAt:l.expiresAt}}:(await this.db.insert(e).values({entityType:r,entityId:o,locale:c,lockedBy:s.id,lockedByName:s.name??null,lockedAt:n,expiresAt:d}),{acquired:!0})}async renewLock(r,o,c,s){let{eq:t,and:a,gte:i}=await import("drizzle-orm"),e=await this.getTable();if(!e)return!0;let n=new Date,d=new Date(n.getTime()+p*60*1e3);return(await this.db.update(e).set({expiresAt:d}).where(a(t(e.entityType,r),t(e.entityId,o),t(e.locale,c),t(e.lockedBy,s),i(e.expiresAt,n))).returning({id:e.id})).length>0}async releaseLock(r,o,c,s){let{eq:t,and:a}=await import("drizzle-orm"),i=await this.getTable();return i?(await this.db.delete(i).where(a(t(i.entityType,r),t(i.entityId,o),t(i.locale,c),t(i.lockedBy,s))).returning({id:i.id})).length>0:!0}async forceReleaseLock(r,o,c){let{eq:s,and:t}=await import("drizzle-orm"),a=await this.getTable();return a?(await this.db.delete(a).where(t(s(a.entityType,r),s(a.entityId,o),s(a.locale,c))).returning({id:a.id})).length>0:!0}async getLock(r,o,c){let{eq:s,and:t,gte:a}=await import("drizzle-orm"),i=await this.getTable();if(!i)return null;let e=new Date,[n]=await this.db.select().from(i).where(t(s(i.entityType,r),s(i.entityId,o),s(i.locale,c),a(i.expiresAt,e))).limit(1);return n?{lockedBy:n.lockedBy,lockedByName:n.lockedByName??null,lockedAt:n.lockedAt,expiresAt:n.expiresAt}:null}};async function y(){let{getApp:u}=await import("@murumets-ee/core"),r=u();return new g({db:r.db.readWrite})}export{g as LockService,y as createLockService};
@@ -0,0 +1 @@
1
+ import{pgTable as o,timestamp as l,unique as n,uuid as i,varchar as t}from"drizzle-orm/pg-core";var d=o("toolkit_content_locks",{id:i("id").primaryKey().defaultRandom(),entityType:t("entity_type",{length:100}).notNull(),entityId:t("entity_id",{length:255}).notNull(),locale:t("locale",{length:10}).notNull().default("_"),lockedBy:t("locked_by",{length:255}).notNull(),lockedByName:t("locked_by_name",{length:255}),lockedAt:l("locked_at",{withTimezone:!0}).notNull().defaultNow(),expiresAt:l("expires_at",{withTimezone:!0}).notNull()},e=>({uniqueEntityLock:n().on(e.entityType,e.entityId,e.locale)}));export{d as toolkitContentLocks};
@@ -0,0 +1,33 @@
1
+ import { Plugin } from '@murumets-ee/core';
2
+ import { C as ContentConfig } from './types-BYeiE1ie.js';
3
+ import '@murumets-ee/entity';
4
+
5
+ /**
6
+ * Content plugin — registers locale configuration and version schemas.
7
+ *
8
+ * Usage:
9
+ * import { content } from '@murumets-ee/content/plugin'
10
+ *
11
+ * export default defineConfig({
12
+ * plugins: [
13
+ * content({
14
+ * locales: [
15
+ * { code: 'en', label: 'English' },
16
+ * { code: 'de', label: 'German' },
17
+ * ],
18
+ * defaultLocale: 'en',
19
+ * }),
20
+ * ],
21
+ * })
22
+ */
23
+
24
+ /**
25
+ * Get the content configuration. Throws if plugin not initialized.
26
+ */
27
+ declare function getContentConfig(): ContentConfig;
28
+ /**
29
+ * Content plugin factory.
30
+ */
31
+ declare function content(config: ContentConfig): Plugin;
32
+
33
+ export { content, getContentConfig };
package/dist/plugin.js ADDED
@@ -0,0 +1 @@
1
+ var r=null;function v(){if(!r)throw new Error("@murumets-ee/content plugin not initialized. Add content() to your plugins array.");return r}function E(a){if(!a.locales||a.locales.length===0)throw new Error("content() requires at least one locale");if(!a.defaultLocale)throw new Error("content() requires a defaultLocale");if(!a.locales.some(l=>l.code===a.defaultLocale))throw new Error(`defaultLocale "${a.defaultLocale}" not found in locales: [${a.locales.map(l=>l.code).join(", ")}]`);return{name:"@murumets-ee/content",init:async l=>{r=a;let{schemaRegistry:e}=await import("@murumets-ee/db"),{generateVersionsSchema:d}=await import("./versions-4ICOJIK2.js"),{generateLocaleStatusSchema:g}=await import("./locale-status-B7B3WFXF.js"),{generateDraftsSchema:p}=await import("./drafts-2ZRAHAUK.js"),{toolkitContentLocks:y}=await import("./locks-AXFCTV7F.js"),{generateLayoutSchema:C,generateLayoutTranslationSchema:w}=await import("@murumets-ee/entity"),c=0,u=0,m=0,f=0;e.has("toolkit_content_locks")||e.register("toolkit_content_locks",y);for(let[n,o]of l.entities){if(o.behaviors?.some(t=>t.name==="versionable")){let t=`${n}_versions`;if(!e.has(t)){let s=d(o);e.register(t,s)}c++}let h=o.allFields;if(Object.values(h).some(t=>t.type==="blocks")){let t=`${n}_layout`;if(!e.has(t)){let i=C(o);i&&e.register(t,i)}let s=`${n}_layout_translations`;if(!e.has(s)){let i=w(o);i&&e.register(s,i)}u++}let b=o.behaviors?.some(t=>t.name==="publishable"),S=Object.values(h).some(t=>t.translatable);if(b&&S){let t=`${n}_locale_status`;if(!e.has(t)){let s=g(o);e.register(t,s)}m++}if(b){let t=`${n}_drafts`;if(!e.has(t)){let s=p(o);e.register(t,s)}f++}}l.logger.info({locales:a.locales.map(n=>n.code),defaultLocale:a.defaultLocale,versionableEntities:c,blockEntities:u,localeStatusEntities:m,draftEntities:f},"Content plugin initialized")}}}export{E as content,v as getContentConfig};
@@ -0,0 +1,67 @@
1
+ import { Logger } from '@murumets-ee/core';
2
+ import { Entity } from '@murumets-ee/entity';
3
+ import { FindByIdOptions, FindManyOptions, CountOptions } from '@murumets-ee/entity/query';
4
+ import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
5
+
6
+ /**
7
+ * ContentQueryClient — read-only content queries, safe for client bundles.
8
+ *
9
+ * Usage:
10
+ * import { createContentQueryClient } from '@murumets-ee/content/query'
11
+ * const pages = createContentQueryClient(Page)
12
+ * const page = await pages.findBySlug('about', { locale: 'de' })
13
+ */
14
+
15
+ interface ContentQueryClientConfig {
16
+ entity: Entity;
17
+ db: PostgresJsDatabase;
18
+ logger?: Logger;
19
+ }
20
+ declare class ContentQueryClient {
21
+ private entity;
22
+ private db;
23
+ private logger?;
24
+ private query;
25
+ constructor(config: ContentQueryClientConfig);
26
+ private getQuery;
27
+ findById(id: string, options?: FindByIdOptions): Promise<({
28
+ id: string;
29
+ } & {} & {
30
+ [x: string]: string | number | boolean | Record<string, unknown> | Date | Record<string, unknown>[] | {
31
+ [key: string]: unknown;
32
+ _block: string;
33
+ _id: string;
34
+ }[] | null | undefined;
35
+ }) | null>;
36
+ /**
37
+ * Find entity by slug — convenience for slug-based lookups.
38
+ */
39
+ findBySlug(slug: string, options?: {
40
+ locale?: string;
41
+ }): Promise<({
42
+ id: string;
43
+ } & {} & {
44
+ [x: string]: string | number | boolean | Record<string, unknown> | Date | Record<string, unknown>[] | {
45
+ [key: string]: unknown;
46
+ _block: string;
47
+ _id: string;
48
+ }[] | null | undefined;
49
+ }) | null>;
50
+ findMany(options?: FindManyOptions): Promise<({
51
+ id: string;
52
+ } & {} & {
53
+ [x: string]: string | number | boolean | Record<string, unknown> | Date | Record<string, unknown>[] | {
54
+ [key: string]: unknown;
55
+ _block: string;
56
+ _id: string;
57
+ }[] | null | undefined;
58
+ })[]>;
59
+ count(options?: CountOptions): Promise<number>;
60
+ }
61
+ /**
62
+ * Factory — creates a ContentQueryClient for an entity.
63
+ * Must be called after createApp().
64
+ */
65
+ declare function createContentQueryClient(entity: Entity): Promise<ContentQueryClient>;
66
+
67
+ export { ContentQueryClient, type ContentQueryClientConfig, createContentQueryClient };
@@ -0,0 +1 @@
1
+ var i=class{entity;db;logger;query=null;constructor(t){this.entity=t.entity,this.db=t.db,this.logger=t.logger}async getQuery(){if(!this.query){let{QueryClient:t}=await import("@murumets-ee/entity/query");this.query=new t({entity:this.entity,db:this.db,logger:this.logger})}return this.query}async findById(t,e){return(await this.getQuery()).findById(t,e)}async findBySlug(t,e){let{schemaRegistry:s}=await import("@murumets-ee/db"),{eq:o}=await import("drizzle-orm"),n=s.get(this.entity.name);return!n||!n.slug?null:(await(await this.getQuery()).findMany({where:o(n.slug,t),limit:1,locale:e?.locale}))[0]??null}async findMany(t){return(await this.getQuery()).findMany(t)}async count(t){return(await this.getQuery()).count(t)}};async function u(r){let{getApp:t}=await import("@murumets-ee/core"),e=t();return new i({entity:r,db:e.db.readOnly,logger:e.logger.child({content:r.name})})}export{i as ContentQueryClient,u as createContentQueryClient};
@@ -0,0 +1,99 @@
1
+ import { FieldConfig, InferEntityDTO } from '@murumets-ee/entity';
2
+
3
+ /**
4
+ * Content package types
5
+ */
6
+
7
+ /**
8
+ * A block definition — describes a reusable content block's data shape.
9
+ *
10
+ * @typeParam F - The literal field map, inferred from the call site.
11
+ */
12
+ interface BlockDefinition<F extends Record<string, FieldConfig> = Record<string, FieldConfig>> {
13
+ slug: string;
14
+ label: string;
15
+ fields: F;
16
+ }
17
+ /**
18
+ * Infer the data shape of a single block definition.
19
+ * Produces `{ _block: 'hero'; _id: string; heading: string; ... }`
20
+ */
21
+ type InferBlockData<B extends BlockDefinition> = B extends BlockDefinition<infer F> ? {
22
+ _block: B['slug'];
23
+ _id: string;
24
+ } & Omit<InferEntityDTO<F>, 'id'> : never;
25
+ /**
26
+ * Infer a discriminated union from a tuple of block definitions.
27
+ *
28
+ * @example
29
+ * type Layout = InferBlocksUnion<typeof [HeroBlock, RichTextBlock]>
30
+ * // = { _block: 'hero'; ... } | { _block: 'richtext'; ... }
31
+ */
32
+ type InferBlocksUnion<T extends readonly BlockDefinition[]> = {
33
+ [K in keyof T]: T[K] extends BlockDefinition ? InferBlockData<T[K]> : never;
34
+ }[number];
35
+ interface LocaleConfig {
36
+ code: string;
37
+ label: string;
38
+ }
39
+ interface ContentConfig {
40
+ locales: LocaleConfig[];
41
+ defaultLocale: string;
42
+ }
43
+ interface VersionableOptions {
44
+ /** Maximum versions kept per document. Default: 50 */
45
+ maxPerDoc?: number;
46
+ /** Enable autosave (single mutable autosave version). Default: false */
47
+ autosave?: boolean;
48
+ }
49
+ interface VersionEntry {
50
+ id: string;
51
+ entityId: string;
52
+ version: number;
53
+ locale: string;
54
+ data: Record<string, unknown>;
55
+ delta: Record<string, unknown> | null;
56
+ status: string | null;
57
+ createdBy: string | null;
58
+ createdByName: string | null;
59
+ createdAt: Date | string;
60
+ isAutosave: boolean;
61
+ }
62
+ interface TranslationFieldStatus {
63
+ complete: boolean;
64
+ total: number;
65
+ translated: number;
66
+ missing: string[];
67
+ }
68
+ interface TranslationStatus {
69
+ [locale: string]: TranslationFieldStatus;
70
+ }
71
+ interface TranslationReport {
72
+ total: number;
73
+ complete: number;
74
+ partial: number;
75
+ missing: number;
76
+ }
77
+ interface LocalePublishStatus {
78
+ locale: string;
79
+ status: 'draft' | 'published';
80
+ publishedAt: Date | string | null;
81
+ }
82
+ interface DraftEntry {
83
+ id: string;
84
+ entityId: string;
85
+ locale: string;
86
+ data: Record<string, unknown>;
87
+ createdBy: string;
88
+ createdByName: string | null;
89
+ createdAt: Date | string;
90
+ updatedAt: Date | string;
91
+ }
92
+ interface LockInfo {
93
+ lockedBy: string;
94
+ lockedByName: string | null;
95
+ lockedAt: Date | string;
96
+ expiresAt: Date | string;
97
+ }
98
+
99
+ export type { BlockDefinition as B, ContentConfig as C, DraftEntry as D, InferBlockData as I, LocalePublishStatus as L, TranslationStatus as T, VersionEntry as V, TranslationReport as a, VersionableOptions as b, InferBlocksUnion as c, LocaleConfig as d, LockInfo as e, TranslationFieldStatus as f };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Diff result types for version comparison.
3
+ */
4
+ /** A segment within a word-level text diff. */
5
+ interface DiffSegment {
6
+ type: 'equal' | 'add' | 'remove';
7
+ value: string;
8
+ }
9
+ /** Diff result for a single scalar field. */
10
+ interface FieldDiff {
11
+ field: string;
12
+ type: 'text' | 'number' | 'boolean' | 'date' | 'select' | 'media' | 'richtext' | 'json' | 'blocks' | 'unknown';
13
+ from: unknown;
14
+ to: unknown;
15
+ /** Word-level diff segments (present for text/richtext/slug fields). */
16
+ segments?: DiffSegment[];
17
+ /** Block-level structural diff (present for blocks fields). */
18
+ blockDiffs?: BlockDiffEntry[];
19
+ }
20
+ /** Status of a single block within a structural diff. */
21
+ type BlockDiffStatus = 'added' | 'removed' | 'moved' | 'changed' | 'unchanged';
22
+ /** Diff result for a single block within a blocks field. */
23
+ interface BlockDiffEntry {
24
+ blockId: string;
25
+ blockType: string;
26
+ status: BlockDiffStatus;
27
+ fromIndex?: number;
28
+ toIndex?: number;
29
+ /** Per-prop diffs for changed blocks. */
30
+ propDiffs?: PropDiff[];
31
+ }
32
+ /** Diff for a single prop within a changed block. */
33
+ interface PropDiff {
34
+ field: string;
35
+ from: unknown;
36
+ to: unknown;
37
+ /** Word-level segments for string props. */
38
+ segments?: DiffSegment[];
39
+ }
40
+
41
+ export type { BlockDiffEntry as B, DiffSegment as D, FieldDiff as F, PropDiff as P, BlockDiffStatus as a };
@@ -0,0 +1 @@
1
+ import{boolean as i,integer as r,jsonb as a,pgTable as u,timestamp as d,unique as s,uuid as n,varchar as e}from"drizzle-orm/pg-core";function c(o){let l=`${o.name}_versions`;return u(l,{id:n("id").primaryKey().defaultRandom(),entityId:n("entity_id").notNull(),version:r("version").notNull(),locale:e("locale",{length:10}).notNull().default("_"),data:a("data").notNull(),delta:a("delta"),status:e("status",{length:20}),createdBy:e("created_by",{length:255}),createdByName:e("created_by_name",{length:255}),createdAt:d("created_at",{withTimezone:!0}).notNull().defaultNow(),isAutosave:i("is_autosave").notNull().default(!1)},t=>({uniqueEntityVersionLocale:s().on(t.entityId,t.version,t.locale)}))}export{c as generateVersionsSchema};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@murumets-ee/content",
3
+ "version": "0.1.0",
4
+ "license": "Elastic-2.0",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./client": {
12
+ "types": "./dist/client.d.ts",
13
+ "import": "./dist/client.js"
14
+ },
15
+ "./query": {
16
+ "types": "./dist/query-client.d.ts",
17
+ "import": "./dist/query-client.js"
18
+ },
19
+ "./plugin": {
20
+ "types": "./dist/plugin.d.ts",
21
+ "import": "./dist/plugin.js"
22
+ },
23
+ "./blocks": {
24
+ "types": "./dist/blocks/index.d.ts",
25
+ "import": "./dist/blocks/index.js"
26
+ },
27
+ "./lock": {
28
+ "types": "./dist/lock-service.d.ts",
29
+ "import": "./dist/lock-service.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "dependencies": {
36
+ "drizzle-orm": "^0.45.1",
37
+ "server-only": "^0.0.1",
38
+ "zod": "^3.24.1",
39
+ "@murumets-ee/core": "0.1.0",
40
+ "@murumets-ee/logging": "0.1.0",
41
+ "@murumets-ee/entity": "0.1.0",
42
+ "@murumets-ee/db": "0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.10.5",
46
+ "tsup": "^8.3.5",
47
+ "typescript": "^5.7.3",
48
+ "vitest": "^2.1.8"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "dev": "tsup --watch",
53
+ "test": "vitest"
54
+ }
55
+ }