@murumets-ee/content 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/block-diff-H1AttKiz.mjs +2 -0
  2. package/dist/block-diff-H1AttKiz.mjs.map +1 -0
  3. package/dist/blocks/index.d.mts +197 -0
  4. package/dist/blocks/index.d.mts.map +1 -0
  5. package/dist/blocks/index.mjs +2 -0
  6. package/dist/blocks/index.mjs.map +1 -0
  7. package/dist/client.d.mts +208 -0
  8. package/dist/client.d.mts.map +1 -0
  9. package/dist/client.mjs +2 -0
  10. package/dist/client.mjs.map +1 -0
  11. package/dist/define-block-D-7OpIn5.mjs +2 -0
  12. package/dist/define-block-D-7OpIn5.mjs.map +1 -0
  13. package/dist/drafts-BYyRdmiT.mjs +2 -0
  14. package/dist/drafts-BYyRdmiT.mjs.map +1 -0
  15. package/dist/index.d.mts +71 -0
  16. package/dist/index.d.mts.map +1 -0
  17. package/dist/index.mjs +2 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/locale-status-CGttT38v.mjs +2 -0
  20. package/dist/locale-status-CGttT38v.mjs.map +1 -0
  21. package/dist/lock-service.d.mts +54 -0
  22. package/dist/lock-service.d.mts.map +1 -0
  23. package/dist/lock-service.mjs +2 -0
  24. package/dist/lock-service.mjs.map +1 -0
  25. package/dist/locks-CNxQJieo.mjs +2 -0
  26. package/dist/locks-CNxQJieo.mjs.map +1 -0
  27. package/dist/plugin.d.mts +15 -0
  28. package/dist/plugin.d.mts.map +1 -0
  29. package/dist/plugin.mjs +2 -0
  30. package/dist/plugin.mjs.map +1 -0
  31. package/dist/query-client.d.mts +60 -0
  32. package/dist/query-client.d.mts.map +1 -0
  33. package/dist/query-client.mjs +2 -0
  34. package/dist/query-client.mjs.map +1 -0
  35. package/dist/types-nXbE5Ufa.d.mts +43 -0
  36. package/dist/types-nXbE5Ufa.d.mts.map +1 -0
  37. package/dist/types-wLHyjcWf.d.mts +95 -0
  38. package/dist/types-wLHyjcWf.d.mts.map +1 -0
  39. package/dist/versions-cotph2VG.mjs +2 -0
  40. package/dist/versions-cotph2VG.mjs.map +1 -0
  41. package/package.json +20 -20
  42. package/dist/blocks/index.d.ts +0 -187
  43. package/dist/blocks/index.js +0 -1
  44. package/dist/chunk-BVMZMEPV.js +0 -1
  45. package/dist/chunk-CULYIGZI.js +0 -1
  46. package/dist/client.d.ts +0 -215
  47. package/dist/client.js +0 -1
  48. package/dist/drafts-2ZRAHAUK.js +0 -1
  49. package/dist/index.d.ts +0 -142
  50. package/dist/index.js +0 -1
  51. package/dist/locale-status-B7B3WFXF.js +0 -1
  52. package/dist/lock-service.d.ts +0 -61
  53. package/dist/lock-service.js +0 -1
  54. package/dist/locks-AXFCTV7F.js +0 -1
  55. package/dist/plugin.d.ts +0 -33
  56. package/dist/plugin.js +0 -1
  57. package/dist/query-client.d.ts +0 -67
  58. package/dist/query-client.js +0 -1
  59. package/dist/types-BYeiE1ie.d.ts +0 -99
  60. package/dist/types-Cr0ifLn_.d.ts +0 -41
  61. package/dist/versions-4ICOJIK2.js +0 -1
@@ -0,0 +1,2 @@
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-D-7OpIn5.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-block-D-7OpIn5.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,2 @@
1
+ import{jsonb as e,pgTable as t,timestamp as n,unique as r,uuid as i,varchar as a}from"drizzle-orm/pg-core";function o(o){return t(`${o.name}_drafts`,{id:i(`id`).primaryKey().defaultRandom(),entityId:i(`entity_id`).notNull(),locale:a(`locale`,{length:10}).notNull().default(`_`),data:e(`data`).notNull(),createdBy:a(`created_by`,{length:255}).notNull(),createdByName:a(`created_by_name`,{length:255}),createdAt:n(`created_at`,{withTimezone:!0}).notNull().defaultNow(),updatedAt:n(`updated_at`,{withTimezone:!0}).notNull().defaultNow()},e=>({uniqueEntityLocale:r().on(e.entityId,e.locale)}))}export{o as generateDraftsSchema};
2
+ //# sourceMappingURL=drafts-BYyRdmiT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drafts-BYyRdmiT.mjs","names":[],"sources":["../src/schema/drafts.ts"],"sourcesContent":["/**\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\nimport type { Entity } from '@murumets-ee/entity'\nimport { jsonb, pgTable, timestamp, unique, uuid, varchar } from 'drizzle-orm/pg-core'\n\n/**\n * Generate drafts table schema for a publishable entity.\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 generateDraftsSchema(entity: Entity) {\n const tableName = `${entity.name}_drafts`\n\n return pgTable(\n tableName,\n {\n id: uuid('id').primaryKey().defaultRandom(),\n entityId: uuid('entity_id').notNull(),\n locale: varchar('locale', { length: 10 }).notNull().default('_'),\n data: jsonb('data').notNull(),\n createdBy: varchar('created_by', { length: 255 }).notNull(),\n createdByName: varchar('created_by_name', { length: 255 }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),\n },\n (table) => ({\n uniqueEntityLocale: unique().on(table.entityId, table.locale),\n }),\n )\n}\n"],"mappings":"2GA6BA,SAAgB,EAAqB,EAAgB,CAGnD,OAAO,EAFW,GAAG,EAAO,KAAK,SAI/B,CACE,GAAI,EAAK,KAAK,CAAC,YAAY,CAAC,eAAe,CAC3C,SAAU,EAAK,YAAY,CAAC,SAAS,CACrC,OAAQ,EAAQ,SAAU,CAAE,OAAQ,GAAI,CAAC,CAAC,SAAS,CAAC,QAAQ,IAAI,CAChE,KAAM,EAAM,OAAO,CAAC,SAAS,CAC7B,UAAW,EAAQ,aAAc,CAAE,OAAQ,IAAK,CAAC,CAAC,SAAS,CAC3D,cAAe,EAAQ,kBAAmB,CAAE,OAAQ,IAAK,CAAC,CAC1D,UAAW,EAAU,aAAc,CAAE,aAAc,GAAM,CAAC,CAAC,SAAS,CAAC,YAAY,CACjF,UAAW,EAAU,aAAc,CAAE,aAAc,GAAM,CAAC,CAAC,SAAS,CAAC,YAAY,CAClF,CACA,IAAW,CACV,mBAAoB,GAAQ,CAAC,GAAG,EAAM,SAAU,EAAM,OAAO,CAC9D,EACF"}
@@ -0,0 +1,71 @@
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-wLHyjcWf.mjs";
2
+ import { a as PropDiff, i as FieldDiff, n as BlockDiffStatus, r as DiffSegment, t as BlockDiffEntry } from "./types-nXbE5Ufa.mjs";
3
+ import { Behavior, DateField, FieldConfig, NumberField, SelectField, TextField } from "@murumets-ee/entity";
4
+
5
+ //#region src/behaviors/versionable.d.ts
6
+ type VersionableFields = {
7
+ _version: NumberField & {
8
+ required: true;
9
+ };
10
+ _versionedAt: DateField;
11
+ };
12
+ declare function versionable(options?: VersionableOptions): Behavior<VersionableFields>;
13
+ /**
14
+ * Extract versionable options from an entity's behaviors
15
+ */
16
+ declare function getVersionableOptions(behaviors?: Behavior[]): Required<VersionableOptions> | null;
17
+ //#endregion
18
+ //#region src/behaviors/workflowable.d.ts
19
+ declare const WORKFLOW_STATES: readonly ["draft", "in_review", "approved", "rejected", "published"];
20
+ type WorkflowState = (typeof WORKFLOW_STATES)[number];
21
+ type WorkflowableFields = {
22
+ _workflowStatus: SelectField & {
23
+ required: true;
24
+ };
25
+ _workflowAssignee: TextField;
26
+ _workflowNote: TextField;
27
+ _workflowUpdatedAt: DateField;
28
+ };
29
+ declare function workflowable(): Behavior<WorkflowableFields>;
30
+ //#endregion
31
+ //#region src/define-block.d.ts
32
+ declare function defineBlock<const F extends Record<string, FieldConfig>>(definition: {
33
+ slug: string;
34
+ label: string;
35
+ fields: F;
36
+ }): BlockDefinition<F>;
37
+ //#endregion
38
+ //#region src/diff/block-diff.d.ts
39
+ /**
40
+ * Compute structural diff between two block arrays.
41
+ *
42
+ * Each block is identified by its `_id` field. Position changes,
43
+ * additions, removals, and prop-level changes are all detected.
44
+ */
45
+ declare function diffBlocks(fromBlocks: unknown[], toBlocks: unknown[]): BlockDiffEntry[];
46
+ //#endregion
47
+ //#region src/diff/field-diff.d.ts
48
+ /**
49
+ * Compute diff for a single field, using the field's type to determine
50
+ * the best diff strategy.
51
+ */
52
+ declare function diffField(fieldName: string, from: unknown, to: unknown, fieldConfig?: FieldConfig): FieldDiff | null;
53
+ /**
54
+ * Diff all fields between two version snapshots.
55
+ * Pass fieldConfigs to get type-aware diffs; without them, types are inferred.
56
+ */
57
+ declare function diffFields(from: Record<string, unknown>, to: Record<string, unknown>, fieldConfigs?: Record<string, FieldConfig>): FieldDiff[];
58
+ /** Diff two string values and return segments (used by block-diff for string props). */
59
+ declare function diffStringValues(from: unknown, to: unknown): DiffSegment[] | undefined;
60
+ //#endregion
61
+ //#region src/diff/text-diff.d.ts
62
+ /**
63
+ * Compute word-level diff between two strings.
64
+ *
65
+ * Uses a simple LCS (dynamic programming) approach which is fine for
66
+ * the text sizes we deal with (entity field values, not source code).
67
+ */
68
+ declare function diffText(from: string, to: string): DiffSegment[];
69
+ //#endregion
70
+ export { type BlockDefinition, type BlockDiffEntry, type BlockDiffStatus, type ContentConfig, type DiffSegment, type DraftEntry, type FieldDiff, type InferBlockData, type InferBlocksUnion, type LocaleConfig, type LockInfo, type PropDiff, type TranslationFieldStatus, type TranslationReport, type TranslationStatus, type VersionEntry, type VersionableOptions, WORKFLOW_STATES, type WorkflowState, type WorkflowableFields, defineBlock, diffBlocks, diffField, diffFields, diffStringValues, diffText, getVersionableOptions, versionable, workflowable };
71
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/behaviors/versionable.ts","../src/behaviors/workflowable.ts","../src/define-block.ts","../src/diff/block-diff.ts","../src/diff/field-diff.ts","../src/diff/text-diff.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;;;;iBAoCpD,qBAAA,CAAsB,SAAA,GAAY,QAAA,KAAa,QAAA,CAAS,kBAAA;;;cC1C3D,eAAA;AAAA,KACD,aAAA,WAAwB,eAAA;AAAA,KAExB,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;;;iBCVzB,WAAA,iBAA4B,MAAA,SAAe,WAAA,EAAA,CAAc,UAAA;EACvE,IAAA;EACA,KAAA;EACA,MAAA,EAAQ,CAAA;AAAA,IACN,eAAA,CAAgB,CAAA;;;;;;;;AFEpB;iBGsCgB,UAAA,CAAW,UAAA,aAAuB,QAAA,cAAsB,cAAA;;;AHhDxE;;;;AAAA,iBISgB,SAAA,CACd,SAAA,UACA,IAAA,WACA,EAAA,WACA,WAAA,GAAc,WAAA,GACb,SAAA;;;;;iBAkDa,UAAA,CACd,IAAA,EAAM,MAAA,mBACN,EAAA,EAAI,MAAA,mBACJ,YAAA,GAAe,MAAA,SAAe,WAAA,IAC7B,SAAA;;iBAqCa,gBAAA,CAAiB,IAAA,WAAe,EAAA,YAAc,WAAA;;;;;;;;;iBCrG9C,QAAA,CAAS,IAAA,UAAc,EAAA,WAAa,WAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./block-diff-H1AttKiz.mjs";import{t as s}from"./define-block-D-7OpIn5.mjs";import{field as c}from"@murumets-ee/entity";const l=[`draft`,`in_review`,`approved`,`rejected`,`published`];function u(){return{name:`workflowable`,fields:{_workflowStatus:c.select({options:l,default:`draft`,indexed:!0,required:!0}),_workflowAssignee:c.text({required:!1}),_workflowNote:c.text({required:!1}),_workflowUpdatedAt:c.date()},hooks:{beforeCreate:async e=>(e._workflowStatus=`draft`,e._workflowUpdatedAt=new Date,e)}}}export{l as WORKFLOW_STATES,s as defineBlock,o as diffBlocks,n as diffField,i as diffFields,t as diffStringValues,e as diffText,r as getVersionableOptions,a as versionable,u as workflowable};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.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. Works alongside\n * `publishable()` — the workflow status tracks the editorial pipeline,\n * while publishable's `status` remains the public-facing state.\n *\n * Key invariant: `status` can only transition to 'published' when\n * `_workflowStatus === 'approved'` (unless the user has direct `publish`\n * permission, which bypasses the workflow).\n *\n * Workflow states:\n * - draft → in_review (submit for review, requires 'update')\n * - in_review → approved (approve, requires 'publish')\n * - in_review → rejected (reject with note, requires 'publish')\n * - approved → published (publish, requires 'publish')\n * - rejected → in_review (resubmit, requires 'update')\n * - published → draft (unpublish, requires 'publish')\n */\n\nimport type { Behavior, DateField, SelectField, TextField } 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\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 }) as SelectField & { required: true },\n _workflowAssignee: field.text({ required: false }),\n _workflowNote: field.text({ required: false }),\n _workflowUpdatedAt: field.date(),\n },\n hooks: {\n beforeCreate: async (data) => {\n data._workflowStatus = 'draft'\n data._workflowUpdatedAt = new Date()\n return data\n },\n },\n }\n}\n"],"mappings":"+KAuBA,MAAa,EAAkB,CAAC,QAAS,YAAa,WAAY,WAAY,YAAY,CAU1F,SAAgB,GAA6C,CAC3D,MAAO,CACL,KAAM,eACN,OAAQ,CACN,gBAAiB,EAAM,OAAO,CAC5B,QAAS,EACT,QAAS,QACT,QAAS,GACT,SAAU,GACX,CAAC,CACF,kBAAmB,EAAM,KAAK,CAAE,SAAU,GAAO,CAAC,CAClD,cAAe,EAAM,KAAK,CAAE,SAAU,GAAO,CAAC,CAC9C,mBAAoB,EAAM,MAAM,CACjC,CACD,MAAO,CACL,aAAc,KAAO,KACnB,EAAK,gBAAkB,QACvB,EAAK,mBAAqB,IAAI,KACvB,GAEV,CACF"}
@@ -0,0 +1,2 @@
1
+ import{pgTable as e,timestamp as t,unique as n,uuid as r,varchar as i}from"drizzle-orm/pg-core";function a(a){return e(`${a.name}_locale_status`,{id:r(`id`).primaryKey().defaultRandom(),entityId:r(`entity_id`).notNull(),locale:i(`locale`,{length:10}).notNull(),status:i(`status`,{length:20}).notNull().default(`draft`),publishedAt:t(`published_at`,{withTimezone:!0})},e=>({uniqueEntityLocale:n().on(e.entityId,e.locale)}))}export{a as generateLocaleStatusSchema};
2
+ //# sourceMappingURL=locale-status-CGttT38v.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-status-CGttT38v.mjs","names":[],"sources":["../src/schema/locale-status.ts"],"sourcesContent":["/**\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\nimport type { Entity } from '@murumets-ee/entity'\nimport { pgTable, timestamp, unique, uuid, varchar } from 'drizzle-orm/pg-core'\n\n/**\n * Generate locale status table schema for a publishable + translatable entity.\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 generateLocaleStatusSchema(entity: Entity) {\n const tableName = `${entity.name}_locale_status`\n\n return pgTable(\n tableName,\n {\n id: uuid('id').primaryKey().defaultRandom(),\n entityId: uuid('entity_id').notNull(),\n locale: varchar('locale', { length: 10 }).notNull(),\n status: varchar('status', { length: 20 }).notNull().default('draft'),\n publishedAt: timestamp('published_at', { withTimezone: true }),\n },\n (table) => ({\n uniqueEntityLocale: unique().on(table.entityId, table.locale),\n }),\n )\n}\n"],"mappings":"gGA0BA,SAAgB,EAA2B,EAAgB,CAGzD,OAAO,EAFW,GAAG,EAAO,KAAK,gBAI/B,CACE,GAAI,EAAK,KAAK,CAAC,YAAY,CAAC,eAAe,CAC3C,SAAU,EAAK,YAAY,CAAC,SAAS,CACrC,OAAQ,EAAQ,SAAU,CAAE,OAAQ,GAAI,CAAC,CAAC,SAAS,CACnD,OAAQ,EAAQ,SAAU,CAAE,OAAQ,GAAI,CAAC,CAAC,SAAS,CAAC,QAAQ,QAAQ,CACpE,YAAa,EAAU,eAAgB,CAAE,aAAc,GAAM,CAAC,CAC/D,CACA,IAAW,CACV,mBAAoB,GAAQ,CAAC,GAAG,EAAM,SAAU,EAAM,OAAO,CAC9D,EACF"}
@@ -0,0 +1,54 @@
1
+ import { c as LockInfo } from "./types-wLHyjcWf.mjs";
2
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
3
+
4
+ //#region src/lock-service.d.ts
5
+ interface LockServiceConfig {
6
+ db: PostgresJsDatabase;
7
+ }
8
+ declare class LockService {
9
+ private db;
10
+ constructor(config: LockServiceConfig);
11
+ private getTable;
12
+ /**
13
+ * Acquire a lock on an entity+locale.
14
+ *
15
+ * - Deletes expired locks first
16
+ * - Attempts INSERT; on conflict checks if same user (re-entrant → renew)
17
+ * - Returns { acquired: true } or { acquired: false, lock: LockInfo }
18
+ */
19
+ acquireLock(entityType: string, entityId: string, locale: string, user: {
20
+ id: string;
21
+ name?: string;
22
+ }): Promise<{
23
+ acquired: true;
24
+ } | {
25
+ acquired: false;
26
+ lock: LockInfo;
27
+ }>;
28
+ /**
29
+ * Renew an existing lock (heartbeat).
30
+ * Only succeeds if the caller holds the lock and it hasn't expired.
31
+ */
32
+ renewLock(entityType: string, entityId: string, locale: string, userId: string): Promise<boolean>;
33
+ /**
34
+ * Release a lock. Only the lock holder can release their own lock.
35
+ */
36
+ releaseLock(entityType: string, entityId: string, locale: string, userId: string): Promise<boolean>;
37
+ /**
38
+ * Force-release a lock regardless of who holds it.
39
+ * Admin-only operation — must be audit-logged by the caller.
40
+ */
41
+ forceReleaseLock(entityType: string, entityId: string, locale: string): Promise<boolean>;
42
+ /**
43
+ * Get the current lock for an entity+locale.
44
+ * Returns null if no lock or lock has expired.
45
+ */
46
+ getLock(entityType: string, entityId: string, locale: string): Promise<LockInfo | null>;
47
+ }
48
+ /**
49
+ * Factory — creates a LockService using the app's DB connection.
50
+ */
51
+ declare function createLockService(): Promise<LockService>;
52
+ //#endregion
53
+ export { LockService, LockServiceConfig, createLockService };
54
+ //# sourceMappingURL=lock-service.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock-service.d.mts","names":[],"sources":["../src/lock-service.ts"],"mappings":";;;;UAgBiB,iBAAA;EACf,EAAA,EAAI,kBAAA;AAAA;AAAA,cAGO,WAAA;EAAA,QACH,EAAA;cAEI,MAAA,EAAQ,iBAAA;EAAA,QAIN,QAAA;EA+HX;;;;;;;EAnHG,WAAA,CACJ,UAAA,UACA,QAAA,UACA,MAAA,UACA,IAAA;IAAQ,EAAA;IAAY,IAAA;EAAA,IACnB,OAAA;IAAU,QAAA;EAAA;IAAqB,QAAA;IAAiB,IAAA,EAAM,QAAA;EAAA;EADnC;;;;EAyEhB,SAAA,CACJ,UAAA,UACA,QAAA,UACA,MAAA,UACA,MAAA,WACC,OAAA;EA7EgD;;;EAyG7C,WAAA,CACJ,UAAA,UACA,QAAA,UACA,MAAA,UACA,MAAA,WACC,OAAA;EApCD;;;;EA4DI,gBAAA,CAAiB,UAAA,UAAoB,QAAA,UAAkB,MAAA,WAAiB,OAAA;EA5B5E;;;;EAmDI,OAAA,CAAQ,UAAA,UAAoB,QAAA,UAAkB,MAAA,WAAiB,OAAA,CAAQ,QAAA;AAAA;;;;iBAkCzD,iBAAA,CAAA,GAAqB,OAAA,CAAQ,WAAA"}
@@ -0,0 +1,2 @@
1
+ import"server-only";var e=class{db;constructor(e){this.db=e.db}async getTable(){let{schemaRegistry:e}=await import(`@murumets-ee/db`);return e.get(`toolkit_content_locks`)}async acquireLock(e,t,n,r){let{eq:i,and:a,lt:o}=await import(`drizzle-orm`),s=await this.getTable();if(!s)return{acquired:!0};let c=new Date,l=new Date(c.getTime()+300*1e3);await this.db.delete(s).where(a(i(s.entityType,e),i(s.entityId,t),i(s.locale,n),o(s.expiresAt,c)));let[u]=await this.db.select().from(s).where(a(i(s.entityType,e),i(s.entityId,t),i(s.locale,n))).limit(1);return u?u.lockedBy===r.id?(await this.db.update(s).set({expiresAt:l,lockedAt:c}).where(i(s.id,u.id)),{acquired:!0}):{acquired:!1,lock:{lockedBy:u.lockedBy,lockedByName:u.lockedByName??null,lockedAt:u.lockedAt,expiresAt:u.expiresAt}}:(await this.db.insert(s).values({entityType:e,entityId:t,locale:n,lockedBy:r.id,lockedByName:r.name??null,lockedAt:c,expiresAt:l}),{acquired:!0})}async renewLock(e,t,n,r){let{eq:i,and:a,gte:o}=await import(`drizzle-orm`),s=await this.getTable();if(!s)return!0;let c=new Date,l=new Date(c.getTime()+300*1e3);return(await this.db.update(s).set({expiresAt:l}).where(a(i(s.entityType,e),i(s.entityId,t),i(s.locale,n),i(s.lockedBy,r),o(s.expiresAt,c))).returning({id:s.id})).length>0}async releaseLock(e,t,n,r){let{eq:i,and:a}=await import(`drizzle-orm`),o=await this.getTable();return o?(await this.db.delete(o).where(a(i(o.entityType,e),i(o.entityId,t),i(o.locale,n),i(o.lockedBy,r))).returning({id:o.id})).length>0:!0}async forceReleaseLock(e,t,n){let{eq:r,and:i}=await import(`drizzle-orm`),a=await this.getTable();return a?(await this.db.delete(a).where(i(r(a.entityType,e),r(a.entityId,t),r(a.locale,n))).returning({id:a.id})).length>0:!0}async getLock(e,t,n){let{eq:r,and:i,gte:a}=await import(`drizzle-orm`),o=await this.getTable();if(!o)return null;let s=new Date,[c]=await this.db.select().from(o).where(i(r(o.entityType,e),r(o.entityId,t),r(o.locale,n),a(o.expiresAt,s))).limit(1);return c?{lockedBy:c.lockedBy,lockedByName:c.lockedByName??null,lockedAt:c.lockedAt,expiresAt:c.expiresAt}:null}};async function t(){let{getApp:t}=await import(`@murumets-ee/core`);return new e({db:t().db.readWrite})}export{e as LockService,t as createLockService};
2
+ //# sourceMappingURL=lock-service.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock-service.mjs","names":[],"sources":["../src/lock-service.ts"],"sourcesContent":["/**\n * Content lock service — pessimistic locking for entity editing.\n *\n * Prevents concurrent edit conflicts by allowing only one editor at a time\n * per entity+locale combination. Locks expire after 5 minutes and must\n * be renewed via heartbeat.\n */\n\nimport 'server-only'\n\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { LockInfo } from './types.js'\n\n/** Lock duration in minutes */\nconst LOCK_DURATION_MINUTES = 5\n\nexport interface LockServiceConfig {\n db: PostgresJsDatabase\n}\n\nexport class LockService {\n private db: PostgresJsDatabase\n\n constructor(config: LockServiceConfig) {\n this.db = config.db\n }\n\n private async getTable() {\n const { schemaRegistry } = await import('@murumets-ee/db')\n return schemaRegistry.get('toolkit_content_locks')\n }\n\n /**\n * Acquire a lock on an entity+locale.\n *\n * - Deletes expired locks first\n * - Attempts INSERT; on conflict checks if same user (re-entrant → renew)\n * - Returns { acquired: true } or { acquired: false, lock: LockInfo }\n */\n async acquireLock(\n entityType: string,\n entityId: string,\n locale: string,\n user: { id: string; name?: string },\n ): Promise<{ acquired: true } | { acquired: false; lock: LockInfo }> {\n const { eq, and, lt } = await import('drizzle-orm')\n const table = await this.getTable()\n if (!table) return { acquired: true }\n\n const now = new Date()\n const expiresAt = new Date(now.getTime() + LOCK_DURATION_MINUTES * 60 * 1000)\n\n // 1. Clean up expired locks for this entity\n await this.db\n .delete(table)\n .where(\n and(\n eq(table.entityType, entityType),\n eq(table.entityId, entityId),\n eq(table.locale, locale),\n lt(table.expiresAt, now),\n ),\n )\n\n // 2. Check for existing lock\n const [existing] = await this.db\n .select()\n .from(table)\n .where(\n and(\n eq(table.entityType, entityType),\n eq(table.entityId, entityId),\n eq(table.locale, locale),\n ),\n )\n .limit(1)\n\n if (existing) {\n // Re-entrant: same user → renew\n if ((existing.lockedBy as string) === user.id) {\n await this.db\n .update(table)\n .set({ expiresAt, lockedAt: now })\n .where(eq(table.id, existing.id))\n return { acquired: true }\n }\n // Different user holds lock\n return {\n acquired: false,\n lock: {\n lockedBy: existing.lockedBy as string,\n lockedByName: (existing.lockedByName as string) ?? null,\n lockedAt: existing.lockedAt as Date,\n expiresAt: existing.expiresAt as Date,\n },\n }\n }\n\n // 3. No lock exists — insert\n await this.db.insert(table).values({\n entityType,\n entityId,\n locale,\n lockedBy: user.id,\n lockedByName: user.name ?? null,\n lockedAt: now,\n expiresAt,\n })\n\n return { acquired: true }\n }\n\n /**\n * Renew an existing lock (heartbeat).\n * Only succeeds if the caller holds the lock and it hasn't expired.\n */\n async renewLock(\n entityType: string,\n entityId: string,\n locale: string,\n userId: string,\n ): Promise<boolean> {\n const { eq, and, gte } = await import('drizzle-orm')\n const table = await this.getTable()\n if (!table) return true\n\n const now = new Date()\n const expiresAt = new Date(now.getTime() + LOCK_DURATION_MINUTES * 60 * 1000)\n\n const result = await this.db\n .update(table)\n .set({ expiresAt })\n .where(\n and(\n eq(table.entityType, entityType),\n eq(table.entityId, entityId),\n eq(table.locale, locale),\n eq(table.lockedBy, userId),\n gte(table.expiresAt, now),\n ),\n )\n .returning({ id: table.id })\n\n return result.length > 0\n }\n\n /**\n * Release a lock. Only the lock holder can release their own lock.\n */\n async releaseLock(\n entityType: string,\n entityId: string,\n locale: string,\n userId: string,\n ): Promise<boolean> {\n const { eq, and } = await import('drizzle-orm')\n const table = await this.getTable()\n if (!table) return true\n\n const result = await this.db\n .delete(table)\n .where(\n and(\n eq(table.entityType, entityType),\n eq(table.entityId, entityId),\n eq(table.locale, locale),\n eq(table.lockedBy, userId),\n ),\n )\n .returning({ id: table.id })\n\n return result.length > 0\n }\n\n /**\n * Force-release a lock regardless of who holds it.\n * Admin-only operation — must be audit-logged by the caller.\n */\n async forceReleaseLock(entityType: string, entityId: string, locale: string): Promise<boolean> {\n const { eq, and } = await import('drizzle-orm')\n const table = await this.getTable()\n if (!table) return true\n\n const result = await this.db\n .delete(table)\n .where(\n and(\n eq(table.entityType, entityType),\n eq(table.entityId, entityId),\n eq(table.locale, locale),\n ),\n )\n .returning({ id: table.id })\n\n return result.length > 0\n }\n\n /**\n * Get the current lock for an entity+locale.\n * Returns null if no lock or lock has expired.\n */\n async getLock(entityType: string, entityId: string, locale: string): Promise<LockInfo | null> {\n const { eq, and, gte } = await import('drizzle-orm')\n const table = await this.getTable()\n if (!table) return null\n\n const now = new Date()\n\n const [row] = await this.db\n .select()\n .from(table)\n .where(\n and(\n eq(table.entityType, entityType),\n eq(table.entityId, entityId),\n eq(table.locale, locale),\n gte(table.expiresAt, now),\n ),\n )\n .limit(1)\n\n if (!row) return null\n\n return {\n lockedBy: row.lockedBy as string,\n lockedByName: (row.lockedByName as string) ?? null,\n lockedAt: row.lockedAt as Date,\n expiresAt: row.expiresAt as Date,\n }\n }\n}\n\n/**\n * Factory — creates a LockService using the app's DB connection.\n */\nexport async function createLockService(): Promise<LockService> {\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n return new LockService({ db: app.db.readWrite })\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{pgTable as e,timestamp as t,unique as n,uuid as r,varchar as i}from"drizzle-orm/pg-core";const a=e(`toolkit_content_locks`,{id:r(`id`).primaryKey().defaultRandom(),entityType:i(`entity_type`,{length:100}).notNull(),entityId:i(`entity_id`,{length:255}).notNull(),locale:i(`locale`,{length:10}).notNull().default(`_`),lockedBy:i(`locked_by`,{length:255}).notNull(),lockedByName:i(`locked_by_name`,{length:255}),lockedAt:t(`locked_at`,{withTimezone:!0}).notNull().defaultNow(),expiresAt:t(`expires_at`,{withTimezone:!0}).notNull()},e=>({uniqueEntityLock:n().on(e.entityType,e.entityId,e.locale)}));export{a as toolkitContentLocks};
2
+ //# sourceMappingURL=locks-CNxQJieo.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locks-CNxQJieo.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 { pgTable, timestamp, unique, uuid, varchar } from 'drizzle-orm/pg-core'\n\nexport const toolkitContentLocks = pgTable(\n 'toolkit_content_locks',\n {\n id: uuid('id').primaryKey().defaultRandom(),\n entityType: varchar('entity_type', { length: 100 }).notNull(),\n entityId: varchar('entity_id', { length: 255 }).notNull(),\n locale: varchar('locale', { length: 10 }).notNull().default('_'),\n lockedBy: varchar('locked_by', { length: 255 }).notNull(),\n lockedByName: varchar('locked_by_name', { length: 255 }),\n lockedAt: timestamp('locked_at', { withTimezone: true }).notNull().defaultNow(),\n expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),\n },\n (table) => ({\n uniqueEntityLock: unique().on(table.entityType, table.entityId, table.locale),\n }),\n)\n"],"mappings":"gGAYA,MAAa,EAAsB,EACjC,wBACA,CACE,GAAI,EAAK,KAAK,CAAC,YAAY,CAAC,eAAe,CAC3C,WAAY,EAAQ,cAAe,CAAE,OAAQ,IAAK,CAAC,CAAC,SAAS,CAC7D,SAAU,EAAQ,YAAa,CAAE,OAAQ,IAAK,CAAC,CAAC,SAAS,CACzD,OAAQ,EAAQ,SAAU,CAAE,OAAQ,GAAI,CAAC,CAAC,SAAS,CAAC,QAAQ,IAAI,CAChE,SAAU,EAAQ,YAAa,CAAE,OAAQ,IAAK,CAAC,CAAC,SAAS,CACzD,aAAc,EAAQ,iBAAkB,CAAE,OAAQ,IAAK,CAAC,CACxD,SAAU,EAAU,YAAa,CAAE,aAAc,GAAM,CAAC,CAAC,SAAS,CAAC,YAAY,CAC/E,UAAW,EAAU,aAAc,CAAE,aAAc,GAAM,CAAC,CAAC,SAAS,CACrE,CACA,IAAW,CACV,iBAAkB,GAAQ,CAAC,GAAG,EAAM,WAAY,EAAM,SAAU,EAAM,OAAO,CAC9E,EACF"}
@@ -0,0 +1,15 @@
1
+ import { n as ContentConfig } from "./types-wLHyjcWf.mjs";
2
+ import { Plugin } from "@murumets-ee/core";
3
+
4
+ //#region src/plugin.d.ts
5
+ /**
6
+ * Get the content configuration. Throws if plugin not initialized.
7
+ */
8
+ declare function getContentConfig(): ContentConfig;
9
+ /**
10
+ * Content plugin factory.
11
+ */
12
+ declare function content(config: ContentConfig): Plugin;
13
+ //#endregion
14
+ export { content, getContentConfig };
15
+ //# sourceMappingURL=plugin.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;;;;iBA2BgB,gBAAA,CAAA,GAAoB,aAAA;;;;iBAYpB,OAAA,CAAQ,MAAA,EAAQ,aAAA,GAAgB,MAAA"}
@@ -0,0 +1,2 @@
1
+ let e=null;function t(){if(!e)throw Error(`@murumets-ee/content plugin not initialized. Add content() to your plugins array.`);return e}function n(t){if(!t.locales||t.locales.length===0)throw Error(`content() requires at least one locale`);if(!t.defaultLocale)throw Error(`content() requires a defaultLocale`);if(!t.locales.some(e=>e.code===t.defaultLocale))throw Error(`defaultLocale "${t.defaultLocale}" not found in locales: [${t.locales.map(e=>e.code).join(`, `)}]`);return{name:`@murumets-ee/content`,init:async n=>{e=t;let{schemaRegistry:r}=await import(`@murumets-ee/db`),{generateVersionsSchema:i}=await import(`./versions-cotph2VG.mjs`),{generateLocaleStatusSchema:a}=await import(`./locale-status-CGttT38v.mjs`),{generateDraftsSchema:o}=await import(`./drafts-BYyRdmiT.mjs`),{toolkitContentLocks:s}=await import(`./locks-CNxQJieo.mjs`),{generateLayoutSchema:c,generateLayoutTranslationSchema:l}=await import(`@murumets-ee/entity`),u=0,d=0,f=0,p=0;r.has(`toolkit_content_locks`)||r.register(`toolkit_content_locks`,s);for(let[e,t]of n.entities){if(t.behaviors?.some(e=>e.name===`versionable`)){let n=`${e}_versions`;if(!r.has(n)){let e=i(t);r.register(n,e)}u++}let n=t.allFields;if(Object.values(n).some(e=>e.type===`blocks`)){let n=`${e}_layout`;if(!r.has(n)){let e=c(t);e&&r.register(n,e)}let i=`${e}_layout_translations`;if(!r.has(i)){let e=l(t);e&&r.register(i,e)}d++}let s=t.behaviors?.some(e=>e.name===`publishable`),m=Object.values(n).some(e=>e.translatable);if(s&&m){let n=`${e}_locale_status`;if(!r.has(n)){let e=a(t);r.register(n,e)}f++}if(s){let n=`${e}_drafts`;if(!r.has(n)){let e=o(t);r.register(n,e)}p++}}n.logger.info({locales:t.locales.map(e=>e.code),defaultLocale:t.defaultLocale,versionableEntities:u,blockEntities:d,localeStatusEntities:f,draftEntities:p},`Content plugin initialized`)}}}export{n as content,t as getContentConfig};
2
+ //# sourceMappingURL=plugin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Content plugin — registers locale configuration and version schemas.\n *\n * Usage:\n * import { content } from '@murumets-ee/content/plugin'\n *\n * export default defineConfig({\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 type { Plugin } from '@murumets-ee/core'\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): Plugin {\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 {\n name: '@murumets-ee/content',\n init: async (app) => {\n // Store config globally\n _contentConfig = config\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { generateVersionsSchema } = await import('./schema/versions.js')\n const { generateLocaleStatusSchema } = await import('./schema/locale-status.js')\n const { generateDraftsSchema } = await import('./schema/drafts.js')\n const { toolkitContentLocks } = await import('./schema/locks.js')\n const { generateLayoutSchema, generateLayoutTranslationSchema } = await import(\n '@murumets-ee/entity'\n )\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 versionsSchema = generateVersionsSchema(entity)\n schemaRegistry.register(versionsTableName, versionsSchema)\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 statusSchema = generateLocaleStatusSchema(entity)\n schemaRegistry.register(statusTableName, statusSchema)\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 draftsSchema = generateDraftsSchema(entity)\n schemaRegistry.register(draftsTableName, draftsSchema)\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"],"mappings":"AAsBA,IAAI,EAAuC,KAK3C,SAAgB,GAAkC,CAChD,GAAI,CAAC,EACH,MAAU,MACR,oFACD,CAEH,OAAO,EAMT,SAAgB,EAAQ,EAA+B,CAErD,GAAI,CAAC,EAAO,SAAW,EAAO,QAAQ,SAAW,EAC/C,MAAU,MAAM,yCAAyC,CAG3D,GAAI,CAAC,EAAO,cACV,MAAU,MAAM,qCAAqC,CAIvD,GAAI,CADkB,EAAO,QAAQ,KAAM,GAAM,EAAE,OAAS,EAAO,cAAc,CAE/E,MAAU,MACR,kBAAkB,EAAO,cAAc,2BAA2B,EAAO,QAAQ,IAAK,GAAM,EAAE,KAAK,CAAC,KAAK,KAAK,CAAC,GAChH,CAGH,MAAO,CACL,KAAM,uBACN,KAAM,KAAO,IAAQ,CAEnB,EAAiB,EAEjB,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,0BAA2B,MAAM,OAAO,2BAC1C,CAAE,8BAA+B,MAAM,OAAO,gCAC9C,CAAE,wBAAyB,MAAM,OAAO,yBACxC,CAAE,uBAAwB,MAAM,OAAO,wBACvC,CAAE,uBAAsB,mCAAoC,MAAM,OACtE,uBAGE,EAAmB,EACnB,EAAc,EACd,EAAoB,EACpB,EAAc,EAGb,EAAe,IAAI,wBAAwB,EAC9C,EAAe,SAAS,wBAAyB,EAAoB,CAGvE,IAAK,GAAM,CAAC,EAAM,KAAW,EAAI,SAAU,CAKzC,GAHuB,EAAO,WAAW,KACtC,GAAwB,EAAE,OAAS,cACrC,CACmB,CAClB,IAAM,EAAoB,GAAG,EAAK,WAClC,GAAI,CAAC,EAAe,IAAI,EAAkB,CAAE,CAC1C,IAAM,EAAiB,EAAuB,EAAO,CACrD,EAAe,SAAS,EAAmB,EAAe,CAE5D,IAIF,IAAM,EAAY,EAAO,UAKzB,GADkB,OAAO,OAAO,EAAU,CAAC,KAAM,GAAM,EAAE,OAAS,SAAS,CAC5D,CACb,IAAM,EAAkB,GAAG,EAAK,SAChC,GAAI,CAAC,EAAe,IAAI,EAAgB,CAAE,CACxC,IAAM,EAAe,EAAqB,EAAO,CAC7C,GACF,EAAe,SAAS,EAAiB,EAAa,CAI1D,IAAM,EAAuB,GAAG,EAAK,sBACrC,GAAI,CAAC,EAAe,IAAI,EAAqB,CAAE,CAC7C,IAAM,EAAoB,EAAgC,EAAO,CAC7D,GACF,EAAe,SAAS,EAAsB,EAAkB,CAIpE,IAIF,IAAM,EAAgB,EAAO,WAAW,KACrC,GAAwB,EAAE,OAAS,cACrC,CACK,EAAkB,OAAO,OAAO,EAAU,CAAC,KAAM,GAAM,EAAE,aAAa,CAC5E,GAAI,GAAiB,EAAiB,CACpC,IAAM,EAAkB,GAAG,EAAK,gBAChC,GAAI,CAAC,EAAe,IAAI,EAAgB,CAAE,CACxC,IAAM,EAAe,EAA2B,EAAO,CACvD,EAAe,SAAS,EAAiB,EAAa,CAExD,IAIF,GAAI,EAAe,CACjB,IAAM,EAAkB,GAAG,EAAK,SAChC,GAAI,CAAC,EAAe,IAAI,EAAgB,CAAE,CACxC,IAAM,EAAe,EAAqB,EAAO,CACjD,EAAe,SAAS,EAAiB,EAAa,CAExD,KAIJ,EAAI,OAAO,KACT,CACE,QAAS,EAAO,QAAQ,IAAK,GAAM,EAAE,KAAK,CAC1C,cAAe,EAAO,cACtB,oBAAqB,EACrB,cAAe,EACf,qBAAsB,EACtB,cAAe,EAChB,CACD,6BACD,EAEJ"}
@@ -0,0 +1,60 @@
1
+ import { Entity } from "@murumets-ee/entity";
2
+ import { Logger } from "@murumets-ee/core";
3
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
+ import { CountOptions, FindByIdOptions, FindManyOptions } from "@murumets-ee/entity/query";
5
+
6
+ //#region src/query-client.d.ts
7
+ interface ContentQueryClientConfig {
8
+ entity: Entity;
9
+ db: PostgresJsDatabase;
10
+ logger?: Logger;
11
+ }
12
+ declare class ContentQueryClient {
13
+ private entity;
14
+ private db;
15
+ private logger?;
16
+ private query;
17
+ constructor(config: ContentQueryClientConfig);
18
+ private getQuery;
19
+ findById(id: string, options?: FindByIdOptions): Promise<({
20
+ id: string;
21
+ } & {} & {
22
+ [x: string]: string | number | boolean | Record<string, unknown> | Date | Record<string, unknown>[] | {
23
+ [key: string]: unknown;
24
+ _block: string;
25
+ _id: string;
26
+ }[] | null | undefined;
27
+ }) | null>;
28
+ /**
29
+ * Find entity by slug — convenience for slug-based lookups.
30
+ */
31
+ findBySlug(slug: string, options?: {
32
+ locale?: string;
33
+ }): Promise<({
34
+ id: string;
35
+ } & {} & {
36
+ [x: string]: string | number | boolean | Record<string, unknown> | Date | Record<string, unknown>[] | {
37
+ [key: string]: unknown;
38
+ _block: string;
39
+ _id: string;
40
+ }[] | null | undefined;
41
+ }) | null>;
42
+ findMany(options?: FindManyOptions): Promise<({
43
+ id: string;
44
+ } & {} & {
45
+ [x: string]: string | number | boolean | Record<string, unknown> | Date | Record<string, unknown>[] | {
46
+ [key: string]: unknown;
47
+ _block: string;
48
+ _id: string;
49
+ }[] | null | undefined;
50
+ })[]>;
51
+ count(options?: CountOptions): Promise<number>;
52
+ }
53
+ /**
54
+ * Factory — creates a ContentQueryClient for an entity.
55
+ * Must be called after createApp().
56
+ */
57
+ declare function createContentQueryClient(entity: Entity): Promise<ContentQueryClient>;
58
+ //#endregion
59
+ export { ContentQueryClient, ContentQueryClientConfig, createContentQueryClient };
60
+ //# sourceMappingURL=query-client.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-client.d.mts","names":[],"sources":["../src/query-client.ts"],"mappings":";;;;;;UAmBiB,wBAAA;EACf,MAAA,EAAQ,MAAA;EACR,EAAA,EAAI,kBAAA;EACJ,MAAA,GAAS,MAAA;AAAA;AAAA,cAGE,kBAAA;EAAA,QACH,MAAA;EAAA,QACA,EAAA;EAAA,QACA,MAAA;EAAA,QACA,KAAA;cAEI,MAAA,EAAQ,wBAAA;EAAA,QAMN,QAAA;EAgBR,QAAA,CAAS,EAAA,UAAY,OAAA,GAAU,eAAA,GAAe,OAAA;;;;;;;;;;;;EAQ9C,UAAA,CAAW,IAAA,UAAc,OAAA;IAAY,MAAA;EAAA,IAAiB,OAAA;;;;;;;;;EAmBtD,QAAA,CAAS,OAAA,GAAU,eAAA,GAAe,OAAA;;;;;;;;;EAKlC,KAAA,CAAM,OAAA,GAAU,YAAA,GAAY,OAAA;AAAA;;;;;iBAUd,wBAAA,CAAyB,MAAA,EAAQ,MAAA,GAAS,OAAA,CAAQ,kBAAA"}
@@ -0,0 +1,2 @@
1
+ var e=class{entity;db;logger;query=null;constructor(e){this.entity=e.entity,this.db=e.db,this.logger=e.logger}async getQuery(){if(!this.query){let{QueryClient:e}=await import(`@murumets-ee/entity/query`);this.query=new e({entity:this.entity,db:this.db,logger:this.logger})}return this.query}async findById(e,t){return(await this.getQuery()).findById(e,t)}async findBySlug(e,t){let{schemaRegistry:n}=await import(`@murumets-ee/db`),{eq:r}=await import(`drizzle-orm`),i=n.get(this.entity.name);return!i||!i.slug?null:(await(await this.getQuery()).findMany({where:r(i.slug,e),limit:1,locale:t?.locale}))[0]??null}async findMany(e){return(await this.getQuery()).findMany(e)}async count(e){return(await this.getQuery()).count(e)}};async function t(t){let{getApp:n}=await import(`@murumets-ee/core`),r=n();return new e({entity:t,db:r.db.readOnly,logger:r.logger.child({content:t.name})})}export{e as ContentQueryClient,t as createContentQueryClient};
2
+ //# sourceMappingURL=query-client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-client.mjs","names":[],"sources":["../src/query-client.ts"],"sourcesContent":["/**\n * ContentQueryClient — read-only content queries, safe for client bundles.\n *\n * Usage:\n * import { createContentQueryClient } from '@murumets-ee/content/query'\n * const pages = createContentQueryClient(Page)\n * const page = await pages.findBySlug('about', { locale: 'de' })\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { Entity } from '@murumets-ee/entity'\nimport type {\n CountOptions,\n FindByIdOptions,\n FindManyOptions,\n QueryClient,\n} from '@murumets-ee/entity/query'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n\nexport interface ContentQueryClientConfig {\n entity: Entity\n db: PostgresJsDatabase\n logger?: Logger\n}\n\nexport class ContentQueryClient {\n private entity: Entity\n private db: PostgresJsDatabase\n private logger?: Logger\n private query: QueryClient | null = null\n\n constructor(config: ContentQueryClientConfig) {\n this.entity = config.entity\n this.db = config.db\n this.logger = config.logger\n }\n\n private async getQuery() {\n if (!this.query) {\n const { QueryClient } = await import('@murumets-ee/entity/query')\n this.query = new QueryClient({\n entity: this.entity,\n db: this.db,\n logger: this.logger,\n })\n }\n return this.query\n }\n\n // ---------------------------------------------------------------\n // Read operations (delegated to QueryClient)\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: FindByIdOptions) {\n const query = await this.getQuery()\n return query.findById(id, options)\n }\n\n /**\n * Find entity by slug — convenience for slug-based lookups.\n */\n async findBySlug(slug: string, options?: { locale?: string }) {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq } = await import('drizzle-orm')\n\n const table = schemaRegistry.get(this.entity.name)\n if (!table || !table.slug) return null\n\n const query = await this.getQuery()\n\n // Build where clause with slug filter\n const results = await query.findMany({\n where: eq(table.slug, slug),\n limit: 1,\n locale: options?.locale,\n })\n\n return results[0] ?? null\n }\n\n async findMany(options?: FindManyOptions) {\n const query = await this.getQuery()\n return query.findMany(options)\n }\n\n async count(options?: CountOptions) {\n const query = await this.getQuery()\n return query.count(options)\n }\n}\n\n/**\n * Factory — creates a ContentQueryClient for an entity.\n * Must be called after createApp().\n */\nexport async function createContentQueryClient(entity: Entity): Promise<ContentQueryClient> {\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n return new ContentQueryClient({\n entity,\n db: app.db.readOnly,\n logger: app.logger.child({ content: entity.name }),\n })\n}\n"],"mappings":"AAyBA,IAAa,EAAb,KAAgC,CAC9B,OACA,GACA,OACA,MAAoC,KAEpC,YAAY,EAAkC,CAC5C,KAAK,OAAS,EAAO,OACrB,KAAK,GAAK,EAAO,GACjB,KAAK,OAAS,EAAO,OAGvB,MAAc,UAAW,CACvB,GAAI,CAAC,KAAK,MAAO,CACf,GAAM,CAAE,eAAgB,MAAM,OAAO,6BACrC,KAAK,MAAQ,IAAI,EAAY,CAC3B,OAAQ,KAAK,OACb,GAAI,KAAK,GACT,OAAQ,KAAK,OACd,CAAC,CAEJ,OAAO,KAAK,MAOd,MAAM,SAAS,EAAY,EAA2B,CAEpD,OADc,MAAM,KAAK,UAAU,EACtB,SAAS,EAAI,EAAQ,CAMpC,MAAM,WAAW,EAAc,EAA+B,CAC5D,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,MAAO,MAAM,OAAO,eAEtB,EAAQ,EAAe,IAAI,KAAK,OAAO,KAAK,CAYlD,MAXI,CAAC,GAAS,CAAC,EAAM,KAAa,MAKlB,MAHF,MAAM,KAAK,UAAU,EAGP,SAAS,CACnC,MAAO,EAAG,EAAM,KAAM,EAAK,CAC3B,MAAO,EACP,OAAQ,GAAS,OAClB,CAAC,EAEa,IAAM,KAGvB,MAAM,SAAS,EAA2B,CAExC,OADc,MAAM,KAAK,UAAU,EACtB,SAAS,EAAQ,CAGhC,MAAM,MAAM,EAAwB,CAElC,OADc,MAAM,KAAK,UAAU,EACtB,MAAM,EAAQ,GAQ/B,eAAsB,EAAyB,EAA6C,CAC1F,GAAM,CAAE,UAAW,MAAM,OAAO,qBAC1B,EAAM,GAAQ,CACpB,OAAO,IAAI,EAAmB,CAC5B,SACA,GAAI,EAAI,GAAG,SACX,OAAQ,EAAI,OAAO,MAAM,CAAE,QAAS,EAAO,KAAM,CAAC,CACnD,CAAC"}
@@ -0,0 +1,43 @@
1
+ //#region src/diff/types.d.ts
2
+ /**
3
+ * Diff result types for version comparison.
4
+ */
5
+ /** A segment within a word-level text diff. */
6
+ interface DiffSegment {
7
+ type: 'equal' | 'add' | 'remove';
8
+ value: string;
9
+ }
10
+ /** Diff result for a single scalar field. */
11
+ interface FieldDiff {
12
+ field: string;
13
+ type: 'text' | 'number' | 'boolean' | 'date' | 'select' | 'media' | 'richtext' | 'json' | 'blocks' | 'unknown';
14
+ from: unknown;
15
+ to: unknown;
16
+ /** Word-level diff segments (present for text/richtext/slug fields). */
17
+ segments?: DiffSegment[];
18
+ /** Block-level structural diff (present for blocks fields). */
19
+ blockDiffs?: BlockDiffEntry[];
20
+ }
21
+ /** Status of a single block within a structural diff. */
22
+ type BlockDiffStatus = 'added' | 'removed' | 'moved' | 'changed' | 'unchanged';
23
+ /** Diff result for a single block within a blocks field. */
24
+ interface BlockDiffEntry {
25
+ blockId: string;
26
+ blockType: string;
27
+ status: BlockDiffStatus;
28
+ fromIndex?: number;
29
+ toIndex?: number;
30
+ /** Per-prop diffs for changed blocks. */
31
+ propDiffs?: PropDiff[];
32
+ }
33
+ /** Diff for a single prop within a changed block. */
34
+ interface PropDiff {
35
+ field: string;
36
+ from: unknown;
37
+ to: unknown;
38
+ /** Word-level segments for string props. */
39
+ segments?: DiffSegment[];
40
+ }
41
+ //#endregion
42
+ export { PropDiff as a, FieldDiff as i, BlockDiffStatus as n, DiffSegment as r, BlockDiffEntry as t };
43
+ //# sourceMappingURL=types-nXbE5Ufa.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-nXbE5Ufa.d.mts","names":[],"sources":["../src/diff/types.ts"],"mappings":";;AAKA;;;UAAiB,WAAA;EACf,IAAA;EACA,KAAA;AAAA;;UAIe,SAAA;EACf,KAAA;EACA,IAAA;EAWA,IAAA;EACA,EAAA;EAEA;EAAA,QAAA,GAAW,WAAA;EAEX;EAAA,UAAA,GAAa,cAAA;AAAA;;KAIH,eAAA;;UAGK,cAAA;EACf,OAAA;EACA,SAAA;EACA,MAAA,EAAQ,eAAA;EACR,SAAA;EACA,OAAA;EAEoB;EAApB,SAAA,GAAY,QAAA;AAAA;;UAIG,QAAA;EACf,KAAA;EACA,IAAA;EACA,EAAA;EAPY;EASZ,QAAA,GAAW,WAAA;AAAA"}
@@ -0,0 +1,95 @@
1
+ import { FieldConfig, InferEntityDTO } from "@murumets-ee/entity";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * A block definition — describes a reusable content block's data shape.
6
+ *
7
+ * @typeParam F - The literal field map, inferred from the call site.
8
+ */
9
+ interface BlockDefinition<F extends Record<string, FieldConfig> = Record<string, FieldConfig>> {
10
+ slug: string;
11
+ label: string;
12
+ fields: F;
13
+ }
14
+ /**
15
+ * Infer the data shape of a single block definition.
16
+ * Produces `{ _block: 'hero'; _id: string; heading: string; ... }`
17
+ */
18
+ type InferBlockData<B extends BlockDefinition> = B extends BlockDefinition<infer F> ? {
19
+ _block: B['slug'];
20
+ _id: string;
21
+ } & Omit<InferEntityDTO<F>, 'id'> : never;
22
+ /**
23
+ * Infer a discriminated union from a tuple of block definitions.
24
+ *
25
+ * @example
26
+ * type Layout = InferBlocksUnion<typeof [HeroBlock, RichTextBlock]>
27
+ * // = { _block: 'hero'; ... } | { _block: 'richtext'; ... }
28
+ */
29
+ type InferBlocksUnion<T extends readonly BlockDefinition[]> = { [K in keyof T]: T[K] extends BlockDefinition ? InferBlockData<T[K]> : never }[number];
30
+ interface LocaleConfig {
31
+ code: string;
32
+ label: string;
33
+ }
34
+ interface ContentConfig {
35
+ locales: LocaleConfig[];
36
+ defaultLocale: string;
37
+ }
38
+ interface VersionableOptions {
39
+ /** Maximum versions kept per document. Default: 50 */
40
+ maxPerDoc?: number;
41
+ /** Enable autosave (single mutable autosave version). Default: false */
42
+ autosave?: boolean;
43
+ }
44
+ interface VersionEntry {
45
+ id: string;
46
+ entityId: string;
47
+ version: number;
48
+ locale: string;
49
+ data: Record<string, unknown>;
50
+ delta: Record<string, unknown> | null;
51
+ status: string | null;
52
+ createdBy: string | null;
53
+ createdByName: string | null;
54
+ createdAt: Date | string;
55
+ isAutosave: boolean;
56
+ }
57
+ interface TranslationFieldStatus {
58
+ complete: boolean;
59
+ total: number;
60
+ translated: number;
61
+ missing: string[];
62
+ }
63
+ interface TranslationStatus {
64
+ [locale: string]: TranslationFieldStatus;
65
+ }
66
+ interface TranslationReport {
67
+ total: number;
68
+ complete: number;
69
+ partial: number;
70
+ missing: number;
71
+ }
72
+ interface LocalePublishStatus {
73
+ locale: string;
74
+ status: 'draft' | 'published';
75
+ publishedAt: Date | string | null;
76
+ }
77
+ interface DraftEntry {
78
+ id: string;
79
+ entityId: string;
80
+ locale: string;
81
+ data: Record<string, unknown>;
82
+ createdBy: string;
83
+ createdByName: string | null;
84
+ createdAt: Date | string;
85
+ updatedAt: Date | string;
86
+ }
87
+ interface LockInfo {
88
+ lockedBy: string;
89
+ lockedByName: string | null;
90
+ lockedAt: Date | string;
91
+ expiresAt: Date | string;
92
+ }
93
+ //#endregion
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-wLHyjcWf.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-wLHyjcWf.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;EACf,EAAA;EACA,QAAA;EACA,OAAA;EACA,MAAA;EACA,IAAA,EAAM,MAAA;EACN,KAAA,EAAO,MAAA;EACP,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;EACf,EAAA;EACA,QAAA;EACA,MAAA;EACA,IAAA,EAAM,MAAA;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{boolean as e,integer as t,jsonb as n,pgTable as r,timestamp as i,unique as a,uuid as o,varchar as s}from"drizzle-orm/pg-core";function c(c){return r(`${c.name}_versions`,{id:o(`id`).primaryKey().defaultRandom(),entityId:o(`entity_id`).notNull(),version:t(`version`).notNull(),locale:s(`locale`,{length:10}).notNull().default(`_`),data:n(`data`).notNull(),delta:n(`delta`),status:s(`status`,{length:20}),createdBy:s(`created_by`,{length:255}),createdByName:s(`created_by_name`,{length:255}),createdAt:i(`created_at`,{withTimezone:!0}).notNull().defaultNow(),isAutosave:e(`is_autosave`).notNull().default(!1)},e=>({uniqueEntityVersionLocale:a().on(e.entityId,e.version,e.locale)}))}export{c as generateVersionsSchema};
2
+ //# sourceMappingURL=versions-cotph2VG.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versions-cotph2VG.mjs","names":[],"sources":["../src/schema/versions.ts"],"sourcesContent":["/**\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\nimport type { Entity } from '@murumets-ee/entity'\nimport {\n boolean,\n integer,\n jsonb,\n pgTable,\n timestamp,\n unique,\n uuid,\n varchar,\n} from 'drizzle-orm/pg-core'\n\n/**\n * Generate versions table schema for a versionable entity.\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 uuid,\n * created_at timestamp 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 generateVersionsSchema(entity: Entity) {\n const tableName = `${entity.name}_versions`\n\n return pgTable(\n tableName,\n {\n id: uuid('id').primaryKey().defaultRandom(),\n entityId: uuid('entity_id').notNull(),\n version: integer('version').notNull(),\n locale: varchar('locale', { length: 10 }).notNull().default('_'),\n data: jsonb('data').notNull(),\n delta: jsonb('delta'),\n status: varchar('status', { length: 20 }),\n createdBy: varchar('created_by', { length: 255 }),\n createdByName: varchar('created_by_name', { length: 255 }),\n createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),\n isAutosave: boolean('is_autosave').notNull().default(false),\n },\n (table) => ({\n uniqueEntityVersionLocale: unique().on(table.entityId, table.version, table.locale),\n }),\n )\n}\n"],"mappings":"qIAwCA,SAAgB,EAAuB,EAAgB,CAGrD,OAAO,EAFW,GAAG,EAAO,KAAK,WAI/B,CACE,GAAI,EAAK,KAAK,CAAC,YAAY,CAAC,eAAe,CAC3C,SAAU,EAAK,YAAY,CAAC,SAAS,CACrC,QAAS,EAAQ,UAAU,CAAC,SAAS,CACrC,OAAQ,EAAQ,SAAU,CAAE,OAAQ,GAAI,CAAC,CAAC,SAAS,CAAC,QAAQ,IAAI,CAChE,KAAM,EAAM,OAAO,CAAC,SAAS,CAC7B,MAAO,EAAM,QAAQ,CACrB,OAAQ,EAAQ,SAAU,CAAE,OAAQ,GAAI,CAAC,CACzC,UAAW,EAAQ,aAAc,CAAE,OAAQ,IAAK,CAAC,CACjD,cAAe,EAAQ,kBAAmB,CAAE,OAAQ,IAAK,CAAC,CAC1D,UAAW,EAAU,aAAc,CAAE,aAAc,GAAM,CAAC,CAAC,SAAS,CAAC,YAAY,CACjF,WAAY,EAAQ,cAAc,CAAC,SAAS,CAAC,QAAQ,GAAM,CAC5D,CACA,IAAW,CACV,0BAA2B,GAAQ,CAAC,GAAG,EAAM,SAAU,EAAM,QAAS,EAAM,OAAO,CACpF,EACF"}
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
1
  {
2
2
  "name": "@murumets-ee/content",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js"
8
+ "types": "./dist/index.d.mts",
9
+ "import": "./dist/index.mjs"
10
10
  },
11
11
  "./client": {
12
- "types": "./dist/client.d.ts",
13
- "import": "./dist/client.js"
12
+ "types": "./dist/client.d.mts",
13
+ "import": "./dist/client.mjs"
14
14
  },
15
15
  "./query": {
16
- "types": "./dist/query-client.d.ts",
17
- "import": "./dist/query-client.js"
16
+ "types": "./dist/query-client.d.mts",
17
+ "import": "./dist/query-client.mjs"
18
18
  },
19
19
  "./plugin": {
20
- "types": "./dist/plugin.d.ts",
21
- "import": "./dist/plugin.js"
20
+ "types": "./dist/plugin.d.mts",
21
+ "import": "./dist/plugin.mjs"
22
22
  },
23
23
  "./blocks": {
24
- "types": "./dist/blocks/index.d.ts",
25
- "import": "./dist/blocks/index.js"
24
+ "types": "./dist/blocks/index.d.mts",
25
+ "import": "./dist/blocks/index.mjs"
26
26
  },
27
27
  "./lock": {
28
- "types": "./dist/lock-service.d.ts",
29
- "import": "./dist/lock-service.js"
28
+ "types": "./dist/lock-service.d.mts",
29
+ "import": "./dist/lock-service.mjs"
30
30
  }
31
31
  },
32
32
  "files": [
@@ -36,20 +36,20 @@
36
36
  "drizzle-orm": "^0.45.1",
37
37
  "server-only": "^0.0.1",
38
38
  "zod": "^3.24.1",
39
- "@murumets-ee/db": "0.1.4",
40
- "@murumets-ee/entity": "0.1.4",
41
- "@murumets-ee/core": "0.1.5",
42
- "@murumets-ee/logging": "0.1.5"
39
+ "@murumets-ee/core": "0.1.6",
40
+ "@murumets-ee/entity": "0.1.5",
41
+ "@murumets-ee/logging": "0.1.6",
42
+ "@murumets-ee/db": "0.1.5"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^22.10.5",
46
- "tsup": "^8.3.5",
46
+ "tsdown": "^0.21.7",
47
47
  "typescript": "^5.7.3",
48
48
  "vitest": "^2.1.8"
49
49
  },
50
50
  "scripts": {
51
- "build": "tsup",
52
- "dev": "tsup --watch",
51
+ "build": "tsdown",
52
+ "dev": "tsdown --watch",
53
53
  "test": "vitest"
54
54
  }
55
55
  }