@murumets-ee/content 0.10.0 → 0.12.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/dist/blocks/index.d.mts +1 -1
- package/dist/blocks/index.mjs +1 -1
- package/dist/client.d.mts +38 -311
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/{define-block-DpMPEEdA.mjs → define-block-D-7OpIn5.mjs} +1 -1
- package/dist/{define-block-DpMPEEdA.mjs.map → define-block-D-7OpIn5.mjs.map} +1 -1
- package/dist/errors-kkf8Ue1H.mjs +2 -0
- package/dist/errors-kkf8Ue1H.mjs.map +1 -0
- package/dist/index-B4kqLg-c.d.mts +148 -0
- package/dist/index-B4kqLg-c.d.mts.map +1 -0
- package/dist/index.d.mts +3 -71
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/locale-utils-By7EdAT1.mjs +2 -0
- package/dist/locale-utils-By7EdAT1.mjs.map +1 -0
- package/dist/lock-service.d.mts +16 -10
- package/dist/lock-service.d.mts.map +1 -1
- package/dist/lock-service.mjs +1 -1
- package/dist/lock-service.mjs.map +1 -1
- package/dist/locks-BRtmYqgS.mjs +2 -0
- package/dist/{locks-BkO2qMfm.mjs.map → locks-BRtmYqgS.mjs.map} +1 -1
- package/dist/plugin-yijaaFS7.mjs +2 -0
- package/dist/plugin-yijaaFS7.mjs.map +1 -0
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -2
- package/dist/query-client.d.mts +1 -1
- package/dist/query-client.d.mts.map +1 -1
- package/dist/query-client.mjs +1 -1
- package/dist/query-client.mjs.map +1 -1
- package/dist/{types-D-ATlx7t.d.mts → types-BCmX1nBL.d.mts} +6 -6
- package/dist/types-BCmX1nBL.d.mts.map +1 -0
- package/dist/versionable-B0bP2Gib.mjs +2 -0
- package/dist/versionable-B0bP2Gib.mjs.map +1 -0
- package/package.json +7 -7
- package/dist/block-diff-BBH6FoFT.mjs +0 -2
- package/dist/block-diff-BBH6FoFT.mjs.map +0 -1
- package/dist/drafts-CyOivEtr.mjs +0 -2
- package/dist/drafts-CyOivEtr.mjs.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/locale-status-DTyv4wcn.mjs +0 -2
- package/dist/locale-status-DTyv4wcn.mjs.map +0 -1
- package/dist/locks-BkO2qMfm.mjs +0 -2
- package/dist/plugin.mjs.map +0 -1
- package/dist/registry-BJSOGFEN.mjs +0 -2
- package/dist/registry-BJSOGFEN.mjs.map +0 -1
- package/dist/rolldown-runtime-DR3Ue2cl.mjs +0 -1
- package/dist/types-COcneY5H.d.mts +0 -43
- package/dist/types-COcneY5H.d.mts.map +0 -1
- package/dist/types-D-ATlx7t.d.mts.map +0 -1
- package/dist/versions-CO6oDyet.mjs +0 -2
- package/dist/versions-CO6oDyet.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale-utils-By7EdAT1.mjs","names":[],"sources":["../src/locale-utils.ts"],"sourcesContent":["/**\n * Locale validation helpers.\n *\n * Every ContentClient and LockService method that accepts a `locale`\n * parameter must run it through `validateLocale()` before passing it to\n * the database. Drizzle parameterizes queries so SQL injection isn't a\n * concern, but unvalidated locale strings:\n * - bypass the configured locale set (typo'd `'frr'` writes a row that\n * never resolves on read),\n * - silently truncate at the column's `varchar(10)` boundary (e.g.\n * `'pt-BR-traditional'` → `'pt-BR-trad'`, breaking later equality),\n * - pollute drafts/versions/locale_status tables with rows that no\n * UI ever queries.\n *\n * The `'_'` sentinel is allowed because it's the conventional value for\n * default-locale rows in versions/drafts (where every row needs a\n * non-null `locale` for unique-constraint composition).\n */\n\nimport { InvalidLocaleError } from './errors.js'\nimport { getContentConfig } from './plugin.js'\n\n/** The internal sentinel for \"default locale\" used in version/draft storage. */\nexport const DEFAULT_LOCALE_SENTINEL = '_'\n\n/**\n * Throw `InvalidLocaleError` if `locale` is neither the `'_'` sentinel nor\n * one of the configured plugin locale codes.\n */\nexport function validateLocale(locale: string): void {\n if (locale === DEFAULT_LOCALE_SENTINEL) return\n const config = getContentConfig()\n for (const l of config.locales) {\n if (l.code === locale) return\n }\n throw new InvalidLocaleError(\n locale,\n config.locales.map((l) => l.code),\n )\n}\n"],"mappings":"oFA6BA,SAAgB,EAAe,EAAsB,CACnD,GAAI,IAAA,IAAoC,OACxC,IAAM,EAAS,GAAkB,CACjC,IAAK,IAAM,KAAK,EAAO,QACrB,GAAI,EAAE,OAAS,EAAQ,OAEzB,MAAM,IAAI,EACR,EACA,EAAO,QAAQ,IAAK,GAAM,EAAE,KAAK,CAClC"}
|
package/dist/lock-service.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as LockInfo } from "./types-
|
|
1
|
+
import { c as LockInfo } from "./types-BCmX1nBL.mjs";
|
|
2
2
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
3
|
|
|
4
4
|
//#region src/lock-service.d.ts
|
|
@@ -9,11 +9,21 @@ declare class LockService {
|
|
|
9
9
|
private client;
|
|
10
10
|
constructor(config: LockServiceConfig);
|
|
11
11
|
/**
|
|
12
|
-
* Acquire a lock on an entity+locale.
|
|
12
|
+
* Acquire a lock on an entity+locale, atomically.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Implemented as a single `INSERT … ON CONFLICT DO UPDATE … WITH SET WHERE`
|
|
15
|
+
* via {@link TableClient.tryClaim} — no `deleteMany → findOne → insert`
|
|
16
|
+
* race. Two concurrent acquires of the same free slot produce exactly
|
|
17
|
+
* one `acquired: true` and one `acquired: false`; the loser sees the
|
|
18
|
+
* winner's row.
|
|
19
|
+
*
|
|
20
|
+
* Replace semantics: take the slot if **any** of these is true:
|
|
21
|
+
* - the slot is empty (INSERT path),
|
|
22
|
+
* - the existing row is past its expiry (`expiresAt < now`),
|
|
23
|
+
* - the existing row is held by the same user (re-entrant renew).
|
|
24
|
+
*
|
|
25
|
+
* Otherwise — fresh lock by another user — return `{ acquired: false,
|
|
26
|
+
* lock }` so the caller can render "X is editing".
|
|
17
27
|
*/
|
|
18
28
|
acquireLock(entityType: string, entityId: string, locale: string, user: {
|
|
19
29
|
id: string;
|
|
@@ -44,10 +54,6 @@ declare class LockService {
|
|
|
44
54
|
*/
|
|
45
55
|
getLock(entityType: string, entityId: string, locale: string): Promise<LockInfo | null>;
|
|
46
56
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Factory — creates a LockService using the app's DB connection.
|
|
49
|
-
*/
|
|
50
|
-
declare function createLockService(): Promise<LockService>;
|
|
51
57
|
//#endregion
|
|
52
|
-
export { LockService, LockServiceConfig
|
|
58
|
+
export { LockService, LockServiceConfig };
|
|
53
59
|
//# sourceMappingURL=lock-service.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock-service.d.mts","names":[],"sources":["../src/lock-service.ts"],"mappings":";;;;UAqBiB,iBAAA;EACf,EAAA,EAAI,kBAAA;AAAA;AAAA,cAGO,WAAA;EAAA,QACH,MAAA;cAEI,MAAA,EAAQ,iBAAA;
|
|
1
|
+
{"version":3,"file":"lock-service.d.mts","names":[],"sources":["../src/lock-service.ts"],"mappings":";;;;UAqBiB,iBAAA;EACf,EAAA,EAAI,kBAAA;AAAA;AAAA,cAGO,WAAA;EAAA,QACH,MAAA;cAEI,MAAA,EAAQ,iBAAA;EAqG0D;;;;;;;;;;;;;;;;;EAhFxE,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;EA8CtD;;;;EALG,SAAA,CACJ,UAAA,UACA,QAAA,UACA,MAAA,UACA,MAAA,WACC,OAAA;EAmBD;;;EAJI,WAAA,CACJ,UAAA,UACA,QAAA,UACA,MAAA,UACA,MAAA,WACC,OAAA;EASwC;;;;EAArC,gBAAA,CAAiB,UAAA,UAAoB,QAAA,UAAkB,MAAA,WAAiB,OAAA;EAS5C;;;;EAA5B,OAAA,CAAQ,UAAA,UAAoB,QAAA,UAAkB,MAAA,WAAiB,OAAA,CAAQ,QAAA;AAAA"}
|
package/dist/lock-service.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./locks-
|
|
1
|
+
import{t as e}from"./locks-BRtmYqgS.mjs";import"server-only";var t=class{client;constructor(t){this.client=e.makeClient(t.db)}async acquireLock(e,t,n,r){let i=new Date,a=new Date(i.getTime()+300*1e3),o=await this.client.tryClaim({entityType:e,entityId:t,locale:n,lockedBy:r.id,lockedByName:r.name??null,lockedAt:i,expiresAt:a},{target:[`entityType`,`entityId`,`locale`],replaceWhen:{$or:[{expiresAt:{lt:i}},{lockedBy:r.id}]}});return o.acquired?{acquired:!0}:{acquired:!1,lock:{lockedBy:o.row.lockedBy,lockedByName:o.row.lockedByName??null,lockedAt:o.row.lockedAt,expiresAt:o.row.expiresAt}}}async renewLock(e,t,n,r){let i=new Date,a=new Date(i.getTime()+300*1e3);return await this.client.update({entityType:e,entityId:t,locale:n,lockedBy:r,expiresAt:{gte:i}},{expiresAt:a})!==null}async releaseLock(e,t,n,r){return await this.client.delete({entityType:e,entityId:t,locale:n,lockedBy:r})!==null}async forceReleaseLock(e,t,n){return await this.client.delete({entityType:e,entityId:t,locale:n})!==null}async getLock(e,t,n){let r=new Date,i=await this.client.findOne({entityType:e,entityId:t,locale:n,expiresAt:{gte:r}});return i?{lockedBy:i.lockedBy,lockedByName:i.lockedByName??null,lockedAt:i.lockedAt,expiresAt:i.expiresAt}:null}};export{t as LockService};
|
|
2
2
|
//# sourceMappingURL=lock-service.mjs.map
|
|
@@ -1 +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 * Uses TableClient for all DB operations — zero direct Drizzle queries.\n */\n\nimport 'server-only'\n\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { contentLocksTable } from './schema/locks.js'\nimport type { LockInfo } from './types.js'\n\n/** Lock duration in minutes */\nconst LOCK_DURATION_MINUTES = 5\n\ntype LockClient = ReturnType<typeof contentLocksTable.makeClient>\n\nexport interface LockServiceConfig {\n db: PostgresJsDatabase\n}\n\nexport class LockService {\n private client: LockClient\n\n constructor(config: LockServiceConfig) {\n this.client = contentLocksTable.makeClient(config.db)\n }\n\n /**\n * Acquire a lock on an entity+locale.\n *\n *
|
|
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 * Uses TableClient for all DB operations — zero direct Drizzle queries.\n */\n\nimport 'server-only'\n\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { contentLocksTable } from './schema/locks.js'\nimport type { LockInfo } from './types.js'\n\n/** Lock duration in minutes */\nconst LOCK_DURATION_MINUTES = 5\n\ntype LockClient = ReturnType<typeof contentLocksTable.makeClient>\n\nexport interface LockServiceConfig {\n db: PostgresJsDatabase\n}\n\nexport class LockService {\n private client: LockClient\n\n constructor(config: LockServiceConfig) {\n this.client = contentLocksTable.makeClient(config.db)\n }\n\n /**\n * Acquire a lock on an entity+locale, atomically.\n *\n * Implemented as a single `INSERT … ON CONFLICT DO UPDATE … WITH SET WHERE`\n * via {@link TableClient.tryClaim} — no `deleteMany → findOne → insert`\n * race. Two concurrent acquires of the same free slot produce exactly\n * one `acquired: true` and one `acquired: false`; the loser sees the\n * winner's row.\n *\n * Replace semantics: take the slot if **any** of these is true:\n * - the slot is empty (INSERT path),\n * - the existing row is past its expiry (`expiresAt < now`),\n * - the existing row is held by the same user (re-entrant renew).\n *\n * Otherwise — fresh lock by another user — return `{ acquired: false,\n * lock }` so the caller can render \"X is editing\".\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 now = new Date()\n const expiresAt = new Date(now.getTime() + LOCK_DURATION_MINUTES * 60 * 1000)\n\n const result = await this.client.tryClaim(\n {\n entityType,\n entityId,\n locale,\n lockedBy: user.id,\n lockedByName: user.name ?? null,\n lockedAt: now,\n expiresAt,\n },\n {\n target: ['entityType', 'entityId', 'locale'],\n // Replace if expired OR if I already hold this slot. Using $or\n // lets re-entrancy and stale-takeover share one atomic statement.\n replaceWhen: {\n $or: [{ expiresAt: { lt: now } }, { lockedBy: user.id }],\n },\n },\n )\n\n if (result.acquired) return { acquired: true }\n\n return {\n acquired: false,\n lock: {\n lockedBy: result.row.lockedBy,\n lockedByName: result.row.lockedByName ?? null,\n lockedAt: result.row.lockedAt,\n expiresAt: result.row.expiresAt,\n },\n }\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 now = new Date()\n const expiresAt = new Date(now.getTime() + LOCK_DURATION_MINUTES * 60 * 1000)\n\n const updated = await this.client.update(\n { entityType, entityId, locale, lockedBy: userId, expiresAt: { gte: now } },\n { expiresAt },\n )\n\n return updated !== null\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 deleted = await this.client.delete({ entityType, entityId, locale, lockedBy: userId })\n return deleted !== null\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 deleted = await this.client.delete({ entityType, entityId, locale })\n return deleted !== null\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 now = new Date()\n\n const row = await this.client.findOne({\n entityType,\n entityId,\n locale,\n expiresAt: { gte: now },\n })\n\n if (!row) return null\n\n return {\n lockedBy: row.lockedBy,\n lockedByName: row.lockedByName ?? null,\n lockedAt: row.lockedAt as Date,\n expiresAt: row.expiresAt as Date,\n }\n }\n}\n"],"mappings":"6DAyBA,IAAa,EAAb,KAAyB,CACvB,OAEA,YAAY,EAA2B,CACrC,KAAK,OAAS,EAAkB,WAAW,EAAO,GAAG,CAoBvD,MAAM,YACJ,EACA,EACA,EACA,EACmE,CACnE,IAAM,EAAM,IAAI,KACV,EAAY,IAAI,KAAK,EAAI,SAAS,CAAG,IAA6B,IAAK,CAEvE,EAAS,MAAM,KAAK,OAAO,SAC/B,CACE,aACA,WACA,SACA,SAAU,EAAK,GACf,aAAc,EAAK,MAAQ,KAC3B,SAAU,EACV,YACD,CACD,CACE,OAAQ,CAAC,aAAc,WAAY,SAAS,CAG5C,YAAa,CACX,IAAK,CAAC,CAAE,UAAW,CAAE,GAAI,EAAK,CAAE,CAAE,CAAE,SAAU,EAAK,GAAI,CAAC,CACzD,CACF,CACF,CAID,OAFI,EAAO,SAAiB,CAAE,SAAU,GAAM,CAEvC,CACL,SAAU,GACV,KAAM,CACJ,SAAU,EAAO,IAAI,SACrB,aAAc,EAAO,IAAI,cAAgB,KACzC,SAAU,EAAO,IAAI,SACrB,UAAW,EAAO,IAAI,UACvB,CACF,CAOH,MAAM,UACJ,EACA,EACA,EACA,EACkB,CAClB,IAAM,EAAM,IAAI,KACV,EAAY,IAAI,KAAK,EAAI,SAAS,CAAG,IAA6B,IAAK,CAO7E,OAAO,MALe,KAAK,OAAO,OAChC,CAAE,aAAY,WAAU,SAAQ,SAAU,EAAQ,UAAW,CAAE,IAAK,EAAK,CAAE,CAC3E,CAAE,YAAW,CACd,GAEkB,KAMrB,MAAM,YACJ,EACA,EACA,EACA,EACkB,CAElB,OAAO,MADe,KAAK,OAAO,OAAO,CAAE,aAAY,WAAU,SAAQ,SAAU,EAAQ,CAAC,GACzE,KAOrB,MAAM,iBAAiB,EAAoB,EAAkB,EAAkC,CAE7F,OAAO,MADe,KAAK,OAAO,OAAO,CAAE,aAAY,WAAU,SAAQ,CAAC,GACvD,KAOrB,MAAM,QAAQ,EAAoB,EAAkB,EAA0C,CAC5F,IAAM,EAAM,IAAI,KAEV,EAAM,MAAM,KAAK,OAAO,QAAQ,CACpC,aACA,WACA,SACA,UAAW,CAAE,IAAK,EAAK,CACxB,CAAC,CAIF,OAFK,EAEE,CACL,SAAU,EAAI,SACd,aAAc,EAAI,cAAgB,KAClC,SAAU,EAAI,SACd,UAAW,EAAI,UAChB,CAPgB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{column as e,defineTable as t}from"@murumets-ee/db";const n=t({name:`toolkit_content_locks`,columns:{id:e.uuid({primaryKey:!0,defaultRandom:!0}),entityType:e.varchar({length:100,notNull:!0,pgName:`entity_type`}),entityId:e.varchar({length:255,notNull:!0,pgName:`entity_id`}),locale:e.varchar({length:10,notNull:!0,default:`_`}),lockedBy:e.varchar({length:255,notNull:!0,pgName:`locked_by`}),lockedByName:e.varchar({length:255,pgName:`locked_by_name`}),lockedAt:e.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`locked_at`}),expiresAt:e.timestamp({notNull:!0,withTimezone:!0,pgName:`expires_at`})},unique:[{on:[`entityType`,`entityId`,`locale`]}]}),r=n.table;export{r as n,n as t};
|
|
2
|
+
//# sourceMappingURL=locks-BRtmYqgS.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locks-
|
|
1
|
+
{"version":3,"file":"locks-BRtmYqgS.mjs","names":[],"sources":["../src/schema/locks.ts"],"sourcesContent":["/**\n * Content lock table schema\n *\n * Single shared table (not per-entity) for pessimistic content locking.\n * Prevents concurrent edit conflicts by allowing only one editor at a time\n * per entity+locale combination.\n *\n * Locks expire after 5 minutes and must be renewed via heartbeat.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\nexport const contentLocksTable = defineTable({\n name: 'toolkit_content_locks',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityType: column.varchar({ length: 100, notNull: true, pgName: 'entity_type' }),\n entityId: column.varchar({ length: 255, notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n lockedBy: column.varchar({ length: 255, notNull: true, pgName: 'locked_by' }),\n lockedByName: column.varchar({ length: 255, pgName: 'locked_by_name' }),\n lockedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'locked_at',\n }),\n expiresAt: column.timestamp({\n notNull: true,\n withTimezone: true,\n pgName: 'expires_at',\n }),\n },\n unique: [{ on: ['entityType', 'entityId', 'locale'] }],\n})\n\n/** Backwards-compatible export — existing imports of the raw pgTable still work. */\nexport const toolkitContentLocks = contentLocksTable.table\n"],"mappings":"0DAYA,MAAa,EAAoB,EAAY,CAC3C,KAAM,wBACN,QAAS,CACP,GAAI,EAAO,KAAK,CAAE,WAAY,GAAM,cAAe,GAAM,CAAC,CAC1D,WAAY,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,OAAQ,cAAe,CAAC,CACjF,SAAU,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,OAAQ,YAAa,CAAC,CAC7E,OAAQ,EAAO,QAAQ,CAAE,OAAQ,GAAI,QAAS,GAAM,QAAS,IAAK,CAAC,CACnE,SAAU,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,OAAQ,YAAa,CAAC,CAC7E,aAAc,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,iBAAkB,CAAC,CACvE,SAAU,EAAO,UAAU,CACzB,QAAS,GACT,WAAY,GACZ,aAAc,GACd,OAAQ,YACT,CAAC,CACF,UAAW,EAAO,UAAU,CAC1B,QAAS,GACT,aAAc,GACd,OAAQ,aACT,CAAC,CACH,CACD,OAAQ,CAAC,CAAE,GAAI,CAAC,aAAc,WAAY,SAAS,CAAE,CAAC,CACvD,CAAC,CAGW,EAAsB,EAAkB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{n as e}from"./locks-BRtmYqgS.mjs";import{generateLayoutSchema as t,generateLayoutTranslationSchema as n}from"@murumets-ee/entity";import{column as r,defineTable as i,schemaRegistry as a}from"@murumets-ee/db";const o=new Map;function s(e,t){let n=o.get(e)??{};o.set(e,{...n,...t})}function c(e){return o.get(e)??{}}function l(e){return i({name:`${e.name}_drafts`,columns:{id:r.uuid({primaryKey:!0,defaultRandom:!0}),entityId:r.uuid({notNull:!0,pgName:`entity_id`}),locale:r.varchar({length:10,notNull:!0,default:`_`}),data:r.jsonb({notNull:!0}),createdBy:r.varchar({length:255,notNull:!0,pgName:`created_by`}),createdByName:r.varchar({length:255,pgName:`created_by_name`}),createdAt:r.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),updatedAt:r.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityId`,`locale`]}]})}function u(e){return i({name:`${e.name}_locale_status`,columns:{id:r.uuid({primaryKey:!0,defaultRandom:!0}),entityId:r.uuid({notNull:!0,pgName:`entity_id`}),locale:r.varchar({length:10,notNull:!0}),status:r.varchar({length:20,notNull:!0,default:`draft`}),publishedAt:r.timestamp({withTimezone:!0,pgName:`published_at`})},unique:[{on:[`entityId`,`locale`]}]})}function d(e){return i({name:`${e.name}_versions`,columns:{id:r.uuid({primaryKey:!0,defaultRandom:!0}),entityId:r.uuid({notNull:!0,pgName:`entity_id`}),version:r.integer({notNull:!0}),locale:r.varchar({length:10,notNull:!0,default:`_`}),data:r.jsonb({notNull:!0}),delta:r.jsonb(),status:r.varchar({length:20}),createdBy:r.varchar({length:255,pgName:`created_by`}),createdByName:r.varchar({length:255,pgName:`created_by_name`}),createdAt:r.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),isAutosave:r.boolean({notNull:!0,default:!1,pgName:`is_autosave`})},unique:[{on:[`entityId`,`version`,`locale`]}]})}let f=null;function p(){if(!f)throw Error(`@murumets-ee/content plugin not initialized. Add content() to your plugins array.`);return f}function m(r){if(!r.locales||r.locales.length===0)throw Error(`content() requires at least one locale`);if(!r.defaultLocale)throw Error(`content() requires a defaultLocale`);if(!r.locales.some(e=>e.code===r.defaultLocale))throw Error(`defaultLocale "${r.defaultLocale}" not found in locales: [${r.locales.map(e=>e.code).join(`, `)}]`);return{name:`@murumets-ee/content`,server:{init:async i=>{f=r;let o=0,c=0,p=0,m=0;a.has(`toolkit_content_locks`)||a.register(`toolkit_content_locks`,e);for(let[e,r]of i.entities){if(r.behaviors?.some(e=>e.name===`versionable`)){let t=`${e}_versions`;if(!a.has(t)){let n=d(r);a.register(t,n.table),s(e,{versions:n})}o++}let i=r.allFields;if(Object.values(i).some(e=>e.type===`blocks`)){let i=`${e}_layout`;if(!a.has(i)){let e=t(r);e&&a.register(i,e)}let o=`${e}_layout_translations`;if(!a.has(o)){let e=n(r);e&&a.register(o,e)}c++}let f=r.behaviors?.some(e=>e.name===`publishable`),h=Object.values(i).some(e=>e.translatable);if(f&&h){let t=`${e}_locale_status`;if(!a.has(t)){let n=u(r);a.register(t,n.table),s(e,{localeStatus:n})}p++}if(f){let t=`${e}_drafts`;if(!a.has(t)){let n=l(r);a.register(t,n.table),s(e,{drafts:n})}m++}}i.logger.info({locales:r.locales.map(e=>e.code),defaultLocale:r.defaultLocale,versionableEntities:o,blockEntities:c,localeStatusEntities:p,draftEntities:m},`Content plugin initialized`)}}}}export{p as n,c as r,m as t};
|
|
2
|
+
//# sourceMappingURL=plugin-yijaaFS7.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-yijaaFS7.mjs","names":[],"sources":["../src/schema/registry.ts","../src/schema/drafts.ts","../src/schema/locale-status.ts","../src/schema/versions.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Per-entity content table registry.\n *\n * Maps an entity name to its content infrastructure tables — drafts,\n * versions, locale_status. Each entry stores the `DefinedTable` (from\n * `defineTable`), which exposes a typed `.makeClient(db)` for CRUD via\n * `TableClient` and a raw `.table` for legacy callers.\n *\n * The content plugin populates this registry at `init` time; ContentClient\n * reads from it lazily on first use. Tables that don't apply to an entity\n * (non-publishable entities have no drafts, etc.) are simply absent.\n */\n\nimport type { DraftsTable } from './drafts.js'\nimport type { LocaleStatusTable } from './locale-status.js'\nimport type { VersionsTable } from './versions.js'\n\n/**\n * The full set of optional content infra tables attached to an entity.\n * Every field is optional — entities without the relevant behavior don't\n * get the corresponding table.\n */\nexport interface ContentTables {\n /** `{entity}_drafts` — present when the entity has `publishable()`. */\n drafts?: DraftsTable\n /** `{entity}_versions` — present when the entity has `versionable()`. */\n versions?: VersionsTable\n /** `{entity}_locale_status` — present when the entity is publishable + has translatable fields. */\n localeStatus?: LocaleStatusTable\n}\n\n// Module-level map. Single source of truth for ContentClient lookups.\nconst tables = new Map<string, ContentTables>()\n\n/**\n * Register content tables for an entity. Called from `content().init()`.\n *\n * Subsequent calls for the same entity name MERGE the provided patch into\n * the existing entry. This lets the plugin add tables one at a time (drafts,\n * then versions, then locale_status) without the caller needing to build the\n * whole object up front.\n */\nexport function registerContentTables(entityName: string, patch: ContentTables): void {\n const existing = tables.get(entityName) ?? {}\n tables.set(entityName, { ...existing, ...patch })\n}\n\n/**\n * Get the content tables for an entity. Returns an empty object (rather\n * than undefined) when the entity has no registered tables — that\n * simplifies the common ContentClient lookup idiom:\n *\n * const { drafts } = getContentTables(entity.name)\n * if (!drafts) return null\n */\nexport function getContentTables(entityName: string): ContentTables {\n return tables.get(entityName) ?? {}\n}\n\n/**\n * Clear the registry. **Test-only.** Throws in production.\n */\nexport function clearContentTables(): void {\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'clearContentTables() is not allowed in production — this is a test-only utility.',\n )\n }\n tables.clear()\n}\n","/**\n * Draft overlay table schema generator\n *\n * Generates a {entity}_drafts table for entities with the publishable() behavior.\n * Stores a mutable work-in-progress snapshot that non-publishers edit instead of\n * writing directly to the live published row.\n *\n * One draft per entity+locale. Drafts are upserted, discarded, or promoted to live.\n *\n * Uses `defineTable` so ContentClient can operate on drafts through a typed\n * TableClient (see registry.ts). The migration pipeline uses the\n * entity-package generator (which emits FK references) — this one produces a\n * runtime-only table shape for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a publishable entity's drafts table.\n *\n * Schema:\n * {entity}_drafts (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_',\n * data jsonb NOT NULL, -- full entity snapshot\n * created_by varchar(255) NOT NULL, -- user ID (varchar, NOT uuid!)\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * updated_at timestamptz NOT NULL DEFAULT now(),\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateDraftsTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_drafts`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n createdBy: column.varchar({ length: 255, notNull: true, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type DraftsTable = ReturnType<typeof generateDraftsTable>\n","/**\n * Per-locale publish status table schema generator\n *\n * Generates a {entity}_locale_status table for entities that have both\n * publishable() behavior AND translatable fields. This allows different\n * locales to have independent publish statuses.\n *\n * Fallback rule: no row for a locale = inherit base entity status.\n *\n * Uses `defineTable` so ContentClient can operate on rows through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references); this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a publishable + translatable entity's\n * locale status table.\n *\n * Schema:\n * {entity}_locale_status (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL,\n * status varchar(20) NOT NULL DEFAULT 'draft',\n * published_at timestamptz,\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateLocaleStatusTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_locale_status`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true }),\n status: column.varchar({ length: 20, notNull: true, default: 'draft' }),\n publishedAt: column.timestamp({ withTimezone: true, pgName: 'published_at' }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type LocaleStatusTable = ReturnType<typeof generateLocaleStatusTable>\n","/**\n * Version history table schema generator\n *\n * Generates a {entity}_versions table for entities with the versionable() behavior.\n * Each version stores a full snapshot + delta (changed fields only).\n *\n * Uses `defineTable` so ContentClient can operate on versions through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references) — this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a versionable entity's versions table.\n *\n * Schema:\n * {entity}_versions (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * version integer NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_', -- locale this snapshot belongs to\n * data jsonb NOT NULL, -- full snapshot (locale-merged view)\n * delta jsonb, -- only changed fields\n * status varchar(20), -- 'draft' | 'published' at time of save\n * created_by varchar(255),\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * is_autosave boolean NOT NULL DEFAULT false,\n * UNIQUE(entity_id, version, locale)\n * )\n *\n * Each locale has its own version sequence. The default locale uses '_'\n * as the locale value (distinguishable from actual locale codes).\n */\nexport function generateVersionsTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_versions`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n version: column.integer({ notNull: true }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n delta: column.jsonb<Record<string, unknown>>(),\n status: column.varchar({ length: 20 }),\n createdBy: column.varchar({ length: 255, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n isAutosave: column.boolean({ notNull: true, default: false, pgName: 'is_autosave' }),\n },\n unique: [{ on: ['entityId', 'version', 'locale'] }],\n })\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type VersionsTable = ReturnType<typeof generateVersionsTable>\n","/**\n * Content plugin — registers locale configuration and version schemas.\n *\n * Usage:\n * import { content } from '@murumets-ee/content/plugin'\n *\n * export default defineLumiConfig({\n * plugins: [\n * content({\n * locales: [\n * { code: 'en', label: 'English' },\n * { code: 'de', label: 'German' },\n * ],\n * defaultLocale: 'en',\n * }),\n * ],\n * })\n */\n\nimport type { Plugin } from '@murumets-ee/core'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { generateLayoutSchema, generateLayoutTranslationSchema } from '@murumets-ee/entity'\nimport { generateDraftsTable } from './schema/drafts.js'\nimport { generateLocaleStatusTable } from './schema/locale-status.js'\nimport { toolkitContentLocks } from './schema/locks.js'\nimport { registerContentTables } from './schema/registry.js'\nimport { generateVersionsTable } from './schema/versions.js'\nimport type { ContentConfig } from './types.js'\n\nlet _contentConfig: ContentConfig | null = null\n\n/**\n * Get the content configuration. Throws if plugin not initialized.\n */\nexport function getContentConfig(): ContentConfig {\n if (!_contentConfig) {\n throw new Error(\n '@murumets-ee/content plugin not initialized. Add content() to your plugins array.',\n )\n }\n return _contentConfig\n}\n\n/**\n * Content plugin factory.\n */\nexport function content(config: ContentConfig): 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 server: {\n init: async (app) => {\n // Store config globally\n _contentConfig = config\n\n let versionableCount = 0\n let blocksCount = 0\n let localeStatusCount = 0\n let draftsCount = 0\n\n // Register shared locks table (once, outside entity loop)\n if (!schemaRegistry.has('toolkit_content_locks')) {\n schemaRegistry.register('toolkit_content_locks', toolkitContentLocks)\n }\n\n for (const [name, entity] of app.entities) {\n // Register version tables for versionable entities\n const hasVersionable = entity.behaviors?.some(\n (b: { name: string }) => b.name === 'versionable',\n )\n if (hasVersionable) {\n const versionsTableName = `${name}_versions`\n if (!schemaRegistry.has(versionsTableName)) {\n const versions = generateVersionsTable(entity)\n schemaRegistry.register(versionsTableName, versions.table)\n registerContentTables(name, { versions })\n }\n versionableCount++\n }\n\n // Register layout tables for entities with blocks fields\n const allFields = entity.allFields as Record<\n string,\n { type: string; translatable?: boolean }\n >\n const hasBlocks = Object.values(allFields).some((f) => f.type === 'blocks')\n if (hasBlocks) {\n const layoutTableName = `${name}_layout`\n if (!schemaRegistry.has(layoutTableName)) {\n const layoutSchema = generateLayoutSchema(entity)\n if (layoutSchema) {\n schemaRegistry.register(layoutTableName, layoutSchema)\n }\n }\n\n const layoutTransTableName = `${name}_layout_translations`\n if (!schemaRegistry.has(layoutTransTableName)) {\n const layoutTransSchema = generateLayoutTranslationSchema(entity)\n if (layoutTransSchema) {\n schemaRegistry.register(layoutTransTableName, layoutTransSchema)\n }\n }\n\n blocksCount++\n }\n\n // Register locale_status tables for publishable + translatable entities\n const isPublishable = entity.behaviors?.some(\n (b: { name: string }) => b.name === 'publishable',\n )\n const hasTranslatable = Object.values(allFields).some((f) => f.translatable)\n if (isPublishable && hasTranslatable) {\n const statusTableName = `${name}_locale_status`\n if (!schemaRegistry.has(statusTableName)) {\n const localeStatus = generateLocaleStatusTable(entity)\n schemaRegistry.register(statusTableName, localeStatus.table)\n registerContentTables(name, { localeStatus })\n }\n localeStatusCount++\n }\n\n // Register drafts table for publishable entities\n if (isPublishable) {\n const draftsTableName = `${name}_drafts`\n if (!schemaRegistry.has(draftsTableName)) {\n const drafts = generateDraftsTable(entity)\n schemaRegistry.register(draftsTableName, drafts.table)\n registerContentTables(name, { drafts })\n }\n draftsCount++\n }\n }\n\n app.logger.info(\n {\n locales: config.locales.map((l) => l.code),\n defaultLocale: config.defaultLocale,\n versionableEntities: versionableCount,\n blockEntities: blocksCount,\n localeStatusEntities: localeStatusCount,\n draftEntities: draftsCount,\n },\n 'Content plugin initialized',\n )\n },\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/plugin.d.mts
CHANGED
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;;;;;iBAkCgB,gBAAA,CAAA,GAAoB,aAAA;;;;iBAYpB,OAAA,CAAQ,MAAA,EAAQ,aAAA,GAAgB,MAAA"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//# sourceMappingURL=plugin.mjs.map
|
|
1
|
+
import{n as e,t}from"./plugin-yijaaFS7.mjs";export{t as content,e as getContentConfig};
|
package/dist/query-client.d.mts
CHANGED
|
@@ -120,7 +120,7 @@ declare class ContentQueryClient {
|
|
|
120
120
|
* Factory — creates a ContentQueryClient for an entity.
|
|
121
121
|
* Must be called after createApp().
|
|
122
122
|
*/
|
|
123
|
-
declare function createContentQueryClient(entity: Entity):
|
|
123
|
+
declare function createContentQueryClient(entity: Entity): ContentQueryClient;
|
|
124
124
|
//#endregion
|
|
125
125
|
export { ContentQueryClient, ContentQueryClientConfig, createContentQueryClient };
|
|
126
126
|
//# sourceMappingURL=query-client.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-client.d.mts","names":[],"sources":["../src/query-client.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"query-client.d.mts","names":[],"sources":["../src/query-client.ts"],"mappings":";;;;UA6BiB,wBAAA;EACf,KAAA,EAAO,WAAA;EACP,MAAA,EAAQ,MAAA;AAAA;AAAA,cAGG,kBAAA;EAAA,QACH,MAAA;EAAA,QACA,KAAA;cAEI,MAAA,EAAQ,wBAAA;EASd,QAAA,CAAS,EAAA,UAAY,OAAA,GAAU,eAAA,GAAe,OAAA;;;;iDAsBZ,gBAAA;IAAA;MAAA,wEAIN,gBAAA,0BAAA;IAAA;MAAA,qGAAA,gBAAA,0BAAA,0BAAA;IAAA;MAAA,kIArC1B,gBAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,+JAEY,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,4LASd,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,yNAA+B,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,sPAAe,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA;;;;;;;;;;;;;;;;EAO9C,UAAA,CAAW,IAAA,UAAc,OAAA;IAAY,MAAA;EAAA,IAAiB,OAAA;;;gZAA7B;MAAA,2CAA6B,gBAAA;IAAA;MAAA;;;;;;;;;;;;;;;;;;mVAetD,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,0WAAS,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA;;;;;EAAT,QAAA,CAAS,OAAA,GAAU,eAAA,GAAe,OAAA;;;;;;;;;;;;;;kMAIlC,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,yNAAM,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,sPAAsB,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,mRASI,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,gTAAoC,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,6UAA3B,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA,0WAAS,gBAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA,0BAAA;IAAA;MAAA;;;;;EATlD,KAAA,CAAM,OAAA,GAAU,YAAA,GAAY,OAAA;AAAA;;;;;iBASpB,wBAAA,CAAyB,MAAA,EAAQ,MAAA,GAAS,kBAAA"}
|
package/dist/query-client.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
import{t as e}from"./locale-utils-By7EdAT1.mjs";import"server-only";import{createQueryClient as t}from"@murumets-ee/core/clients";import{schemaRegistry as n}from"@murumets-ee/db";import{eq as r}from"drizzle-orm";var i=class{entity;query;constructor(e){this.entity=e.entity,this.query=e.query}async findById(e,t){return this.query.findById(e,t)}async findBySlug(t,i){i?.locale!==void 0&&e(i.locale);let a=n.get(this.entity.name);return!a||!a.slug?null:(await this.query.findMany({where:r(a.slug,t),limit:1,locale:i?.locale}))[0]??null}async findMany(e){return this.query.findMany(e)}async count(e){return this.query.count(e)}};function a(e){return new i({query:t(e),entity:e})}export{i as ContentQueryClient,a as createContentQueryClient};
|
|
2
2
|
//# sourceMappingURL=query-client.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-client.mjs","names":[],"sources":["../src/query-client.ts"],"sourcesContent":["/**\n * ContentQueryClient — read-only content queries
|
|
1
|
+
{"version":3,"file":"query-client.mjs","names":[],"sources":["../src/query-client.ts"],"sourcesContent":["/**\n * ContentQueryClient — read-only content queries.\n *\n * Server-side only. Reaches into `@murumets-ee/db`'s schemaRegistry for\n * slug-based lookups, which is populated by the plugin at app boot — so\n * this module cannot run in a client bundle. The dedicated subpath export\n * exists so apps that only need reads (e.g. SSR pages, static rendering)\n * don't pull in the full AdminClient mutation surface.\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 'server-only'\n\nimport { createQueryClient } from '@murumets-ee/core/clients'\nimport { schemaRegistry } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\nimport type {\n CountOptions,\n FindByIdOptions,\n FindManyOptions,\n QueryClient,\n} from '@murumets-ee/entity/query'\nimport { eq } from 'drizzle-orm'\nimport { validateLocale } from './locale-utils.js'\n\nexport interface ContentQueryClientConfig {\n query: QueryClient\n entity: Entity\n}\n\nexport class ContentQueryClient {\n private entity: Entity\n private query: QueryClient\n\n constructor(config: ContentQueryClientConfig) {\n this.entity = config.entity\n this.query = config.query\n }\n\n // ---------------------------------------------------------------\n // Read operations (delegated to QueryClient)\n // ---------------------------------------------------------------\n\n async findById(id: string, options?: FindByIdOptions) {\n return this.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 if (options?.locale !== undefined) validateLocale(options.locale)\n const table = schemaRegistry.get(this.entity.name)\n if (!table || !table.slug) return null\n\n // Build where clause with slug filter\n const results = await this.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 return this.query.findMany(options)\n }\n\n async count(options?: CountOptions) {\n return this.query.count(options)\n }\n}\n\n/**\n * Factory — creates a ContentQueryClient for an entity.\n * Must be called after createApp().\n */\nexport function createContentQueryClient(entity: Entity): ContentQueryClient {\n const query = createQueryClient(entity)\n return new ContentQueryClient({\n query,\n entity,\n })\n}\n"],"mappings":"oNAkCA,IAAa,EAAb,KAAgC,CAC9B,OACA,MAEA,YAAY,EAAkC,CAC5C,KAAK,OAAS,EAAO,OACrB,KAAK,MAAQ,EAAO,MAOtB,MAAM,SAAS,EAAY,EAA2B,CACpD,OAAO,KAAK,MAAM,SAAS,EAAI,EAAQ,CAMzC,MAAM,WAAW,EAAc,EAA+B,CACxD,GAAS,SAAW,IAAA,IAAW,EAAe,EAAQ,OAAO,CACjE,IAAM,EAAQ,EAAe,IAAI,KAAK,OAAO,KAAK,CAUlD,MATI,CAAC,GAAS,CAAC,EAAM,KAAa,MAS3B,MANe,KAAK,MAAM,SAAS,CACxC,MAAO,EAAG,EAAM,KAAM,EAAK,CAC3B,MAAO,EACP,OAAQ,GAAS,OAClB,CAAC,EAEa,IAAM,KAGvB,MAAM,SAAS,EAA2B,CACxC,OAAO,KAAK,MAAM,SAAS,EAAQ,CAGrC,MAAM,MAAM,EAAwB,CAClC,OAAO,KAAK,MAAM,MAAM,EAAQ,GAQpC,SAAgB,EAAyB,EAAoC,CAE3E,OAAO,IAAI,EAAmB,CAC5B,MAFY,EAAkB,EAEzB,CACL,SACD,CAAC"}
|
|
@@ -41,13 +41,13 @@ interface VersionableOptions {
|
|
|
41
41
|
/** Enable autosave (single mutable autosave version). Default: false */
|
|
42
42
|
autosave?: boolean;
|
|
43
43
|
}
|
|
44
|
-
interface VersionEntry {
|
|
44
|
+
interface VersionEntry<TData extends Record<string, unknown> = Record<string, unknown>> {
|
|
45
45
|
id: string;
|
|
46
46
|
entityId: string;
|
|
47
47
|
version: number;
|
|
48
48
|
locale: string;
|
|
49
|
-
data:
|
|
50
|
-
delta:
|
|
49
|
+
data: TData;
|
|
50
|
+
delta: Partial<TData> | null;
|
|
51
51
|
status: string | null;
|
|
52
52
|
createdBy: string | null;
|
|
53
53
|
createdByName: string | null;
|
|
@@ -74,11 +74,11 @@ interface LocalePublishStatus {
|
|
|
74
74
|
status: 'draft' | 'published';
|
|
75
75
|
publishedAt: Date | string | null;
|
|
76
76
|
}
|
|
77
|
-
interface DraftEntry {
|
|
77
|
+
interface DraftEntry<TData extends Record<string, unknown> = Record<string, unknown>> {
|
|
78
78
|
id: string;
|
|
79
79
|
entityId: string;
|
|
80
80
|
locale: string;
|
|
81
|
-
data:
|
|
81
|
+
data: TData;
|
|
82
82
|
createdBy: string;
|
|
83
83
|
createdByName: string | null;
|
|
84
84
|
createdAt: Date | string;
|
|
@@ -92,4 +92,4 @@ interface LockInfo {
|
|
|
92
92
|
}
|
|
93
93
|
//#endregion
|
|
94
94
|
export { InferBlocksUnion as a, LockInfo as c, TranslationStatus as d, VersionEntry as f, InferBlockData as i, TranslationFieldStatus as l, ContentConfig as n, LocaleConfig as o, VersionableOptions as p, DraftEntry as r, LocalePublishStatus as s, BlockDefinition as t, TranslationReport as u };
|
|
95
|
-
//# sourceMappingURL=types-
|
|
95
|
+
//# sourceMappingURL=types-BCmX1nBL.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-BCmX1nBL.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;UAeiB,eAAA,WACL,MAAA,SAAe,WAAA,IAAe,MAAA,SAAe,WAAA;EAEvD,IAAA;EACA,KAAA;EACA,MAAA,EAAQ,CAAA;AAAA;;;;;KAWE,cAAA,WAAyB,eAAA,IACnC,CAAA,SAAU,eAAA;EACJ,MAAA,EAAQ,CAAA;EAAW,GAAA;AAAA,IAAgB,IAAA,CAAK,cAAA,CAAe,CAAA;;;AAF/D;;;;;KAYY,gBAAA,oBAAoC,eAAA,oBAClC,CAAA,GAAI,CAAA,CAAE,CAAA,UAAW,eAAA,GAAkB,cAAA,CAAe,CAAA,CAAE,CAAA;AAAA,UAOjD,YAAA;EACf,IAAA;EACA,KAAA;AAAA;AAAA,UAGe,aAAA;EACf,OAAA,EAAS,YAAA;EACT,aAAA;AAAA;AAAA,UAOe,kBAAA;EAhCT;EAkCN,SAAA;EAlCyB;EAoCzB,QAAA;AAAA;AAAA,UAOe,YAAA,eAA2B,MAAA,oBAA0B,MAAA;EACpE,EAAA;EACA,QAAA;EACA,OAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,KAAA,EAAO,OAAA,CAAQ,KAAA;EACf,MAAA;EACA,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,UAAA;AAAA;AAAA,UAae,sBAAA;EACf,QAAA;EACA,KAAA;EACA,UAAA;EACA,OAAA;AAAA;AAAA,UAGe,iBAAA;EAAA,CACd,MAAA,WAAiB,sBAAA;AAAA;AAAA,UAGH,iBAAA;EACf,KAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;AAAA;AAAA,UAOe,mBAAA;EACf,MAAA;EACA,MAAA;EACA,WAAA,EAAa,IAAA;AAAA;AAAA,UAOE,UAAA,eAAyB,MAAA,oBAA0B,MAAA;EAClE,EAAA;EACA,QAAA;EACA,MAAA;EACA,IAAA,EAAM,KAAA;EACN,SAAA;EACA,aAAA;EACA,SAAA,EAAW,IAAA;EACX,SAAA,EAAW,IAAA;AAAA;AAAA,UAOI,QAAA;EACf,QAAA;EACA,YAAA;EACA,QAAA,EAAU,IAAA;EACV,SAAA,EAAW,IAAA;AAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{field as e}from"@murumets-ee/entity";const t={maxPerDoc:50,autosave:!1};function n(n){let r={...t,...n};if(!Number.isInteger(r.maxPerDoc)||r.maxPerDoc<1)throw Error(`versionable({ maxPerDoc: ${r.maxPerDoc} }) — maxPerDoc must be a positive integer.`);return{name:`versionable`,fields:{_version:e.number({required:!0,default:1,integer:!0,internal:!0}),_versionedAt:e.date({internal:!0})},hooks:{beforeCreate:async e=>(e._version=1,e._versionedAt=new Date,e),beforeUpdate:async(e,t)=>(t._versionedAt=new Date,t)},_versionableOptions:r}}function r(e){return e?e.find(e=>e.name===`versionable`)?._versionableOptions??null:null}export{n,r as t};
|
|
2
|
+
//# sourceMappingURL=versionable-B0bP2Gib.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"versionable-B0bP2Gib.mjs","names":[],"sources":["../src/behaviors/versionable.ts"],"sourcesContent":["/**\n * Versionable behavior — full version history with snapshots.\n *\n * Extends the concept of revisionable() (which only tracks a counter)\n * with actual version snapshots stored in {entity}_versions table.\n *\n * The behavior itself adds fields + hooks for version tracking.\n * Actual snapshot persistence happens in ContentClient (which has DB access).\n *\n * Options:\n * - maxPerDoc: Maximum versions kept per document (default: 50)\n * - autosave: Enable smart autosave (default: false)\n */\n\nimport type { Behavior, DateField, NumberField } from '@murumets-ee/entity'\n\nimport { field } from '@murumets-ee/entity'\nimport type { VersionableOptions } from '../types.js'\n\nexport type VersionableFields = {\n _version: NumberField & { required: true }\n _versionedAt: DateField\n}\n\nconst DEFAULT_OPTIONS: Required<VersionableOptions> = {\n maxPerDoc: 50,\n autosave: false,\n}\n\nexport function versionable(options?: VersionableOptions): Behavior<VersionableFields> {\n const opts = { ...DEFAULT_OPTIONS, ...options }\n\n // Clamp maxPerDoc to a positive integer. A zero or negative value would\n // propagate to `pruneVersions` where `client.deleteMany({ id: { notIn: [] } })`\n // could match every row — i.e. delete the entire version history for\n // that entity+locale. Surface the misconfiguration here, not at first prune.\n if (!Number.isInteger(opts.maxPerDoc) || opts.maxPerDoc < 1) {\n throw new Error(\n `versionable({ maxPerDoc: ${opts.maxPerDoc} }) — maxPerDoc must be a positive integer.`,\n )\n }\n\n // Attach options to the behavior for ContentClient to read\n const behavior: Behavior<VersionableFields> & {\n _versionableOptions: Required<VersionableOptions>\n } = {\n name: 'versionable',\n fields: {\n // Both fields are managed by the versionable hooks + ContentClient's\n // snapshot persistence. The HTTP PATCH surface must not be able to\n // poison either — `_version` is also in `validation.ts`'s legacy omit\n // list (so this is belt-and-braces for `_version`), but `_versionedAt`\n // was previously writable via PATCH and only stripped from the read\n // DTO. `internal: true` closes that gap and makes the boundary\n // consistent with workflowable.\n _version: field.number({ required: true, default: 1, integer: true, internal: true }),\n _versionedAt: field.date({ internal: true }),\n },\n hooks: {\n beforeCreate: async (data) => {\n data._version = 1\n data._versionedAt = new Date()\n return data\n },\n beforeUpdate: async (_id, data) => {\n // Only update _versionedAt here. The _version increment is handled by\n // createVersionSnapshot which reads the current version from the DB.\n // The beforeUpdate hook only receives partial data (the PATCH payload),\n // so it can't reliably increment _version without the current value.\n data._versionedAt = new Date()\n return data\n },\n },\n _versionableOptions: opts,\n }\n\n return behavior as Behavior<VersionableFields>\n}\n\n/**\n * Extract versionable options from an entity's behaviors\n */\nexport function getVersionableOptions(behaviors?: Behavior[]): Required<VersionableOptions> | null {\n if (!behaviors) return null\n const vBehavior = behaviors.find((b) => b.name === 'versionable') as\n | (Behavior & { _versionableOptions?: Required<VersionableOptions> })\n | undefined\n return vBehavior?._versionableOptions ?? null\n}\n"],"mappings":"4CAwBA,MAAM,EAAgD,CACpD,UAAW,GACX,SAAU,GACX,CAED,SAAgB,EAAY,EAA2D,CACrF,IAAM,EAAO,CAAE,GAAG,EAAiB,GAAG,EAAS,CAM/C,GAAI,CAAC,OAAO,UAAU,EAAK,UAAU,EAAI,EAAK,UAAY,EACxD,MAAU,MACR,4BAA4B,EAAK,UAAU,6CAC5C,CAqCH,MAAO,CA9BL,KAAM,cACN,OAAQ,CAQN,SAAU,EAAM,OAAO,CAAE,SAAU,GAAM,QAAS,EAAG,QAAS,GAAM,SAAU,GAAM,CAAC,CACrF,aAAc,EAAM,KAAK,CAAE,SAAU,GAAM,CAAC,CAC7C,CACD,MAAO,CACL,aAAc,KAAO,KACnB,EAAK,SAAW,EAChB,EAAK,aAAe,IAAI,KACjB,GAET,aAAc,MAAO,EAAK,KAKxB,EAAK,aAAe,IAAI,KACjB,GAEV,CACD,oBAAqB,EAGR,CAMjB,SAAgB,EAAsB,EAA6D,CAKjG,OAJK,EACa,EAAU,KAAM,GAAM,EAAE,OAAS,cAGnC,EAAE,qBAAuB,KAJlB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/content",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"drizzle-orm": "^0.45.2",
|
|
37
37
|
"server-only": "^0.0.1",
|
|
38
38
|
"zod": "^3.24.1",
|
|
39
|
-
"@murumets-ee/core": "0.
|
|
40
|
-
"@murumets-ee/db": "0.
|
|
41
|
-
"@murumets-ee/entity": "0.
|
|
42
|
-
"@murumets-ee/logging": "0.
|
|
39
|
+
"@murumets-ee/core": "0.12.0",
|
|
40
|
+
"@murumets-ee/db": "0.12.0",
|
|
41
|
+
"@murumets-ee/entity": "0.12.0",
|
|
42
|
+
"@murumets-ee/logging": "0.12.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@types/node": "^
|
|
46
|
-
"tsdown": "^0.21.
|
|
45
|
+
"@types/node": "^20.19.39",
|
|
46
|
+
"tsdown": "^0.21.10",
|
|
47
47
|
"typescript": "^5.7.3",
|
|
48
48
|
"vitest": "^2.1.8"
|
|
49
49
|
},
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{field as e}from"@murumets-ee/entity";const t={maxPerDoc:50,autosave:!1};function n(n){let r={...t,...n};return{name:`versionable`,fields:{_version:e.number({required:!0,default:1,integer:!0}),_versionedAt:e.date()},hooks:{beforeCreate:async e=>(e._version=1,e._versionedAt=new Date,e),beforeUpdate:async(e,t)=>(t._versionedAt=new Date,t)},_versionableOptions:r}}function r(e){return e?e.find(e=>e.name===`versionable`)?._versionableOptions??null:null}function i(e){return e.match(/\S+|\s+/g)??[]}function a(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=i(e),r=i(t),a=n.length,o=r.length,s=Array.from({length:a+1},()=>Array(o+1).fill(0));for(let e=1;e<=a;e++)for(let t=1;t<=o;t++)n[e-1]===r[t-1]?s[e][t]=s[e-1][t-1]+1:s[e][t]=Math.max(s[e-1][t],s[e][t-1]);let c=[],l=a,u=o,d=[];for(;l>0||u>0;)l>0&&u>0&&n[l-1]===r[u-1]?(d.push({type:`equal`,value:n[l-1]}),l--,u--):u>0&&(l===0||s[l][u-1]>=s[l-1][u])?(d.push({type:`add`,value:r[u-1]}),u--):(d.push({type:`remove`,value:n[l-1]}),l--);d.reverse();for(let e of d){let t=c[c.length-1];t&&t.type===e.type?t.value+=e.value:c.push({...e})}return c}function o(e){return e.replace(/<[^>]*>/g,` `).replace(/\s+/g,` `).trim()}function s(e){return e==null?``:e instanceof Date?e.toISOString():typeof e==`object`?JSON.stringify(e):String(e)}function c(e,t,n,r){if(JSON.stringify(t)===JSON.stringify(n))return null;switch(r?.type??u(t,n)){case`text`:case`slug`:return{field:e,type:`text`,from:t,to:n,segments:a(s(t),s(n))};case`richtext`:return{field:e,type:`richtext`,from:t,to:n,segments:a(o(s(t)),o(s(n)))};case`number`:return{field:e,type:`number`,from:t,to:n};case`boolean`:return{field:e,type:`boolean`,from:t,to:n};case`date`:return{field:e,type:`date`,from:t,to:n};case`select`:return{field:e,type:`select`,from:t,to:n};case`media`:return{field:e,type:`media`,from:t,to:n};case`json`:return{field:e,type:`json`,from:t,to:n};case`blocks`:return{field:e,type:`blocks`,from:t,to:n};default:return{field:e,type:`unknown`,from:t,to:n}}}function l(e,t,n){let r=new Set([...Object.keys(e),...Object.keys(t)]),i=[];for(let a of r){if(a===`id`||a===`createdAt`||a===`updatedAt`)continue;let r=c(a,e[a],t[a],n?.[a]);r&&i.push(r)}return i}function u(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 d(e,t){if(typeof e==`string`||typeof t==`string`)return a(s(e),s(t))}function f(e){return typeof e==`object`&&!!e&&`_block`in e&&`_id`in e&&typeof e._block==`string`&&typeof e._id==`string`}function p(e,t){let n=new Set([...Object.keys(e),...Object.keys(t)]),r=[];for(let i of n){if(i===`_block`||i===`_id`)continue;let n=e[i],a=t[i];JSON.stringify(n)!==JSON.stringify(a)&&r.push({field:i,from:n,to:a,segments:d(n,a)})}return r}function m(e,t){let n=e.filter(f),r=t.filter(f),i=new Map,a=new Map;for(let e=0;e<n.length;e++)i.set(n[e]._id,{block:n[e],index:e});for(let e=0;e<r.length;e++)a.set(r[e]._id,{block:r[e],index:e});let o=[],s=new Set;for(let[e,{block:t,index:n}]of a){s.add(e);let r=i.get(e);if(!r){o.push({blockId:e,blockType:t._block,status:`added`,toIndex:n});continue}let a=r.block,c=r.index,l=p(a,t),u=c!==n;l.length===0&&!u?o.push({blockId:e,blockType:t._block,status:`unchanged`,fromIndex:c,toIndex:n}):l.length>0?o.push({blockId:e,blockType:t._block,status:`changed`,fromIndex:c,toIndex:n,propDiffs:l}):o.push({blockId:e,blockType:t._block,status:`moved`,fromIndex:c,toIndex:n})}for(let[e,{block:t,index:n}]of i)s.has(e)||o.push({blockId:e,blockType:t._block,status:`removed`,fromIndex:n});return o}export{a,d as i,c as n,r as o,l as r,n as s,m as t};
|
|
2
|
-
//# sourceMappingURL=block-diff-BBH6FoFT.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"block-diff-BBH6FoFT.mjs","names":[],"sources":["../src/behaviors/versionable.ts","../src/diff/text-diff.ts","../src/diff/field-diff.ts","../src/diff/block-diff.ts"],"sourcesContent":["/**\n * Versionable behavior — full version history with snapshots.\n *\n * Extends the concept of revisionable() (which only tracks a counter)\n * with actual version snapshots stored in {entity}_versions table.\n *\n * The behavior itself adds fields + hooks for version tracking.\n * Actual snapshot persistence happens in ContentClient (which has DB access).\n *\n * Options:\n * - maxPerDoc: Maximum versions kept per document (default: 50)\n * - autosave: Enable smart autosave (default: false)\n */\n\nimport type { Behavior, DateField, NumberField } from '@murumets-ee/entity'\n\nimport { field } from '@murumets-ee/entity'\nimport type { VersionableOptions } from '../types.js'\n\nexport type VersionableFields = {\n _version: NumberField & { required: true }\n _versionedAt: DateField\n}\n\nconst DEFAULT_OPTIONS: Required<VersionableOptions> = {\n maxPerDoc: 50,\n autosave: false,\n}\n\nexport function versionable(options?: VersionableOptions): Behavior<VersionableFields> {\n const opts = { ...DEFAULT_OPTIONS, ...options }\n\n // Attach options to the behavior for ContentClient to read\n const behavior: Behavior<VersionableFields> & {\n _versionableOptions: Required<VersionableOptions>\n } = {\n name: 'versionable',\n fields: {\n _version: field.number({ required: true, default: 1, integer: true }),\n _versionedAt: field.date(),\n },\n hooks: {\n beforeCreate: async (data) => {\n data._version = 1\n data._versionedAt = new Date()\n return data\n },\n beforeUpdate: async (_id, data) => {\n // Only update _versionedAt here. The _version increment is handled by\n // createVersionSnapshot which reads the current version from the DB.\n // The beforeUpdate hook only receives partial data (the PATCH payload),\n // so it can't reliably increment _version without the current value.\n data._versionedAt = new Date()\n return data\n },\n },\n _versionableOptions: opts,\n }\n\n return behavior as Behavior<VersionableFields>\n}\n\n/**\n * Extract versionable options from an entity's behaviors\n */\nexport function getVersionableOptions(behaviors?: Behavior[]): Required<VersionableOptions> | null {\n if (!behaviors) return null\n const vBehavior = behaviors.find((b) => b.name === 'versionable') as\n | (Behavior & { _versionableOptions?: Required<VersionableOptions> })\n | undefined\n return vBehavior?._versionableOptions ?? null\n}\n","/**\n * Word-level text diff using a simplified Myers algorithm.\n *\n * Splits text into words (preserving whitespace), computes the longest common\n * subsequence, and returns an array of diff segments.\n */\n\nimport type { DiffSegment } from './types.js'\n\n/**\n * Split text into tokens — words and whitespace runs — so diffs are\n * human-readable (\"Changed *this* word\") rather than character-level.\n */\nfunction tokenize(text: string): string[] {\n return text.match(/\\S+|\\s+/g) ?? []\n}\n\n/**\n * Compute word-level diff between two strings.\n *\n * Uses a simple LCS (dynamic programming) approach which is fine for\n * the text sizes we deal with (entity field values, not source code).\n */\nexport function diffText(from: string, to: string): DiffSegment[] {\n if (from === to) {\n return from ? [{ type: 'equal', value: from }] : []\n }\n if (!from) {\n return [{ type: 'add', value: to }]\n }\n if (!to) {\n return [{ type: 'remove', value: from }]\n }\n\n const fromTokens = tokenize(from)\n const toTokens = tokenize(to)\n\n // Build LCS table\n const m = fromTokens.length\n const n = toTokens.length\n const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0))\n\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n if (fromTokens[i - 1] === toTokens[j - 1]) {\n dp[i][j] = dp[i - 1][j - 1] + 1\n } else {\n dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])\n }\n }\n }\n\n // Backtrack to build diff segments\n const segments: DiffSegment[] = []\n let i = m\n let j = n\n\n // Collect in reverse, then flip\n const reversed: DiffSegment[] = []\n\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && fromTokens[i - 1] === toTokens[j - 1]) {\n reversed.push({ type: 'equal', value: fromTokens[i - 1] })\n i--\n j--\n } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {\n reversed.push({ type: 'add', value: toTokens[j - 1] })\n j--\n } else {\n reversed.push({ type: 'remove', value: fromTokens[i - 1] })\n i--\n }\n }\n\n reversed.reverse()\n\n // Merge consecutive segments of the same type\n for (const seg of reversed) {\n const last = segments[segments.length - 1]\n if (last && last.type === seg.type) {\n last.value += seg.value\n } else {\n segments.push({ ...seg })\n }\n }\n\n return segments\n}\n","/**\n * Field-level diff — dispatches to the appropriate diff strategy per field type.\n */\n\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport { diffText } from './text-diff.js'\nimport type { DiffSegment, FieldDiff } from './types.js'\n\n/** Strip HTML tags for diffing richtext content as plain text. */\nfunction stripHtml(html: string): string {\n return html\n .replace(/<[^>]*>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\n/** Stringify a value for display (dates, objects, etc.). */\nfunction stringify(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'object') return JSON.stringify(value)\n return String(value)\n}\n\n/**\n * Compute diff for a single field, using the field's type to determine\n * the best diff strategy.\n */\nexport function diffField(\n fieldName: string,\n from: unknown,\n to: unknown,\n fieldConfig?: FieldConfig,\n): FieldDiff | null {\n // No change\n if (JSON.stringify(from) === JSON.stringify(to)) return null\n\n const type = fieldConfig?.type ?? inferType(from, to)\n\n switch (type) {\n case 'text':\n case 'slug': {\n const segments = diffText(stringify(from), stringify(to))\n return { field: fieldName, type: 'text', from, to, segments }\n }\n\n case 'richtext': {\n // Diff on stripped HTML so users see word changes, not tag noise\n const segments = diffText(stripHtml(stringify(from)), stripHtml(stringify(to)))\n return { field: fieldName, type: 'richtext', from, to, segments }\n }\n\n case 'number':\n return { field: fieldName, type: 'number', from, to }\n\n case 'boolean':\n return { field: fieldName, type: 'boolean', from, to }\n\n case 'date':\n return { field: fieldName, type: 'date', from, to }\n\n case 'select':\n return { field: fieldName, type: 'select', from, to }\n\n case 'media':\n return { field: fieldName, type: 'media', from, to }\n\n case 'json':\n return { field: fieldName, type: 'json', from, to }\n\n case 'blocks':\n // Blocks are handled separately by block-diff.ts — just mark the field\n return { field: fieldName, type: 'blocks', from, to }\n\n default:\n return { field: fieldName, type: 'unknown', from, to }\n }\n}\n\n/**\n * Diff all fields between two version snapshots.\n * Pass fieldConfigs to get type-aware diffs; without them, types are inferred.\n */\nexport function diffFields(\n from: Record<string, unknown>,\n to: Record<string, unknown>,\n fieldConfigs?: Record<string, FieldConfig>,\n): FieldDiff[] {\n const allKeys = new Set([...Object.keys(from), ...Object.keys(to)])\n const diffs: FieldDiff[] = []\n\n for (const key of allKeys) {\n // Skip internal/auto-generated fields\n if (key === 'id' || key === 'createdAt' || key === 'updatedAt') continue\n\n const diff = diffField(key, from[key], to[key], fieldConfigs?.[key])\n if (diff) diffs.push(diff)\n }\n\n return diffs\n}\n\n/** Best-effort type inference when no FieldConfig is available. */\nfunction inferType(from: unknown, to: unknown): string {\n const val = from ?? to\n if (typeof val === 'number') return 'number'\n if (typeof val === 'boolean') return 'boolean'\n if (val instanceof Date) return 'date'\n if (typeof val === 'string') {\n if (/^\\d{4}-\\d{2}-\\d{2}/.test(val)) return 'date'\n return 'text'\n }\n if (Array.isArray(val)) {\n // Check if it looks like a blocks array\n if (val.length > 0 && typeof val[0] === 'object' && val[0] !== null && '_block' in val[0]) {\n return 'blocks'\n }\n return 'json'\n }\n if (typeof val === 'object') return 'json'\n return 'unknown'\n}\n\n/** Diff two string values and return segments (used by block-diff for string props). */\nexport function diffStringValues(from: unknown, to: unknown): DiffSegment[] | undefined {\n if (typeof from === 'string' || typeof to === 'string') {\n return diffText(stringify(from), stringify(to))\n }\n return undefined\n}\n","/**\n * Structural diff for Puck block arrays.\n *\n * Blocks are identified by `_id`. The algorithm detects:\n * - Added blocks (new _id in target)\n * - Removed blocks (missing _id in target)\n * - Moved blocks (same _id, different position)\n * - Changed blocks (same _id, different prop values)\n * - Unchanged blocks\n */\n\nimport { diffStringValues } from './field-diff.js'\nimport type { BlockDiffEntry, PropDiff } from './types.js'\n\ninterface Block {\n _block: string\n _id: string\n [key: string]: unknown\n}\n\nfunction isBlock(val: unknown): val is Block {\n return (\n typeof val === 'object' &&\n val !== null &&\n '_block' in val &&\n '_id' in val &&\n typeof (val as Block)._block === 'string' &&\n typeof (val as Block)._id === 'string'\n )\n}\n\n/**\n * Diff two prop objects within a single block, returning per-prop diffs.\n */\nfunction diffProps(\n fromProps: Record<string, unknown>,\n toProps: Record<string, unknown>,\n): PropDiff[] {\n const allKeys = new Set([...Object.keys(fromProps), ...Object.keys(toProps)])\n const diffs: PropDiff[] = []\n\n for (const key of allKeys) {\n // Skip block metadata\n if (key === '_block' || key === '_id') continue\n\n const fromVal = fromProps[key]\n const toVal = toProps[key]\n\n if (JSON.stringify(fromVal) !== JSON.stringify(toVal)) {\n diffs.push({\n field: key,\n from: fromVal,\n to: toVal,\n segments: diffStringValues(fromVal, toVal),\n })\n }\n }\n\n return diffs\n}\n\n/**\n * Compute structural diff between two block arrays.\n *\n * Each block is identified by its `_id` field. Position changes,\n * additions, removals, and prop-level changes are all detected.\n */\nexport function diffBlocks(fromBlocks: unknown[], toBlocks: unknown[]): BlockDiffEntry[] {\n const from = fromBlocks.filter(isBlock)\n const to = toBlocks.filter(isBlock)\n\n const fromMap = new Map<string, { block: Block; index: number }>()\n const toMap = new Map<string, { block: Block; index: number }>()\n\n for (let i = 0; i < from.length; i++) {\n fromMap.set(from[i]._id, { block: from[i], index: i })\n }\n for (let i = 0; i < to.length; i++) {\n toMap.set(to[i]._id, { block: to[i], index: i })\n }\n\n const entries: BlockDiffEntry[] = []\n const processedIds = new Set<string>()\n\n // Walk the \"to\" array to preserve target order\n for (const [id, { block: toBlock, index: toIndex }] of toMap) {\n processedIds.add(id)\n const fromEntry = fromMap.get(id)\n\n if (!fromEntry) {\n // Added\n entries.push({\n blockId: id,\n blockType: toBlock._block,\n status: 'added',\n toIndex,\n })\n continue\n }\n\n const fromBlock = fromEntry.block\n const fromIndex = fromEntry.index\n const propDiffs = diffProps(fromBlock, toBlock)\n const moved = fromIndex !== toIndex\n\n if (propDiffs.length === 0 && !moved) {\n entries.push({\n blockId: id,\n blockType: toBlock._block,\n status: 'unchanged',\n fromIndex,\n toIndex,\n })\n } else if (propDiffs.length > 0) {\n entries.push({\n blockId: id,\n blockType: toBlock._block,\n status: 'changed',\n fromIndex,\n toIndex,\n propDiffs,\n })\n } else {\n entries.push({\n blockId: id,\n blockType: toBlock._block,\n status: 'moved',\n fromIndex,\n toIndex,\n })\n }\n }\n\n // Removed blocks (in \"from\" but not in \"to\")\n for (const [id, { block, index }] of fromMap) {\n if (!processedIds.has(id)) {\n entries.push({\n blockId: id,\n blockType: block._block,\n status: 'removed',\n fromIndex: index,\n })\n }\n }\n\n return entries\n}\n"],"mappings":"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"}
|
package/dist/drafts-CyOivEtr.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{column as e,defineTable as t}from"@murumets-ee/db";function n(n){return t({name:`${n.name}_drafts`,columns:{id:e.uuid({primaryKey:!0,defaultRandom:!0}),entityId:e.uuid({notNull:!0,pgName:`entity_id`}),locale:e.varchar({length:10,notNull:!0,default:`_`}),data:e.jsonb({notNull:!0}),createdBy:e.varchar({length:255,notNull:!0,pgName:`created_by`}),createdByName:e.varchar({length:255,pgName:`created_by_name`}),createdAt:e.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),updatedAt:e.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`updated_at`})},unique:[{on:[`entityId`,`locale`]}]})}export{n as generateDraftsTable};
|
|
2
|
-
//# sourceMappingURL=drafts-CyOivEtr.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"drafts-CyOivEtr.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 * Uses `defineTable` so ContentClient can operate on drafts through a typed\n * TableClient (see registry.ts). The migration pipeline uses the\n * entity-package generator (which emits FK references) — this one produces a\n * runtime-only table shape for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a publishable entity's drafts table.\n *\n * Schema:\n * {entity}_drafts (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_',\n * data jsonb NOT NULL, -- full entity snapshot\n * created_by varchar(255) NOT NULL, -- user ID (varchar, NOT uuid!)\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * updated_at timestamptz NOT NULL DEFAULT now(),\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateDraftsTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_drafts`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n createdBy: column.varchar({ length: 255, notNull: true, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n updatedAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'updated_at',\n }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/**\n * Backwards-compatible wrapper: returns the raw Drizzle pgTable.\n * Still used by `plugin.ts` to register the table in `schemaRegistry`\n * so runtime consumers (QueryClient publish filter, etc.) can find it.\n */\nexport function generateDraftsSchema(entity: Entity) {\n return generateDraftsTable(entity).table\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type DraftsTable = ReturnType<typeof generateDraftsTable>\n"],"mappings":"0DAkCA,SAAgB,EAAoB,EAAgB,CAClD,OAAO,EAAY,CACjB,KAAM,GAAG,EAAO,KAAK,SACrB,QAAS,CACP,GAAI,EAAO,KAAK,CAAE,WAAY,GAAM,cAAe,GAAM,CAAC,CAC1D,SAAU,EAAO,KAAK,CAAE,QAAS,GAAM,OAAQ,YAAa,CAAC,CAC7D,OAAQ,EAAO,QAAQ,CAAE,OAAQ,GAAI,QAAS,GAAM,QAAS,IAAK,CAAC,CACnE,KAAM,EAAO,MAAqC,CAAE,QAAS,GAAM,CAAC,CACpE,UAAW,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,OAAQ,aAAc,CAAC,CAC/E,cAAe,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,kBAAmB,CAAC,CACzE,UAAW,EAAO,UAAU,CAC1B,QAAS,GACT,WAAY,GACZ,aAAc,GACd,OAAQ,aACT,CAAC,CACF,UAAW,EAAO,UAAU,CAC1B,QAAS,GACT,WAAY,GACZ,aAAc,GACd,OAAQ,aACT,CAAC,CACH,CACD,OAAQ,CAAC,CAAE,GAAI,CAAC,WAAY,SAAS,CAAE,CAAC,CACzC,CAAC"}
|
package/dist/index.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{column as e,defineTable as t}from"@murumets-ee/db";function n(n){return t({name:`${n.name}_locale_status`,columns:{id:e.uuid({primaryKey:!0,defaultRandom:!0}),entityId:e.uuid({notNull:!0,pgName:`entity_id`}),locale:e.varchar({length:10,notNull:!0}),status:e.varchar({length:20,notNull:!0,default:`draft`}),publishedAt:e.timestamp({withTimezone:!0,pgName:`published_at`})},unique:[{on:[`entityId`,`locale`]}]})}export{n as generateLocaleStatusTable};
|
|
2
|
-
//# sourceMappingURL=locale-status-DTyv4wcn.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"locale-status-DTyv4wcn.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 * Uses `defineTable` so ContentClient can operate on rows through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references); this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a publishable + translatable entity's\n * locale status table.\n *\n * Schema:\n * {entity}_locale_status (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * locale varchar(10) NOT NULL,\n * status varchar(20) NOT NULL DEFAULT 'draft',\n * published_at timestamptz,\n * UNIQUE(entity_id, locale)\n * )\n */\nexport function generateLocaleStatusTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_locale_status`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n locale: column.varchar({ length: 10, notNull: true }),\n status: column.varchar({ length: 20, notNull: true, default: 'draft' }),\n publishedAt: column.timestamp({ withTimezone: true, pgName: 'published_at' }),\n },\n unique: [{ on: ['entityId', 'locale'] }],\n })\n}\n\n/**\n * Backwards-compatible wrapper: returns the raw Drizzle pgTable.\n * Still used by `plugin.ts` to register the table in `schemaRegistry`\n * for QueryClient's `COALESCE` publish filter.\n */\nexport function generateLocaleStatusSchema(entity: Entity) {\n return generateLocaleStatusTable(entity).table\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type LocaleStatusTable = ReturnType<typeof generateLocaleStatusTable>\n"],"mappings":"0DAgCA,SAAgB,EAA0B,EAAgB,CACxD,OAAO,EAAY,CACjB,KAAM,GAAG,EAAO,KAAK,gBACrB,QAAS,CACP,GAAI,EAAO,KAAK,CAAE,WAAY,GAAM,cAAe,GAAM,CAAC,CAC1D,SAAU,EAAO,KAAK,CAAE,QAAS,GAAM,OAAQ,YAAa,CAAC,CAC7D,OAAQ,EAAO,QAAQ,CAAE,OAAQ,GAAI,QAAS,GAAM,CAAC,CACrD,OAAQ,EAAO,QAAQ,CAAE,OAAQ,GAAI,QAAS,GAAM,QAAS,QAAS,CAAC,CACvE,YAAa,EAAO,UAAU,CAAE,aAAc,GAAM,OAAQ,eAAgB,CAAC,CAC9E,CACD,OAAQ,CAAC,CAAE,GAAI,CAAC,WAAY,SAAS,CAAE,CAAC,CACzC,CAAC"}
|
package/dist/locks-BkO2qMfm.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./rolldown-runtime-DR3Ue2cl.mjs";import{column as t,defineTable as n}from"@murumets-ee/db";var r=e({contentLocksTable:()=>i,toolkitContentLocks:()=>a});const i=n({name:`toolkit_content_locks`,columns:{id:t.uuid({primaryKey:!0,defaultRandom:!0}),entityType:t.varchar({length:100,notNull:!0,pgName:`entity_type`}),entityId:t.varchar({length:255,notNull:!0,pgName:`entity_id`}),locale:t.varchar({length:10,notNull:!0,default:`_`}),lockedBy:t.varchar({length:255,notNull:!0,pgName:`locked_by`}),lockedByName:t.varchar({length:255,pgName:`locked_by_name`}),lockedAt:t.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`locked_at`}),expiresAt:t.timestamp({notNull:!0,withTimezone:!0,pgName:`expires_at`})},unique:[{on:[`entityType`,`entityId`,`locale`]}]}),a=i.table;export{r as n,i as t};
|
|
2
|
-
//# sourceMappingURL=locks-BkO2qMfm.mjs.map
|
package/dist/plugin.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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 defineLumiConfig({\n * plugins: [\n * content({\n * locales: [\n * { code: 'en', label: 'English' },\n * { code: 'de', label: 'German' },\n * ],\n * defaultLocale: 'en',\n * }),\n * ],\n * })\n */\n\nimport 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 server: {\n init: async (app) => {\n // Store config globally\n _contentConfig = config\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { generateVersionsTable } = await import('./schema/versions.js')\n const { generateLocaleStatusTable } = await import('./schema/locale-status.js')\n const { generateDraftsTable } = await import('./schema/drafts.js')\n const { registerContentTables } = await import('./schema/registry.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 versions = generateVersionsTable(entity)\n schemaRegistry.register(versionsTableName, versions.table)\n registerContentTables(name, { versions })\n }\n versionableCount++\n }\n\n // Register layout tables for entities with blocks fields\n const allFields = entity.allFields as Record<\n string,\n { type: string; translatable?: boolean }\n >\n const hasBlocks = Object.values(allFields).some((f) => f.type === 'blocks')\n if (hasBlocks) {\n const layoutTableName = `${name}_layout`\n if (!schemaRegistry.has(layoutTableName)) {\n const layoutSchema = generateLayoutSchema(entity)\n if (layoutSchema) {\n schemaRegistry.register(layoutTableName, layoutSchema)\n }\n }\n\n const layoutTransTableName = `${name}_layout_translations`\n if (!schemaRegistry.has(layoutTransTableName)) {\n const layoutTransSchema = generateLayoutTranslationSchema(entity)\n if (layoutTransSchema) {\n schemaRegistry.register(layoutTransTableName, layoutTransSchema)\n }\n }\n\n blocksCount++\n }\n\n // Register locale_status tables for publishable + translatable entities\n const isPublishable = entity.behaviors?.some(\n (b: { name: string }) => b.name === 'publishable',\n )\n const hasTranslatable = Object.values(allFields).some((f) => f.translatable)\n if (isPublishable && hasTranslatable) {\n const statusTableName = `${name}_locale_status`\n if (!schemaRegistry.has(statusTableName)) {\n const localeStatus = generateLocaleStatusTable(entity)\n schemaRegistry.register(statusTableName, localeStatus.table)\n registerContentTables(name, { localeStatus })\n }\n localeStatusCount++\n }\n\n // Register drafts table for publishable entities\n if (isPublishable) {\n const draftsTableName = `${name}_drafts`\n if (!schemaRegistry.has(draftsTableName)) {\n const drafts = generateDraftsTable(entity)\n schemaRegistry.register(draftsTableName, drafts.table)\n registerContentTables(name, { drafts })\n }\n draftsCount++\n }\n }\n\n app.logger.info(\n {\n locales: config.locales.map((l) => l.code),\n defaultLocale: config.defaultLocale,\n versionableEntities: versionableCount,\n blockEntities: blocksCount,\n localeStatusEntities: localeStatusCount,\n draftEntities: draftsCount,\n },\n 'Content plugin initialized',\n )\n },\n },\n }\n}\n"],"mappings":"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,OAAQ,CACN,KAAM,KAAO,IAAQ,CAEnB,EAAiB,EAEjB,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,yBAA0B,MAAM,OAAO,2BACzC,CAAE,6BAA8B,MAAM,OAAO,gCAC7C,CAAE,uBAAwB,MAAM,OAAO,yBACvC,CAAE,yBAA0B,MAAM,OAAO,2BAAA,KAAA,GAAA,EAAA,EAAA,CACzC,CAAE,uBAAwB,MAAM,OAAO,wBAAA,KAAA,GAAA,EAAA,EAAA,CACvC,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,EAAW,EAAsB,EAAO,CAC9C,EAAe,SAAS,EAAmB,EAAS,MAAM,CAC1D,EAAsB,EAAM,CAAE,WAAU,CAAC,CAE3C,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,EAA0B,EAAO,CACtD,EAAe,SAAS,EAAiB,EAAa,MAAM,CAC5D,EAAsB,EAAM,CAAE,eAAc,CAAC,CAE/C,IAIF,GAAI,EAAe,CACjB,IAAM,EAAkB,GAAG,EAAK,SAChC,GAAI,CAAC,EAAe,IAAI,EAAgB,CAAE,CACxC,IAAM,EAAS,EAAoB,EAAO,CAC1C,EAAe,SAAS,EAAiB,EAAO,MAAM,CACtD,EAAsB,EAAM,CAAE,SAAQ,CAAC,CAEzC,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,CACF"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./rolldown-runtime-DR3Ue2cl.mjs";var t=e({getContentTables:()=>i,registerContentTables:()=>r});const n=new Map;function r(e,t){let r=n.get(e)??{};n.set(e,{...r,...t})}function i(e){return n.get(e)??{}}export{t as n,i as t};
|
|
2
|
-
//# sourceMappingURL=registry-BJSOGFEN.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registry-BJSOGFEN.mjs","names":[],"sources":["../src/schema/registry.ts"],"sourcesContent":["/**\n * Per-entity content table registry.\n *\n * Maps an entity name to its content infrastructure tables — drafts,\n * versions, locale_status. Each entry stores the `DefinedTable` (from\n * `defineTable`), which exposes a typed `.makeClient(db)` for CRUD via\n * `TableClient` and a raw `.table` for legacy callers.\n *\n * The content plugin populates this registry at `init` time; ContentClient\n * reads from it lazily on first use. Tables that don't apply to an entity\n * (non-publishable entities have no drafts, etc.) are simply absent.\n */\n\nimport type { DraftsTable } from './drafts.js'\nimport type { LocaleStatusTable } from './locale-status.js'\nimport type { VersionsTable } from './versions.js'\n\n/**\n * The full set of optional content infra tables attached to an entity.\n * Every field is optional — entities without the relevant behavior don't\n * get the corresponding table.\n */\nexport interface ContentTables {\n /** `{entity}_drafts` — present when the entity has `publishable()`. */\n drafts?: DraftsTable\n /** `{entity}_versions` — present when the entity has `versionable()`. */\n versions?: VersionsTable\n /** `{entity}_locale_status` — present when the entity is publishable + has translatable fields. */\n localeStatus?: LocaleStatusTable\n}\n\n// Module-level map. Single source of truth for ContentClient lookups.\nconst tables = new Map<string, ContentTables>()\n\n/**\n * Register content tables for an entity. Called from `content().init()`.\n *\n * Subsequent calls for the same entity name MERGE the provided patch into\n * the existing entry. This lets the plugin add tables one at a time (drafts,\n * then versions, then locale_status) without the caller needing to build the\n * whole object up front.\n */\nexport function registerContentTables(entityName: string, patch: ContentTables): void {\n const existing = tables.get(entityName) ?? {}\n tables.set(entityName, { ...existing, ...patch })\n}\n\n/**\n * Get the content tables for an entity. Returns an empty object (rather\n * than undefined) when the entity has no registered tables — that\n * simplifies the common ContentClient lookup idiom:\n *\n * const { drafts } = getContentTables(entity.name)\n * if (!drafts) return null\n */\nexport function getContentTables(entityName: string): ContentTables {\n return tables.get(entityName) ?? {}\n}\n\n/**\n * Clear the registry. **Test-only.** Throws in production.\n */\nexport function clearContentTables(): void {\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'clearContentTables() is not allowed in production — this is a test-only utility.',\n )\n }\n tables.clear()\n}\n"],"mappings":"kHAgCA,MAAM,EAAS,IAAI,IAUnB,SAAgB,EAAsB,EAAoB,EAA4B,CACpF,IAAM,EAAW,EAAO,IAAI,EAAW,EAAI,EAAE,CAC7C,EAAO,IAAI,EAAY,CAAE,GAAG,EAAU,GAAG,EAAO,CAAC,CAWnD,SAAgB,EAAiB,EAAmC,CAClE,OAAO,EAAO,IAAI,EAAW,EAAI,EAAE"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r};export{t};
|
|
@@ -1,43 +0,0 @@
|
|
|
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-COcneY5H.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types-COcneY5H.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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types-D-ATlx7t.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"}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{column as e,defineTable as t}from"@murumets-ee/db";function n(n){return t({name:`${n.name}_versions`,columns:{id:e.uuid({primaryKey:!0,defaultRandom:!0}),entityId:e.uuid({notNull:!0,pgName:`entity_id`}),version:e.integer({notNull:!0}),locale:e.varchar({length:10,notNull:!0,default:`_`}),data:e.jsonb({notNull:!0}),delta:e.jsonb(),status:e.varchar({length:20}),createdBy:e.varchar({length:255,pgName:`created_by`}),createdByName:e.varchar({length:255,pgName:`created_by_name`}),createdAt:e.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`}),isAutosave:e.boolean({notNull:!0,default:!1,pgName:`is_autosave`})},unique:[{on:[`entityId`,`version`,`locale`]}]})}export{n as generateVersionsTable};
|
|
2
|
-
//# sourceMappingURL=versions-CO6oDyet.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"versions-CO6oDyet.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 * Uses `defineTable` so ContentClient can operate on versions through a typed\n * TableClient. The migration pipeline uses the entity-package generator\n * (which emits FK references) — this one produces a runtime-only table shape\n * for read/write via TableClient.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\nimport type { Entity } from '@murumets-ee/entity'\n\n/**\n * Generate a `DefinedTable` for a versionable entity's versions table.\n *\n * Schema:\n * {entity}_versions (\n * id uuid PK,\n * entity_id uuid NOT NULL,\n * version integer NOT NULL,\n * locale varchar(10) NOT NULL DEFAULT '_', -- locale this snapshot belongs to\n * data jsonb NOT NULL, -- full snapshot (locale-merged view)\n * delta jsonb, -- only changed fields\n * status varchar(20), -- 'draft' | 'published' at time of save\n * created_by varchar(255),\n * created_by_name varchar(255),\n * created_at timestamptz NOT NULL DEFAULT now(),\n * is_autosave boolean NOT NULL DEFAULT false,\n * UNIQUE(entity_id, version, locale)\n * )\n *\n * Each locale has its own version sequence. The default locale uses '_'\n * as the locale value (distinguishable from actual locale codes).\n */\nexport function generateVersionsTable(entity: Entity) {\n return defineTable({\n name: `${entity.name}_versions`,\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n entityId: column.uuid({ notNull: true, pgName: 'entity_id' }),\n version: column.integer({ notNull: true }),\n locale: column.varchar({ length: 10, notNull: true, default: '_' }),\n data: column.jsonb<Record<string, unknown>, true>({ notNull: true }),\n delta: column.jsonb<Record<string, unknown>>(),\n status: column.varchar({ length: 20 }),\n createdBy: column.varchar({ length: 255, pgName: 'created_by' }),\n createdByName: column.varchar({ length: 255, pgName: 'created_by_name' }),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n isAutosave: column.boolean({ notNull: true, default: false, pgName: 'is_autosave' }),\n },\n unique: [{ on: ['entityId', 'version', 'locale'] }],\n })\n}\n\n/**\n * Backwards-compatible wrapper: returns the raw Drizzle pgTable.\n * Still used by `plugin.ts` to register the table in `schemaRegistry`.\n */\nexport function generateVersionsSchema(entity: Entity) {\n return generateVersionsTable(entity).table\n}\n\n/** Public type export — the full DefinedTable returned by the generator. */\nexport type VersionsTable = ReturnType<typeof generateVersionsTable>\n"],"mappings":"0DAqCA,SAAgB,EAAsB,EAAgB,CACpD,OAAO,EAAY,CACjB,KAAM,GAAG,EAAO,KAAK,WACrB,QAAS,CACP,GAAI,EAAO,KAAK,CAAE,WAAY,GAAM,cAAe,GAAM,CAAC,CAC1D,SAAU,EAAO,KAAK,CAAE,QAAS,GAAM,OAAQ,YAAa,CAAC,CAC7D,QAAS,EAAO,QAAQ,CAAE,QAAS,GAAM,CAAC,CAC1C,OAAQ,EAAO,QAAQ,CAAE,OAAQ,GAAI,QAAS,GAAM,QAAS,IAAK,CAAC,CACnE,KAAM,EAAO,MAAqC,CAAE,QAAS,GAAM,CAAC,CACpE,MAAO,EAAO,OAAgC,CAC9C,OAAQ,EAAO,QAAQ,CAAE,OAAQ,GAAI,CAAC,CACtC,UAAW,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,aAAc,CAAC,CAChE,cAAe,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,kBAAmB,CAAC,CACzE,UAAW,EAAO,UAAU,CAC1B,QAAS,GACT,WAAY,GACZ,aAAc,GACd,OAAQ,aACT,CAAC,CACF,WAAY,EAAO,QAAQ,CAAE,QAAS,GAAM,QAAS,GAAO,OAAQ,cAAe,CAAC,CACrF,CACD,OAAQ,CAAC,CAAE,GAAI,CAAC,WAAY,UAAW,SAAS,CAAE,CAAC,CACpD,CAAC"}
|