@murumets-ee/content 0.3.0 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +94 -0
- package/dist/{block-diff-H1AttKiz.mjs → block-diff-BBH6FoFT.mjs} +1 -1
- package/dist/{block-diff-H1AttKiz.mjs.map → block-diff-BBH6FoFT.mjs.map} +1 -1
- package/dist/blocks/index.d.mts +1 -1
- package/dist/blocks/index.mjs +1 -1
- package/dist/client.d.mts +160 -18
- 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-D-7OpIn5.mjs → define-block-DpMPEEdA.mjs} +1 -1
- package/dist/{define-block-D-7OpIn5.mjs.map → define-block-DpMPEEdA.mjs.map} +1 -1
- package/dist/drafts-CyOivEtr.mjs +2 -0
- package/dist/drafts-CyOivEtr.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/locale-status-DTyv4wcn.mjs +2 -0
- package/dist/locale-status-DTyv4wcn.mjs.map +1 -0
- package/dist/lock-service.d.mts +3 -4
- 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-BkO2qMfm.mjs +2 -0
- package/dist/locks-BkO2qMfm.mjs.map +1 -0
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/query-client.d.mts +80 -14
- 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/registry-BJSOGFEN.mjs +2 -0
- package/dist/registry-BJSOGFEN.mjs.map +1 -0
- package/dist/rolldown-runtime-DR3Ue2cl.mjs +1 -0
- package/dist/{types-nXbE5Ufa.d.mts → types-COcneY5H.d.mts} +1 -1
- package/dist/{types-nXbE5Ufa.d.mts.map → types-COcneY5H.d.mts.map} +1 -1
- package/dist/{types-wLHyjcWf.d.mts → types-D-ATlx7t.d.mts} +1 -1
- package/dist/{types-wLHyjcWf.d.mts.map → types-D-ATlx7t.d.mts.map} +1 -1
- package/dist/versions-CO6oDyet.mjs +2 -0
- package/dist/versions-CO6oDyet.mjs.map +1 -0
- package/package.json +12 -12
- package/dist/drafts-BYyRdmiT.mjs +0 -2
- package/dist/drafts-BYyRdmiT.mjs.map +0 -1
- package/dist/locale-status-CGttT38v.mjs +0 -2
- package/dist/locale-status-CGttT38v.mjs.map +0 -1
- package/dist/locks-CNxQJieo.mjs +0 -2
- package/dist/locks-CNxQJieo.mjs.map +0 -1
- package/dist/versions-cotph2VG.mjs +0 -2
- package/dist/versions-cotph2VG.mjs.map +0 -1
package/dist/client.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * ContentClient — wraps AdminClient with version history and translation status.\n *\n * Usage:\n * import { createContentClient } from '@murumets-ee/content/client'\n * const pages = createContentClient(Page)\n * await pages.create({ title: 'Hello', layout: [...] })\n */\n\nimport 'server-only'\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { Entity, FieldConfig, FieldToTS } from '@murumets-ee/entity'\nimport type { AdminClient, FindManyOptions } from '@murumets-ee/entity/admin'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { getVersionableOptions } from './behaviors/versionable.js'\nimport { diffBlocks } from './diff/block-diff.js'\nimport { diffFields } from './diff/field-diff.js'\nimport type { FieldDiff } from './diff/types.js'\nimport type {\n DraftEntry,\n LocalePublishStatus,\n TranslationReport,\n TranslationStatus,\n VersionEntry,\n} from './types.js'\n\n/** Value type that any entity field can hold */\ntype EntityFieldValue = FieldToTS<FieldConfig> | null | undefined\n\nexport interface ContentClientConfig {\n entity: Entity\n db: PostgresJsDatabase\n logger?: Logger\n}\n\nexport class ContentClient {\n private entity: Entity\n private db: PostgresJsDatabase\n private logger?: Logger\n private admin: AdminClient | null = null\n private versionableOptions: ReturnType<typeof getVersionableOptions>\n\n constructor(config: ContentClientConfig) {\n this.entity = config.entity\n this.db = config.db\n this.logger = config.logger\n this.versionableOptions = getVersionableOptions(config.entity.behaviors)\n }\n\n private async getAdmin() {\n if (!this.admin) {\n const { AdminClient } = await import('@murumets-ee/entity/admin')\n this.admin = new AdminClient({\n entity: this.entity,\n db: this.db,\n logger: this.logger,\n })\n }\n return this.admin\n }\n\n // ---------------------------------------------------------------\n // Standard CRUD (delegated to AdminClient)\n // ---------------------------------------------------------------\n\n async create(data: Record<string, EntityFieldValue>) {\n const admin = await this.getAdmin()\n const result = await admin.create(data)\n\n // Save initial version snapshot\n if (this.versionableOptions) {\n await this.saveVersionSnapshot(result.id, result, null, false)\n }\n\n return result\n }\n\n async findById(id: string, options?: { locale?: string }) {\n const admin = await this.getAdmin()\n return admin.findById(id, options)\n }\n\n async findMany(options?: FindManyOptions) {\n const admin = await this.getAdmin()\n return admin.findMany(options)\n }\n\n async update(id: string, data: Record<string, EntityFieldValue>) {\n const admin = await this.getAdmin()\n\n // Get previous state for delta calculation\n let previousState: Record<string, unknown> | null = null\n if (this.versionableOptions) {\n previousState = await admin.findById(id)\n }\n\n const result = await admin.update(id, data)\n\n // Save version snapshot and update _version on the entity\n if (this.versionableOptions && previousState) {\n const delta = this.calculateDelta(previousState, result)\n const version = await this.saveVersionSnapshot(id, result, delta, false)\n\n if (version !== undefined) {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq } = await import('drizzle-orm')\n const table = schemaRegistry.get(this.entity.name)\n if (table) {\n await this.db.update(table).set({ _version: version }).where(eq(table.id, id))\n ;(result as Record<string, unknown>)._version = version\n }\n }\n\n await this.pruneVersions(id)\n }\n\n return result\n }\n\n async delete(id: string) {\n const admin = await this.getAdmin()\n return admin.delete(id)\n }\n\n // ---------------------------------------------------------------\n // Translation delegation\n // ---------------------------------------------------------------\n\n async saveTranslation(entityId: string, locale: string, translations: Record<string, unknown>) {\n const admin = await this.getAdmin()\n return admin.saveTranslation(entityId, locale, translations)\n }\n\n async deleteTranslation(entityId: string, locale?: string) {\n const admin = await this.getAdmin()\n return admin.deleteTranslation(entityId, locale)\n }\n\n // ---------------------------------------------------------------\n // Block operations\n // ---------------------------------------------------------------\n\n /**\n * Copy layout from one locale to another (for localized: true blocks).\n */\n async copyLayout(entityId: string, options: { from: string; to: string | string[] }) {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and } = await import('drizzle-orm')\n\n const layoutTable = schemaRegistry.get(`${this.entity.name}_layout`)\n if (!layoutTable) return\n\n const targetLocales = Array.isArray(options.to) ? options.to : [options.to]\n\n // Get source layout rows\n const sourceRows = await this.db\n .select()\n .from(layoutTable)\n .where(and(eq(layoutTable.entityId, entityId), eq(layoutTable.locale, options.from)))\n .orderBy(layoutTable.sortOrder)\n\n if (sourceRows.length === 0) return\n\n for (const targetLocale of targetLocales) {\n // Delete existing target layout\n await this.db\n .delete(layoutTable)\n .where(and(eq(layoutTable.entityId, entityId), eq(layoutTable.locale, targetLocale)))\n\n // Copy source rows with new locale\n for (const row of sourceRows) {\n await this.db.insert(layoutTable).values({\n entityId: row.entityId,\n fieldName: row.fieldName,\n blockType: row.blockType,\n sortOrder: row.sortOrder,\n data: row.data,\n locale: targetLocale,\n })\n }\n }\n\n this.logger?.debug(\n { entityId, from: options.from, to: targetLocales },\n 'Layout copied between locales',\n )\n }\n\n // ---------------------------------------------------------------\n // Version operations\n // ---------------------------------------------------------------\n\n /**\n * List versions for an entity (newest first), filtered by locale.\n * Uses '_' as the locale value for default-locale snapshots.\n */\n async getVersions(\n entityId: string,\n options?: { limit?: number; offset?: number; locale?: string },\n ): Promise<VersionEntry[]> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and, desc } = await import('drizzle-orm')\n\n const versionsTable = schemaRegistry.get(`${this.entity.name}_versions`)\n if (!versionsTable) return []\n\n const snapshotLocale = options?.locale ?? '_'\n\n let query = this.db\n .select()\n .from(versionsTable)\n .where(and(eq(versionsTable.entityId, entityId), eq(versionsTable.locale, snapshotLocale)))\n .orderBy(desc(versionsTable.version))\n .$dynamic()\n\n if (options?.limit) {\n query = query.limit(options.limit)\n }\n if (options?.offset) {\n query = query.offset(options.offset)\n }\n\n return query as unknown as VersionEntry[]\n }\n\n /**\n * Get a specific version snapshot.\n */\n async getVersion(entityId: string, version: number, locale = '_'): Promise<VersionEntry | null> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and } = await import('drizzle-orm')\n\n const versionsTable = schemaRegistry.get(`${this.entity.name}_versions`)\n if (!versionsTable) return null\n\n const rows = await this.db\n .select()\n .from(versionsTable)\n .where(\n and(\n eq(versionsTable.entityId, entityId),\n eq(versionsTable.version, version),\n eq(versionsTable.locale, locale),\n ),\n )\n .limit(1)\n\n return (rows[0] as unknown as VersionEntry) ?? null\n }\n\n /**\n * Compare two versions and return field-level diff.\n *\n * Returns structured diffs with word-level segments for text fields\n * and structural block diffs for blocks fields.\n */\n async compareVersions(\n entityId: string,\n v1: number,\n v2: number,\n locale = '_',\n ): Promise<{ diffs: FieldDiff[]; fromVersion: VersionEntry; toVersion: VersionEntry } | null> {\n const [version1, version2] = await Promise.all([\n this.getVersion(entityId, v1, locale),\n this.getVersion(entityId, v2, locale),\n ])\n\n if (!version1 || !version2) return null\n\n const data1 = version1.data as Record<string, unknown>\n const data2 = version2.data as Record<string, unknown>\n\n const fieldConfigs = this.entity.allFields as Record<string, FieldConfig> | undefined\n const diffs = diffFields(data1, data2, fieldConfigs)\n\n // Enhance blocks fields with structural diff\n for (const diff of diffs) {\n if (diff.type === 'blocks' && Array.isArray(diff.from) && Array.isArray(diff.to)) {\n diff.blockDiffs = diffBlocks(diff.from, diff.to)\n }\n }\n\n return { diffs, fromVersion: version1, toVersion: version2 }\n }\n\n /**\n * Compare the current draft with the live entity (or a specific version).\n * Returns field-level diffs between the current published data and the draft.\n */\n async compareDraftWithCurrent(\n entityId: string,\n locale = '_',\n ): Promise<{ diffs: FieldDiff[]; current: Record<string, unknown>; draft: DraftEntry } | null> {\n const draft = await this.getDraft(entityId, locale)\n if (!draft) return null\n\n const admin = await this.getAdmin()\n\n // Fetch the live entity with the same locale context as the draft\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n const defaultLocale = config.defaultLocale\n const findOptions = locale === '_' ? { defaultLocale } : { locale, defaultLocale }\n const current = (await admin.findById(entityId, findOptions)) as Record<string, unknown> | null\n if (!current) return null\n\n const fieldConfigs = this.entity.allFields as Record<string, FieldConfig> | undefined\n\n // Only compare fields present in the draft (draft stores a partial snapshot\n // of user-edited fields — missing fields are NOT \"changed to empty\")\n const currentSubset: Record<string, unknown> = {}\n for (const key of Object.keys(draft.data)) {\n currentSubset[key] = current[key]\n }\n const diffs = diffFields(currentSubset, draft.data, fieldConfigs)\n\n // Enhance blocks fields with structural diff\n for (const diff of diffs) {\n if (diff.type === 'blocks' && Array.isArray(diff.from) && Array.isArray(diff.to)) {\n diff.blockDiffs = diffBlocks(diff.from, diff.to)\n }\n }\n\n return { diffs, current, draft }\n }\n\n /**\n * Restore an entity to a previous version.\n */\n async restoreVersion(entityId: string, version: number) {\n const versionData = await this.getVersion(entityId, version)\n if (!versionData) {\n throw new Error(`Version ${version} not found for entity ${entityId}`)\n }\n\n const data = versionData.data as Record<string, EntityFieldValue>\n\n // Remove auto-generated fields before update\n const { id, createdAt, createdBy, _version, _versionedAt, ...updateData } = data\n\n const admin = await this.getAdmin()\n return admin.update(entityId, updateData)\n }\n\n /**\n * Enforce maxPerDoc retention by deleting oldest versions for a locale.\n */\n async pruneVersions(entityId: string, locale = '_') {\n if (!this.versionableOptions) return\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and, desc, sql } = await import('drizzle-orm')\n\n const versionsTable = schemaRegistry.get(`${this.entity.name}_versions`)\n if (!versionsTable) return\n\n const maxPerDoc = this.versionableOptions.maxPerDoc\n\n // Get IDs of versions to keep (newest N for this locale)\n const keepVersions = await this.db\n .select({ id: versionsTable.id })\n .from(versionsTable)\n .where(and(eq(versionsTable.entityId, entityId), eq(versionsTable.locale, locale)))\n .orderBy(desc(versionsTable.version))\n .limit(maxPerDoc)\n\n if (keepVersions.length < maxPerDoc) return // Under limit\n\n const keepIds = keepVersions.map((v: { id: string }) => v.id)\n\n // Delete everything not in keep list (scoped to this locale)\n await this.db.delete(versionsTable).where(\n sql`${versionsTable.entityId} = ${entityId} AND ${versionsTable.locale} = ${locale} AND ${versionsTable.id} NOT IN (${sql.join(\n keepIds.map((id: string) => sql`${id}`),\n sql`, `,\n )})`,\n )\n }\n\n // ---------------------------------------------------------------\n // Translation status\n // ---------------------------------------------------------------\n\n /**\n * Get translation status for an entity — per-locale completion report.\n */\n async getTranslationStatus(entityId: string): Promise<TranslationStatus> {\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n\n // Find all translatable fields\n const translatableFields = Object.entries(this.entity.allFields as Record<string, FieldConfig>)\n .filter(([_, f]) => f.translatable)\n .map(([name]) => name)\n\n if (translatableFields.length === 0) return {}\n\n // Get existing translations\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq } = await import('drizzle-orm')\n\n const translationTable = schemaRegistry.get(`${this.entity.name}_translations`)\n if (!translationTable) return {}\n\n const translations = await this.db\n .select()\n .from(translationTable)\n .where(eq(translationTable.entityId, entityId))\n\n // Build lookup: locale → { fieldName: value } from real columns\n const translationMap = new Map<string, Record<string, unknown>>()\n for (const t of translations) {\n const row = t as Record<string, unknown>\n const fieldValues: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n fieldValues[fieldName] = row[fieldName]\n }\n translationMap.set(row.locale as string, fieldValues)\n }\n\n const status: TranslationStatus = {}\n\n for (const locale of config.locales) {\n if (locale.code === config.defaultLocale) continue // Skip default locale\n\n const translated = translationMap.get(locale.code) ?? {}\n const missing: string[] = []\n\n for (const fieldName of translatableFields) {\n if (translated[fieldName] === undefined || translated[fieldName] === null) {\n missing.push(fieldName)\n }\n }\n\n const translatedCount = translatableFields.length - missing.length\n\n status[locale.code] = {\n complete: missing.length === 0,\n total: translatableFields.length,\n translated: translatedCount,\n missing,\n }\n }\n\n return status\n }\n\n /**\n * Get translation status for multiple entities in a single query.\n * Returns a map of entityId → TranslationStatus (O(1) DB roundtrips).\n */\n async getTranslationStatusBatch(entityIds: string[]): Promise<Map<string, TranslationStatus>> {\n const result = new Map<string, TranslationStatus>()\n if (entityIds.length === 0) return result\n\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n\n // Find all translatable fields\n const translatableFields = Object.entries(this.entity.allFields as Record<string, FieldConfig>)\n .filter(([_, f]) => f.translatable)\n .map(([name]) => name)\n\n if (translatableFields.length === 0) return result\n\n // Get existing translations for all entity IDs in one query\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const translationTable = schemaRegistry.get(`${this.entity.name}_translations`)\n if (!translationTable) return result\n\n const translations = await this.db\n .select()\n .from(translationTable)\n .where(inArray(translationTable.entityId, entityIds))\n\n // Group translations by entityId → locale → { fieldName: value } from real columns\n const translationsByEntity = new Map<string, Map<string, Record<string, unknown>>>()\n for (const t of translations) {\n const row = t as Record<string, unknown>\n const entityId = row.entityId as string\n const locale = row.locale as string\n const fieldValues: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n fieldValues[fieldName] = row[fieldName]\n }\n if (!translationsByEntity.has(entityId)) {\n translationsByEntity.set(entityId, new Map())\n }\n translationsByEntity.get(entityId)!.set(locale, fieldValues)\n }\n\n // Compute status for each entity\n for (const entityId of entityIds) {\n const localeMap = translationsByEntity.get(entityId)\n const status: TranslationStatus = {}\n\n for (const locale of config.locales) {\n if (locale.code === config.defaultLocale) continue\n\n const translated = localeMap?.get(locale.code) ?? {}\n const missing: string[] = []\n\n for (const fieldName of translatableFields) {\n if (translated[fieldName] === undefined || translated[fieldName] === null) {\n missing.push(fieldName)\n }\n }\n\n const translatedCount = translatableFields.length - missing.length\n\n status[locale.code] = {\n complete: missing.length === 0,\n total: translatableFields.length,\n translated: translatedCount,\n missing,\n }\n }\n\n result.set(entityId, status)\n }\n\n return result\n }\n\n /**\n * Get translation report across all entities of this type.\n */\n async getTranslationReport(options?: { locale?: string }): Promise<TranslationReport> {\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n\n const targetLocale =\n options?.locale ?? config.locales.find((l) => l.code !== config.defaultLocale)?.code\n\n if (!targetLocale) return { total: 0, complete: 0, partial: 0, missing: 0 }\n\n // Get all entities\n const admin = await this.getAdmin()\n const entities = await admin.findMany()\n\n let complete = 0\n let partial = 0\n let missing = 0\n\n for (const entity of entities) {\n const status = await this.getTranslationStatus(entity.id)\n const localeStatus = status[targetLocale]\n\n if (!localeStatus || localeStatus.translated === 0) {\n missing++\n } else if (localeStatus.complete) {\n complete++\n } else {\n partial++\n }\n }\n\n return {\n total: entities.length,\n complete,\n partial,\n missing,\n }\n }\n\n // ---------------------------------------------------------------\n // Per-locale publish status\n // ---------------------------------------------------------------\n\n /**\n * Get per-locale publish status for an entity.\n * Returns status for all non-default locales that have explicit overrides.\n * Missing locale = inherits base entity status.\n */\n async getLocaleStatus(entityId: string): Promise<LocalePublishStatus[]> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq } = await import('drizzle-orm')\n\n const statusTable = schemaRegistry.get(`${this.entity.name}_locale_status`)\n if (!statusTable) return []\n\n const rows = await this.db.select().from(statusTable).where(eq(statusTable.entityId, entityId))\n\n return rows.map((r) => ({\n locale: r.locale as string,\n status: r.status as 'draft' | 'published',\n publishedAt: (r.publishedAt as Date | null) ?? null,\n }))\n }\n\n /**\n * Set per-locale publish status. Upserts into locale_status table.\n * When locale === defaultLocale, updates the base entity status instead.\n */\n async setLocaleStatus(\n entityId: string,\n locale: string,\n status: 'draft' | 'published',\n ): Promise<void> {\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n\n // Default locale → update base entity status column\n if (locale === config.defaultLocale) {\n const admin = await this.getAdmin()\n const data: Record<string, EntityFieldValue> = { status }\n if (status === 'published') {\n data.publishedAt = new Date()\n }\n await admin.update(entityId, data)\n return\n }\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and } = await import('drizzle-orm')\n\n const statusTable = schemaRegistry.get(`${this.entity.name}_locale_status`)\n if (!statusTable) return\n\n const publishedAt = status === 'published' ? new Date() : null\n\n // Upsert: check if row exists\n const [existing] = await this.db\n .select({ id: statusTable.id })\n .from(statusTable)\n .where(and(eq(statusTable.entityId, entityId), eq(statusTable.locale, locale)))\n .limit(1)\n\n if (existing) {\n await this.db\n .update(statusTable)\n .set({ status, publishedAt })\n .where(eq(statusTable.id, existing.id))\n } else {\n await this.db.insert(statusTable).values({\n entityId,\n locale,\n status,\n publishedAt,\n })\n }\n }\n\n /**\n * Get per-locale publish status for multiple entities in a single query.\n * Returns a map of entityId → locale → LocalePublishStatus.\n */\n async getLocaleStatusBatch(\n entityIds: string[],\n ): Promise<Map<string, Record<string, LocalePublishStatus>>> {\n const result = new Map<string, Record<string, LocalePublishStatus>>()\n if (entityIds.length === 0) return result\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const statusTable = schemaRegistry.get(`${this.entity.name}_locale_status`)\n if (!statusTable) return result\n\n const rows = await this.db\n .select()\n .from(statusTable)\n .where(inArray(statusTable.entityId, entityIds))\n\n for (const r of rows) {\n const eid = r.entityId as string\n const locale = r.locale as string\n if (!result.has(eid)) {\n result.set(eid, {})\n }\n result.get(eid)![locale] = {\n locale,\n status: r.status as 'draft' | 'published',\n publishedAt: (r.publishedAt as Date | null) ?? null,\n }\n }\n\n return result\n }\n\n // ---------------------------------------------------------------\n // Draft operations\n // ---------------------------------------------------------------\n\n /**\n * Save (upsert) a draft for an entity+locale.\n * One draft per entity+locale — overwrites any existing draft.\n */\n async saveDraft(\n entityId: string,\n data: Record<string, unknown>,\n locale = '_',\n user?: { id: string; name?: string },\n ): Promise<DraftEntry> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and } = await import('drizzle-orm')\n\n const draftsTable = schemaRegistry.get(`${this.entity.name}_drafts`)\n if (!draftsTable) {\n throw new Error(\n `Drafts table not found for entity \"${this.entity.name}\". Is the content plugin initialized?`,\n )\n }\n\n const now = new Date()\n const createdBy = user?.id ?? ''\n const createdByName = user?.name ?? null\n\n // Check for existing draft\n const [existing] = await this.db\n .select()\n .from(draftsTable)\n .where(and(eq(draftsTable.entityId, entityId), eq(draftsTable.locale, locale)))\n .limit(1)\n\n if (existing) {\n const [updated] = await this.db\n .update(draftsTable)\n .set({ data, updatedAt: now })\n .where(eq(draftsTable.id, existing.id))\n .returning()\n\n return this.toDraftEntry(updated)\n }\n\n const [inserted] = await this.db\n .insert(draftsTable)\n .values({\n entityId,\n locale,\n data,\n createdBy,\n createdByName,\n createdAt: now,\n updatedAt: now,\n })\n .returning()\n\n return this.toDraftEntry(inserted)\n }\n\n /**\n * Get the draft for an entity+locale, or null if none exists.\n */\n async getDraft(entityId: string, locale = '_'): Promise<DraftEntry | null> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and } = await import('drizzle-orm')\n\n const draftsTable = schemaRegistry.get(`${this.entity.name}_drafts`)\n if (!draftsTable) return null\n\n const [row] = await this.db\n .select()\n .from(draftsTable)\n .where(and(eq(draftsTable.entityId, entityId), eq(draftsTable.locale, locale)))\n .limit(1)\n\n return row ? this.toDraftEntry(row) : null\n }\n\n /**\n * Delete (discard) a draft for an entity+locale.\n */\n async deleteDraft(entityId: string, locale = '_'): Promise<boolean> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and } = await import('drizzle-orm')\n\n const draftsTable = schemaRegistry.get(`${this.entity.name}_drafts`)\n if (!draftsTable) return false\n\n const result = await this.db\n .delete(draftsTable)\n .where(and(eq(draftsTable.entityId, entityId), eq(draftsTable.locale, locale)))\n .returning({ id: draftsTable.id })\n\n return result.length > 0\n }\n\n /**\n * Publish a draft: apply draft data to the live entity, then delete the draft.\n * Runs in a transaction to ensure atomicity.\n */\n async publishDraft(\n entityId: string,\n locale = '_',\n user?: { id: string; name?: string },\n ): Promise<Record<string, unknown>> {\n const draft = await this.getDraft(entityId, locale)\n if (!draft) {\n throw new Error(`No draft found for entity ${entityId} locale ${locale}`)\n }\n\n const admin = await this.getAdmin()\n\n // Get previous state for version delta\n let previousState: Record<string, unknown> | null = null\n if (this.versionableOptions) {\n previousState = await admin.findById(entityId)\n }\n\n // Apply draft data to entity\n const draftData = draft.data as Record<string, EntityFieldValue>\n const { id, createdAt, createdBy, _version, _versionedAt, ...updateData } = draftData\n const result = await admin.update(entityId, updateData)\n\n // Delete the draft\n await this.deleteDraft(entityId, locale)\n\n // Create version snapshot\n if (this.versionableOptions) {\n await this.createVersionSnapshot(entityId, result, previousState, locale, user)\n }\n\n return result\n }\n\n /**\n * Quick existence check for list badges — does a draft exist?\n */\n async hasDraft(entityId: string, locale = '_'): Promise<boolean> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and } = await import('drizzle-orm')\n\n const draftsTable = schemaRegistry.get(`${this.entity.name}_drafts`)\n if (!draftsTable) return false\n\n const [row] = await this.db\n .select({ id: draftsTable.id })\n .from(draftsTable)\n .where(and(eq(draftsTable.entityId, entityId), eq(draftsTable.locale, locale)))\n .limit(1)\n\n return !!row\n }\n\n /**\n * Count drafts for this entity type, optionally filtered by locale.\n * Single indexed COUNT query — used by dashboard widgets.\n */\n async countDrafts(locale = '_'): Promise<number> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, sql } = await import('drizzle-orm')\n\n const draftsTable = schemaRegistry.get(`${this.entity.name}_drafts`)\n if (!draftsTable) return 0\n\n const [row] = await this.db\n .select({ count: sql<number>`count(*)::int` })\n .from(draftsTable)\n .where(eq(draftsTable.locale, locale))\n\n return row?.count ?? 0\n }\n\n /**\n * Check draft existence for multiple entities in one query.\n * Returns a set of entity IDs that have drafts.\n */\n async hasDraftBatch(entityIds: string[], locale = '_'): Promise<Set<string>> {\n const result = new Set<string>()\n if (entityIds.length === 0) return result\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray, eq, and } = await import('drizzle-orm')\n\n const draftsTable = schemaRegistry.get(`${this.entity.name}_drafts`)\n if (!draftsTable) return result\n\n const rows = await this.db\n .select({ entityId: draftsTable.entityId })\n .from(draftsTable)\n .where(and(inArray(draftsTable.entityId, entityIds), eq(draftsTable.locale, locale)))\n\n for (const row of rows) {\n result.add(row.entityId as string)\n }\n\n return result\n }\n\n /**\n * Create a version snapshot for an entity after an external update.\n * Use this when the update was performed by AdminClient directly\n * (e.g., the admin API handler) rather than through ContentClient.update().\n *\n * @param locale - The locale being edited. Use undefined or '_' for the default locale.\n * @param user - The user who made the change (id + name for display).\n */\n async createVersionSnapshot(\n entityId: string,\n currentData: Record<string, unknown>,\n previousData?: Record<string, unknown> | null,\n locale?: string,\n user?: { id: string; name?: string },\n ) {\n if (!this.versionableOptions) return\n const snapshotLocale = locale ?? '_'\n const delta = previousData ? this.calculateDelta(previousData, currentData) : null\n const version = await this.saveVersionSnapshot(\n entityId,\n currentData,\n delta,\n false,\n snapshotLocale,\n user,\n )\n\n // Update _version on the entity to match the snapshot version (default locale only)\n if (version !== undefined && snapshotLocale === '_') {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq } = await import('drizzle-orm')\n const table = schemaRegistry.get(this.entity.name)\n if (table) {\n await this.db.update(table).set({ _version: version }).where(eq(table.id, entityId))\n }\n }\n\n await this.pruneVersions(entityId, snapshotLocale)\n }\n\n // ---------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------\n\n private async saveVersionSnapshot(\n entityId: string,\n data: Record<string, unknown>,\n delta: Record<string, unknown> | null,\n isAutosave: boolean,\n locale = '_',\n user?: { id: string; name?: string },\n ): Promise<number | undefined> {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and, sql } = await import('drizzle-orm')\n\n const versionsTable = schemaRegistry.get(`${this.entity.name}_versions`)\n if (!versionsTable) return\n\n // Get the next version number for this locale (max + 1)\n const [maxRow] = await this.db\n .select({ maxVersion: sql<number>`coalesce(max(${versionsTable.version}), 0)` })\n .from(versionsTable)\n .where(and(eq(versionsTable.entityId, entityId), eq(versionsTable.locale, locale)))\n\n const version = (maxRow?.maxVersion ?? 0) + 1\n const status = (data.status as string) ?? null\n\n // For autosave: update existing autosave version instead of creating new one\n if (isAutosave) {\n const [existing] = await this.db\n .select()\n .from(versionsTable)\n .where(\n and(\n eq(versionsTable.entityId, entityId),\n eq(versionsTable.isAutosave, true),\n eq(versionsTable.version, version),\n eq(versionsTable.locale, locale),\n ),\n )\n .limit(1)\n\n if (existing) {\n await this.db\n .update(versionsTable)\n .set({ data, delta, status, createdAt: new Date() })\n .where(eq(versionsTable.id, existing.id))\n return version\n }\n }\n\n await this.db.insert(versionsTable).values({\n entityId,\n version,\n locale,\n data,\n delta,\n status,\n createdBy: user?.id ?? (data.updatedBy as string) ?? (data.createdBy as string) ?? null,\n createdByName: user?.name ?? null,\n isAutosave,\n })\n return version\n }\n\n private toDraftEntry(row: Record<string, unknown>): DraftEntry {\n return {\n id: row.id as string,\n entityId: row.entityId as string,\n locale: row.locale as string,\n data: row.data as Record<string, unknown>,\n createdBy: row.createdBy as string,\n createdByName: (row.createdByName as string) ?? null,\n createdAt: row.createdAt as Date,\n updatedAt: row.updatedAt as Date,\n }\n }\n\n private calculateDelta(\n previous: Record<string, unknown>,\n current: Record<string, unknown>,\n ): Record<string, unknown> {\n const delta: Record<string, unknown> = {}\n\n for (const key of Object.keys(current)) {\n if (key === 'id' || key.startsWith('_')) continue\n if (JSON.stringify(previous[key]) !== JSON.stringify(current[key])) {\n delta[key] = current[key]\n }\n }\n\n return delta\n }\n}\n\n/**\n * Factory — creates a ContentClient for an entity.\n * Must be called after createApp().\n */\nexport async function createContentClient(entity: Entity): Promise<ContentClient> {\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n return new ContentClient({\n entity,\n db: app.db.readWrite,\n logger: app.logger.child({ content: entity.name }),\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * ContentClient — wraps AdminClient with version history, drafts, and translation status.\n *\n * All per-entity infrastructure tables (drafts, versions, locale_status) are\n * accessed through typed TableClients built on `defineTable`. There are NO\n * direct Drizzle queries on these tables — the only remaining `this.db.*`\n * calls are for layout (a separate entity-package concern) and for the\n * `_version` column on the entity table (writing through AdminClient.update\n * would retrigger the versionable hook and loop).\n *\n * Usage:\n * import { createContentClient } from '@murumets-ee/content/client'\n * const pages = createContentClient(Page)\n * await pages.create({ title: 'Hello', layout: [...] })\n */\n\nimport 'server-only'\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { Entity, FieldConfig, FieldToTS } from '@murumets-ee/entity'\nimport type { AdminClient, FindManyOptions } from '@murumets-ee/entity/admin'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { getVersionableOptions } from './behaviors/versionable.js'\nimport type { DraftsTable } from './schema/drafts.js'\n\n/** Row shape returned by the drafts TableClient's reads (Date timestamps, typed columns). */\ntype DraftRow = NonNullable<Awaited<ReturnType<ReturnType<DraftsTable['makeClient']>['findOne']>>>\nimport { diffBlocks } from './diff/block-diff.js'\nimport { diffFields } from './diff/field-diff.js'\nimport type { FieldDiff } from './diff/types.js'\nimport { getContentTables } from './schema/registry.js'\nimport type {\n DraftEntry,\n LocalePublishStatus,\n TranslationReport,\n TranslationStatus,\n VersionEntry,\n} from './types.js'\n\n/** Value type that any entity field can hold */\ntype EntityFieldValue = FieldToTS<FieldConfig> | null | undefined\n\nexport interface ContentClientConfig {\n admin: AdminClient\n entity: Entity\n logger?: Logger\n}\n\nexport class ContentClient {\n private entity: Entity\n private admin: AdminClient\n private logger?: Logger\n private versionableOptions: ReturnType<typeof getVersionableOptions>\n\n constructor(config: ContentClientConfig) {\n this.entity = config.entity\n this.admin = config.admin\n this.logger = config.logger\n this.versionableOptions = getVersionableOptions(config.entity.behaviors)\n }\n\n /** Database handle inherited from the injected AdminClient. */\n private get db(): PostgresJsDatabase {\n return this.admin.getDb()\n }\n\n /** Lazy TableClient for the drafts table. Returns null if the entity has no drafts table. */\n private getDraftsClient() {\n const { drafts } = getContentTables(this.entity.name)\n return drafts ? drafts.makeClient(this.db) : null\n }\n\n /** Lazy TableClient for the versions table. Returns null if the entity has no versions table. */\n private getVersionsClient() {\n const { versions } = getContentTables(this.entity.name)\n return versions ? versions.makeClient(this.db) : null\n }\n\n /** Lazy TableClient for the locale_status table. Returns null if the entity has no locale_status table. */\n private getLocaleStatusClient() {\n const { localeStatus } = getContentTables(this.entity.name)\n return localeStatus ? localeStatus.makeClient(this.db) : null\n }\n\n // ---------------------------------------------------------------\n // Standard CRUD (delegated to AdminClient)\n // ---------------------------------------------------------------\n\n async create(data: Record<string, EntityFieldValue>) {\n const admin = this.admin\n const result = await admin.create(data)\n\n // Save initial version snapshot\n if (this.versionableOptions) {\n await this.saveVersionSnapshot(result.id, result, null, false)\n }\n\n return result\n }\n\n async findById(id: string, options?: { locale?: string }) {\n const admin = this.admin\n return admin.findById(id, options)\n }\n\n async findMany(options?: FindManyOptions) {\n const admin = this.admin\n return admin.findMany(options)\n }\n\n async update(id: string, data: Record<string, EntityFieldValue>) {\n const admin = this.admin\n\n // Get previous state for delta calculation\n let previousState: Record<string, unknown> | null = null\n if (this.versionableOptions) {\n previousState = await admin.findById(id)\n }\n\n const result = await admin.update(id, data)\n\n // Save version snapshot and update _version on the entity\n if (this.versionableOptions && previousState) {\n const delta = this.calculateDelta(previousState, result)\n const version = await this.saveVersionSnapshot(id, result, delta, false)\n\n if (version !== undefined) {\n await this.writeVersionColumn(id, version)\n // ContentClient is entity-generic; `_version` is added by the versionable\n // behavior at runtime but is not visible in the generic DTO. Safe write.\n ;(result as Record<string, unknown>)._version = version\n }\n\n await this.pruneVersions(id)\n }\n\n return result\n }\n\n async delete(id: string) {\n const admin = this.admin\n return admin.delete(id)\n }\n\n // ---------------------------------------------------------------\n // Translation delegation\n // ---------------------------------------------------------------\n\n async saveTranslation(entityId: string, locale: string, translations: Record<string, unknown>) {\n const admin = this.admin\n return admin.saveTranslation(entityId, locale, translations)\n }\n\n async deleteTranslation(entityId: string, locale?: string) {\n const admin = this.admin\n return admin.deleteTranslation(entityId, locale)\n }\n\n // ---------------------------------------------------------------\n // Block operations\n // ---------------------------------------------------------------\n\n /**\n * Copy layout from one locale to another (for localized: true blocks).\n *\n * NOTE: Layout tables are generated in `@murumets-ee/entity` (they need\n * FK references to the parent entity table, which `defineTable` does not\n * currently support). So this method uses direct Drizzle queries against\n * the layout pgTable via `schemaRegistry` — it is the one remaining\n * escape valve in ContentClient. If `defineTable` gains FK support in the\n * future, migrate this to a TableClient pattern like drafts/versions.\n */\n async copyLayout(entityId: string, options: { from: string; to: string | string[] }) {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq, and } = await import('drizzle-orm')\n\n const layoutTable = schemaRegistry.get(`${this.entity.name}_layout`)\n if (!layoutTable) return\n\n const targetLocales = Array.isArray(options.to) ? options.to : [options.to]\n\n // Get source layout rows\n const sourceRows = await this.db\n .select()\n .from(layoutTable)\n .where(and(eq(layoutTable.entityId, entityId), eq(layoutTable.locale, options.from)))\n .orderBy(layoutTable.sortOrder)\n\n if (sourceRows.length === 0) return\n\n for (const targetLocale of targetLocales) {\n // Delete existing target layout\n await this.db\n .delete(layoutTable)\n .where(and(eq(layoutTable.entityId, entityId), eq(layoutTable.locale, targetLocale)))\n\n // Copy source rows with new locale\n for (const row of sourceRows) {\n await this.db.insert(layoutTable).values({\n entityId: row.entityId,\n fieldName: row.fieldName,\n blockType: row.blockType,\n sortOrder: row.sortOrder,\n data: row.data,\n locale: targetLocale,\n })\n }\n }\n\n this.logger?.debug(\n { entityId, from: options.from, to: targetLocales },\n 'Layout copied between locales',\n )\n }\n\n // ---------------------------------------------------------------\n // Version operations\n // ---------------------------------------------------------------\n\n /**\n * List versions for an entity (newest first), filtered by locale.\n * Uses '_' as the locale value for default-locale snapshots.\n */\n async getVersions(\n entityId: string,\n options?: { limit?: number; offset?: number; locale?: string },\n ): Promise<VersionEntry[]> {\n const client = this.getVersionsClient()\n if (!client) return []\n\n const rows = await client.findMany({\n where: { entityId, locale: options?.locale ?? '_' },\n orderBy: [{ column: 'version', dir: 'desc' }],\n ...(options?.limit !== undefined ? { limit: options.limit } : {}),\n ...(options?.offset !== undefined ? { offset: options.offset } : {}),\n })\n // Row<VersionsColumns> is structurally compatible with VersionEntry.\n return rows\n }\n\n /**\n * Get a specific version snapshot.\n */\n async getVersion(entityId: string, version: number, locale = '_'): Promise<VersionEntry | null> {\n const client = this.getVersionsClient()\n if (!client) return null\n\n const row = await client.findOne({ entityId, version, locale })\n // Row<VersionsColumns> is structurally compatible with VersionEntry.\n return row ?? null\n }\n\n /**\n * Compare two versions and return field-level diff.\n *\n * Returns structured diffs with word-level segments for text fields\n * and structural block diffs for blocks fields.\n */\n async compareVersions(\n entityId: string,\n v1: number,\n v2: number,\n locale = '_',\n ): Promise<{ diffs: FieldDiff[]; fromVersion: VersionEntry; toVersion: VersionEntry } | null> {\n const [version1, version2] = await Promise.all([\n this.getVersion(entityId, v1, locale),\n this.getVersion(entityId, v2, locale),\n ])\n\n if (!version1 || !version2) return null\n\n const data1 = version1.data as Record<string, unknown>\n const data2 = version2.data as Record<string, unknown>\n\n const fieldConfigs = this.entity.allFields as Record<string, FieldConfig> | undefined\n const diffs = diffFields(data1, data2, fieldConfigs)\n\n // Enhance blocks fields with structural diff\n for (const diff of diffs) {\n if (diff.type === 'blocks' && Array.isArray(diff.from) && Array.isArray(diff.to)) {\n diff.blockDiffs = diffBlocks(diff.from, diff.to)\n }\n }\n\n return { diffs, fromVersion: version1, toVersion: version2 }\n }\n\n /**\n * Compare the current draft with the live entity (or a specific version).\n * Returns field-level diffs between the current published data and the draft.\n */\n async compareDraftWithCurrent(\n entityId: string,\n locale = '_',\n ): Promise<{ diffs: FieldDiff[]; current: Record<string, unknown>; draft: DraftEntry } | null> {\n const draft = await this.getDraft(entityId, locale)\n if (!draft) return null\n\n const admin = this.admin\n\n // Fetch the live entity with the same locale context as the draft\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n const defaultLocale = config.defaultLocale\n const findOptions = locale === '_' ? { defaultLocale } : { locale, defaultLocale }\n const current = (await admin.findById(entityId, findOptions)) as Record<string, unknown> | null\n if (!current) return null\n\n const fieldConfigs = this.entity.allFields as Record<string, FieldConfig> | undefined\n\n // Only compare fields present in the draft (draft stores a partial snapshot\n // of user-edited fields — missing fields are NOT \"changed to empty\")\n const currentSubset: Record<string, unknown> = {}\n for (const key of Object.keys(draft.data)) {\n currentSubset[key] = current[key]\n }\n const diffs = diffFields(currentSubset, draft.data, fieldConfigs)\n\n // Enhance blocks fields with structural diff\n for (const diff of diffs) {\n if (diff.type === 'blocks' && Array.isArray(diff.from) && Array.isArray(diff.to)) {\n diff.blockDiffs = diffBlocks(diff.from, diff.to)\n }\n }\n\n return { diffs, current, draft }\n }\n\n /**\n * Restore an entity to a previous version.\n */\n async restoreVersion(entityId: string, version: number) {\n const versionData = await this.getVersion(entityId, version)\n if (!versionData) {\n throw new Error(`Version ${version} not found for entity ${entityId}`)\n }\n\n const data = versionData.data as Record<string, EntityFieldValue>\n\n // Remove auto-generated fields before update\n const { id, createdAt, createdBy, _version, _versionedAt, ...updateData } = data\n\n const admin = this.admin\n return admin.update(entityId, updateData)\n }\n\n /**\n * Enforce maxPerDoc retention by deleting oldest versions for a locale.\n */\n async pruneVersions(entityId: string, locale = '_') {\n if (!this.versionableOptions) return\n const client = this.getVersionsClient()\n if (!client) return\n\n const maxPerDoc = this.versionableOptions.maxPerDoc\n\n // Get IDs of versions to keep (newest N for this locale)\n const keepVersions = await client.findMany({\n where: { entityId, locale },\n orderBy: [{ column: 'version', dir: 'desc' }],\n limit: maxPerDoc,\n })\n\n if (keepVersions.length < maxPerDoc) return // Under limit\n\n const keepIds = keepVersions.map((v) => v.id as string)\n\n // Delete everything for this entity+locale NOT in the keep list.\n await client.deleteMany({\n entityId,\n locale,\n id: { notIn: keepIds },\n })\n }\n\n // ---------------------------------------------------------------\n // Translation status\n // ---------------------------------------------------------------\n\n /**\n * Get translation status for an entity — per-locale completion report.\n */\n async getTranslationStatus(entityId: string): Promise<TranslationStatus> {\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n\n // Find all translatable fields\n const translatableFields = Object.entries(this.entity.allFields as Record<string, FieldConfig>)\n .filter(([_, f]) => f.translatable)\n .map(([name]) => name)\n\n if (translatableFields.length === 0) return {}\n\n // Translation tables are generated in @murumets-ee/entity (with FK refs)\n // — still accessed via schemaRegistry + direct Drizzle.\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq } = await import('drizzle-orm')\n\n const translationTable = schemaRegistry.get(`${this.entity.name}_translations`)\n if (!translationTable) return {}\n\n const translations = await this.db\n .select()\n .from(translationTable)\n .where(eq(translationTable.entityId, entityId))\n\n // Build lookup: locale → { fieldName: value } from real columns\n const translationMap = new Map<string, Record<string, unknown>>()\n for (const t of translations) {\n const row = t as Record<string, unknown>\n const fieldValues: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n fieldValues[fieldName] = row[fieldName]\n }\n translationMap.set(row.locale as string, fieldValues)\n }\n\n const status: TranslationStatus = {}\n\n for (const locale of config.locales) {\n if (locale.code === config.defaultLocale) continue // Skip default locale\n\n const translated = translationMap.get(locale.code) ?? {}\n const missing: string[] = []\n\n for (const fieldName of translatableFields) {\n if (translated[fieldName] === undefined || translated[fieldName] === null) {\n missing.push(fieldName)\n }\n }\n\n const translatedCount = translatableFields.length - missing.length\n\n status[locale.code] = {\n complete: missing.length === 0,\n total: translatableFields.length,\n translated: translatedCount,\n missing,\n }\n }\n\n return status\n }\n\n /**\n * Get translation status for multiple entities in a single query.\n * Returns a map of entityId → TranslationStatus (O(1) DB roundtrips).\n */\n async getTranslationStatusBatch(entityIds: string[]): Promise<Map<string, TranslationStatus>> {\n const result = new Map<string, TranslationStatus>()\n if (entityIds.length === 0) return result\n\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n\n // Find all translatable fields\n const translatableFields = Object.entries(this.entity.allFields as Record<string, FieldConfig>)\n .filter(([_, f]) => f.translatable)\n .map(([name]) => name)\n\n if (translatableFields.length === 0) return result\n\n // Translation tables are generated in @murumets-ee/entity (with FK refs)\n // — still accessed via schemaRegistry + direct Drizzle.\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { inArray } = await import('drizzle-orm')\n\n const translationTable = schemaRegistry.get(`${this.entity.name}_translations`)\n if (!translationTable) return result\n\n const translations = await this.db\n .select()\n .from(translationTable)\n .where(inArray(translationTable.entityId, entityIds))\n\n // Group translations by entityId → locale → { fieldName: value } from real columns\n const translationsByEntity = new Map<string, Map<string, Record<string, unknown>>>()\n for (const t of translations) {\n const row = t as Record<string, unknown>\n const entityId = row.entityId as string\n const locale = row.locale as string\n const fieldValues: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n fieldValues[fieldName] = row[fieldName]\n }\n if (!translationsByEntity.has(entityId)) {\n translationsByEntity.set(entityId, new Map())\n }\n translationsByEntity.get(entityId)!.set(locale, fieldValues)\n }\n\n // Compute status for each entity\n for (const entityId of entityIds) {\n const localeMap = translationsByEntity.get(entityId)\n const status: TranslationStatus = {}\n\n for (const locale of config.locales) {\n if (locale.code === config.defaultLocale) continue\n\n const translated = localeMap?.get(locale.code) ?? {}\n const missing: string[] = []\n\n for (const fieldName of translatableFields) {\n if (translated[fieldName] === undefined || translated[fieldName] === null) {\n missing.push(fieldName)\n }\n }\n\n const translatedCount = translatableFields.length - missing.length\n\n status[locale.code] = {\n complete: missing.length === 0,\n total: translatableFields.length,\n translated: translatedCount,\n missing,\n }\n }\n\n result.set(entityId, status)\n }\n\n return result\n }\n\n /**\n * Get translation report across all entities of this type.\n */\n async getTranslationReport(options?: { locale?: string }): Promise<TranslationReport> {\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n\n const targetLocale =\n options?.locale ?? config.locales.find((l) => l.code !== config.defaultLocale)?.code\n\n if (!targetLocale) return { total: 0, complete: 0, partial: 0, missing: 0 }\n\n // Get all entities\n const admin = this.admin\n const entities = await admin.findMany()\n\n let complete = 0\n let partial = 0\n let missing = 0\n\n for (const entity of entities) {\n const status = await this.getTranslationStatus(entity.id)\n const localeStatus = status[targetLocale]\n\n if (!localeStatus || localeStatus.translated === 0) {\n missing++\n } else if (localeStatus.complete) {\n complete++\n } else {\n partial++\n }\n }\n\n return {\n total: entities.length,\n complete,\n partial,\n missing,\n }\n }\n\n // ---------------------------------------------------------------\n // Per-locale publish status\n // ---------------------------------------------------------------\n\n /**\n * Get per-locale publish status for an entity.\n * Returns status for all non-default locales that have explicit overrides.\n * Missing locale = inherits base entity status.\n */\n async getLocaleStatus(entityId: string): Promise<LocalePublishStatus[]> {\n const client = this.getLocaleStatusClient()\n if (!client) return []\n\n const rows = await client.findMany({ where: { entityId } })\n\n return rows.map((r) => ({\n locale: r.locale as string,\n status: r.status as 'draft' | 'published',\n publishedAt: (r.publishedAt as Date | null) ?? null,\n }))\n }\n\n /**\n * Set per-locale publish status. Upserts into locale_status table.\n * When locale === defaultLocale, updates the base entity status instead.\n */\n async setLocaleStatus(\n entityId: string,\n locale: string,\n status: 'draft' | 'published',\n ): Promise<void> {\n const { getContentConfig } = await import('./plugin.js')\n const config = getContentConfig()\n\n // Default locale → update base entity status column\n if (locale === config.defaultLocale) {\n const admin = this.admin\n const data: Record<string, EntityFieldValue> = { status }\n if (status === 'published') {\n data.publishedAt = new Date()\n }\n await admin.update(entityId, data)\n return\n }\n\n const client = this.getLocaleStatusClient()\n if (!client) return\n\n const publishedAt = status === 'published' ? new Date() : null\n\n await client.upsert(\n { entityId, locale, status, publishedAt },\n { target: ['entityId', 'locale'], set: { status, publishedAt } },\n )\n }\n\n /**\n * Get per-locale publish status for multiple entities in a single query.\n * Returns a map of entityId → locale → LocalePublishStatus.\n */\n async getLocaleStatusBatch(\n entityIds: string[],\n ): Promise<Map<string, Record<string, LocalePublishStatus>>> {\n const result = new Map<string, Record<string, LocalePublishStatus>>()\n if (entityIds.length === 0) return result\n\n const client = this.getLocaleStatusClient()\n if (!client) return result\n\n const rows = await client.findMany({ where: { entityId: { in: entityIds } } })\n\n for (const r of rows) {\n const eid = r.entityId as string\n const locale = r.locale as string\n if (!result.has(eid)) {\n result.set(eid, {})\n }\n result.get(eid)![locale] = {\n locale,\n status: r.status as 'draft' | 'published',\n publishedAt: (r.publishedAt as Date | null) ?? null,\n }\n }\n\n return result\n }\n\n // ---------------------------------------------------------------\n // Draft operations\n // ---------------------------------------------------------------\n\n /**\n * Save (upsert) a draft for an entity+locale.\n * One draft per entity+locale — overwrites any existing draft.\n */\n async saveDraft(\n entityId: string,\n data: Record<string, unknown>,\n locale = '_',\n user?: { id: string; name?: string },\n ): Promise<DraftEntry> {\n const client = this.getDraftsClient()\n if (!client) {\n throw new Error(\n `Drafts table not found for entity \"${this.entity.name}\". Is the content plugin initialized?`,\n )\n }\n\n const now = new Date()\n const createdBy = user?.id ?? ''\n const createdByName = user?.name ?? null\n\n const row = await client.upsert(\n {\n entityId,\n locale,\n data,\n createdBy,\n createdByName,\n createdAt: now,\n updatedAt: now,\n },\n {\n target: ['entityId', 'locale'],\n // On conflict, only update the mutable fields — preserve createdBy/createdAt.\n set: { data, updatedAt: now },\n },\n )\n\n return this.toDraftEntry(row)\n }\n\n /**\n * Get the draft for an entity+locale, or null if none exists.\n */\n async getDraft(entityId: string, locale = '_'): Promise<DraftEntry | null> {\n const client = this.getDraftsClient()\n if (!client) return null\n\n const row = await client.findOne({ entityId, locale })\n return row ? this.toDraftEntry(row) : null\n }\n\n /**\n * Delete (discard) a draft for an entity+locale.\n */\n async deleteDraft(entityId: string, locale = '_'): Promise<boolean> {\n const client = this.getDraftsClient()\n if (!client) return false\n\n const deleted = await client.deleteMany({ entityId, locale })\n return deleted.length > 0\n }\n\n /**\n * Publish a draft: apply draft data to the live entity, then delete the draft.\n * Runs in a transaction to ensure atomicity.\n */\n async publishDraft(\n entityId: string,\n locale = '_',\n user?: { id: string; name?: string },\n ): Promise<Record<string, unknown>> {\n const draft = await this.getDraft(entityId, locale)\n if (!draft) {\n throw new Error(`No draft found for entity ${entityId} locale ${locale}`)\n }\n\n const admin = this.admin\n\n // Get previous state for version delta\n let previousState: Record<string, unknown> | null = null\n if (this.versionableOptions) {\n previousState = await admin.findById(entityId)\n }\n\n // Apply draft data to entity\n const draftData = draft.data as Record<string, EntityFieldValue>\n const { id, createdAt, createdBy, _version, _versionedAt, ...updateData } = draftData\n const result = await admin.update(entityId, updateData)\n\n // Delete the draft\n await this.deleteDraft(entityId, locale)\n\n // Create version snapshot\n if (this.versionableOptions) {\n await this.createVersionSnapshot(entityId, result, previousState, locale, user)\n }\n\n return result\n }\n\n /**\n * Quick existence check for list badges — does a draft exist?\n */\n async hasDraft(entityId: string, locale = '_'): Promise<boolean> {\n const client = this.getDraftsClient()\n if (!client) return false\n return client.exists({ entityId, locale })\n }\n\n /**\n * Count drafts for this entity type, optionally filtered by locale.\n * Single indexed COUNT query — used by dashboard widgets.\n */\n async countDrafts(locale = '_'): Promise<number> {\n const client = this.getDraftsClient()\n if (!client) return 0\n return client.count({ locale })\n }\n\n /**\n * Check draft existence for multiple entities in one query.\n * Returns a set of entity IDs that have drafts.\n */\n async hasDraftBatch(entityIds: string[], locale = '_'): Promise<Set<string>> {\n const result = new Set<string>()\n if (entityIds.length === 0) return result\n\n const client = this.getDraftsClient()\n if (!client) return result\n\n const rows = await client.findMany({\n where: { entityId: { in: entityIds }, locale },\n })\n\n for (const row of rows) {\n result.add(row.entityId as string)\n }\n\n return result\n }\n\n /**\n * Create a version snapshot for an entity after an external update.\n * Use this when the update was performed by AdminClient directly\n * (e.g., the admin API handler) rather than through ContentClient.update().\n *\n * @param locale - The locale being edited. Use undefined or '_' for the default locale.\n * @param user - The user who made the change (id + name for display).\n */\n async createVersionSnapshot(\n entityId: string,\n currentData: Record<string, unknown>,\n previousData?: Record<string, unknown> | null,\n locale?: string,\n user?: { id: string; name?: string },\n ) {\n if (!this.versionableOptions) return\n const snapshotLocale = locale ?? '_'\n const delta = previousData ? this.calculateDelta(previousData, currentData) : null\n const version = await this.saveVersionSnapshot(\n entityId,\n currentData,\n delta,\n false,\n snapshotLocale,\n user,\n )\n\n // Update _version on the entity to match the snapshot version (default locale only)\n if (version !== undefined && snapshotLocale === '_') {\n await this.writeVersionColumn(entityId, version)\n }\n\n await this.pruneVersions(entityId, snapshotLocale)\n }\n\n // ---------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------\n\n /**\n * Write the `_version` column on the entity row.\n *\n * KNOWN EXCEPTION: this bypasses AdminClient and goes straight to the\n * entity pgTable via `schemaRegistry` + direct Drizzle. Using\n * `admin.update()` would retrigger the `versionable.beforeUpdate` hook\n * and loop indefinitely. This is the only ContentClient path that still\n * writes to an entity table directly.\n */\n private async writeVersionColumn(entityId: string, version: number) {\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { eq } = await import('drizzle-orm')\n const table = schemaRegistry.get(this.entity.name)\n if (!table) return\n await this.db.update(table).set({ _version: version }).where(eq(table.id, entityId))\n }\n\n private async saveVersionSnapshot(\n entityId: string,\n data: Record<string, unknown>,\n delta: Record<string, unknown> | null,\n isAutosave: boolean,\n locale = '_',\n user?: { id: string; name?: string },\n ): Promise<number | undefined> {\n const client = this.getVersionsClient()\n if (!client) return\n\n // Get the next version number for this locale (max existing + 1).\n const [maxRow] = await client.aggregate<{ maxVersion: number | null }>({\n select: { maxVersion: { fn: 'max', column: 'version' } },\n where: { entityId, locale },\n })\n\n const version = Number(maxRow?.maxVersion ?? 0) + 1\n const status = (data.status as string) ?? null\n\n // For autosave: update existing autosave version instead of creating new one.\n // We look for an autosave row at the SAME version number we just computed —\n // preserves the original control flow (autosaves land on the latest slot).\n if (isAutosave) {\n const existing = await client.findOne({\n entityId,\n isAutosave: true,\n version,\n locale,\n })\n if (existing) {\n await client.update(\n { id: existing.id as string },\n { data, delta, status, createdAt: new Date() },\n )\n return version\n }\n }\n\n await client.insert({\n entityId,\n version,\n locale,\n data,\n delta,\n status,\n createdBy: user?.id ?? (data.updatedBy as string) ?? (data.createdBy as string) ?? null,\n createdByName: user?.name ?? null,\n isAutosave,\n })\n return version\n }\n\n private toDraftEntry(row: DraftRow): DraftEntry {\n return {\n id: row.id,\n entityId: row.entityId,\n locale: row.locale,\n data: row.data,\n createdBy: row.createdBy,\n createdByName: row.createdByName ?? null,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }\n }\n\n private calculateDelta(\n previous: Record<string, unknown>,\n current: Record<string, unknown>,\n ): Record<string, unknown> {\n const delta: Record<string, unknown> = {}\n\n for (const key of Object.keys(current)) {\n if (key === 'id' || key.startsWith('_')) continue\n if (JSON.stringify(previous[key]) !== JSON.stringify(current[key])) {\n delta[key] = current[key]\n }\n }\n\n return delta\n }\n}\n\n/**\n * Factory — creates a ContentClient for an entity.\n * Must be called after createApp().\n */\nexport async function createContentClient(entity: Entity): Promise<ContentClient> {\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const { getApp } = await import('@murumets-ee/core')\n const admin = createAdminClient(entity)\n return new ContentClient({\n admin,\n entity,\n logger: getApp().logger.child({ content: entity.name }),\n })\n}\n"],"mappings":"4HAgDA,IAAa,EAAb,KAA2B,CACzB,OACA,MACA,OACA,mBAEA,YAAY,EAA6B,CACvC,KAAK,OAAS,EAAO,OACrB,KAAK,MAAQ,EAAO,MACpB,KAAK,OAAS,EAAO,OACrB,KAAK,mBAAqB,EAAsB,EAAO,OAAO,UAAU,CAI1E,IAAY,IAAyB,CACnC,OAAO,KAAK,MAAM,OAAO,CAI3B,iBAA0B,CACxB,GAAM,CAAE,UAAW,EAAiB,KAAK,OAAO,KAAK,CACrD,OAAO,EAAS,EAAO,WAAW,KAAK,GAAG,CAAG,KAI/C,mBAA4B,CAC1B,GAAM,CAAE,YAAa,EAAiB,KAAK,OAAO,KAAK,CACvD,OAAO,EAAW,EAAS,WAAW,KAAK,GAAG,CAAG,KAInD,uBAAgC,CAC9B,GAAM,CAAE,gBAAiB,EAAiB,KAAK,OAAO,KAAK,CAC3D,OAAO,EAAe,EAAa,WAAW,KAAK,GAAG,CAAG,KAO3D,MAAM,OAAO,EAAwC,CAEnD,IAAM,EAAS,MADD,KAAK,MACQ,OAAO,EAAK,CAOvC,OAJI,KAAK,oBACP,MAAM,KAAK,oBAAoB,EAAO,GAAI,EAAQ,KAAM,GAAM,CAGzD,EAGT,MAAM,SAAS,EAAY,EAA+B,CAExD,OADc,KAAK,MACN,SAAS,EAAI,EAAQ,CAGpC,MAAM,SAAS,EAA2B,CAExC,OADc,KAAK,MACN,SAAS,EAAQ,CAGhC,MAAM,OAAO,EAAY,EAAwC,CAC/D,IAAM,EAAQ,KAAK,MAGf,EAAgD,KAChD,KAAK,qBACP,EAAgB,MAAM,EAAM,SAAS,EAAG,EAG1C,IAAM,EAAS,MAAM,EAAM,OAAO,EAAI,EAAK,CAG3C,GAAI,KAAK,oBAAsB,EAAe,CAC5C,IAAM,EAAQ,KAAK,eAAe,EAAe,EAAO,CAClD,EAAU,MAAM,KAAK,oBAAoB,EAAI,EAAQ,EAAO,GAAM,CAEpE,IAAY,IAAA,KACd,MAAM,KAAK,mBAAmB,EAAI,EAAQ,CAGxC,EAAmC,SAAW,GAGlD,MAAM,KAAK,cAAc,EAAG,CAG9B,OAAO,EAGT,MAAM,OAAO,EAAY,CAEvB,OADc,KAAK,MACN,OAAO,EAAG,CAOzB,MAAM,gBAAgB,EAAkB,EAAgB,EAAuC,CAE7F,OADc,KAAK,MACN,gBAAgB,EAAU,EAAQ,EAAa,CAG9D,MAAM,kBAAkB,EAAkB,EAAiB,CAEzD,OADc,KAAK,MACN,kBAAkB,EAAU,EAAO,CAiBlD,MAAM,WAAW,EAAkB,EAAkD,CACnF,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,KAAI,OAAQ,MAAM,OAAO,eAE3B,EAAc,EAAe,IAAI,GAAG,KAAK,OAAO,KAAK,SAAS,CACpE,GAAI,CAAC,EAAa,OAElB,IAAM,EAAgB,MAAM,QAAQ,EAAQ,GAAG,CAAG,EAAQ,GAAK,CAAC,EAAQ,GAAG,CAGrE,EAAa,MAAM,KAAK,GAC3B,QAAQ,CACR,KAAK,EAAY,CACjB,MAAM,EAAI,EAAG,EAAY,SAAU,EAAS,CAAE,EAAG,EAAY,OAAQ,EAAQ,KAAK,CAAC,CAAC,CACpF,QAAQ,EAAY,UAAU,CAE7B,KAAW,SAAW,EAE1B,KAAK,IAAM,KAAgB,EAAe,CAExC,MAAM,KAAK,GACR,OAAO,EAAY,CACnB,MAAM,EAAI,EAAG,EAAY,SAAU,EAAS,CAAE,EAAG,EAAY,OAAQ,EAAa,CAAC,CAAC,CAGvF,IAAK,IAAM,KAAO,EAChB,MAAM,KAAK,GAAG,OAAO,EAAY,CAAC,OAAO,CACvC,SAAU,EAAI,SACd,UAAW,EAAI,UACf,UAAW,EAAI,UACf,UAAW,EAAI,UACf,KAAM,EAAI,KACV,OAAQ,EACT,CAAC,CAIN,KAAK,QAAQ,MACX,CAAE,WAAU,KAAM,EAAQ,KAAM,GAAI,EAAe,CACnD,gCACD,EAWH,MAAM,YACJ,EACA,EACyB,CACzB,IAAM,EAAS,KAAK,mBAAmB,CAUvC,OATK,EAEQ,MAAM,EAAO,SAAS,CACjC,MAAO,CAAE,WAAU,OAAQ,GAAS,QAAU,IAAK,CACnD,QAAS,CAAC,CAAE,OAAQ,UAAW,IAAK,OAAQ,CAAC,CAC7C,GAAI,GAAS,QAAU,IAAA,GAAuC,EAAE,CAA7B,CAAE,MAAO,EAAQ,MAAO,CAC3D,GAAI,GAAS,SAAW,IAAA,GAAyC,EAAE,CAA/B,CAAE,OAAQ,EAAQ,OAAQ,CAC/D,CAAC,CAPkB,EAAE,CAexB,MAAM,WAAW,EAAkB,EAAiB,EAAS,IAAmC,CAC9F,IAAM,EAAS,KAAK,mBAAmB,CAKvC,OAJK,EAEO,MAAM,EAAO,QAAQ,CAAE,WAAU,UAAS,SAAQ,CAAC,EAEjD,KAJM,KAatB,MAAM,gBACJ,EACA,EACA,EACA,EAAS,IACmF,CAC5F,GAAM,CAAC,EAAU,GAAY,MAAM,QAAQ,IAAI,CAC7C,KAAK,WAAW,EAAU,EAAI,EAAO,CACrC,KAAK,WAAW,EAAU,EAAI,EAAO,CACtC,CAAC,CAEF,GAAI,CAAC,GAAY,CAAC,EAAU,OAAO,KAEnC,IAAM,EAAQ,EAAS,KACjB,EAAQ,EAAS,KAEjB,EAAe,KAAK,OAAO,UAC3B,EAAQ,EAAW,EAAO,EAAO,EAAa,CAGpD,IAAK,IAAM,KAAQ,EACb,EAAK,OAAS,UAAY,MAAM,QAAQ,EAAK,KAAK,EAAI,MAAM,QAAQ,EAAK,GAAG,GAC9E,EAAK,WAAa,EAAW,EAAK,KAAM,EAAK,GAAG,EAIpD,MAAO,CAAE,QAAO,YAAa,EAAU,UAAW,EAAU,CAO9D,MAAM,wBACJ,EACA,EAAS,IACoF,CAC7F,IAAM,EAAQ,MAAM,KAAK,SAAS,EAAU,EAAO,CACnD,GAAI,CAAC,EAAO,OAAO,KAEnB,IAAM,EAAQ,KAAK,MAGb,CAAE,oBAAqB,MAAM,OAAO,gBAEpC,EADS,GAAkB,CACJ,cACvB,EAAc,IAAW,IAAM,CAAE,gBAAe,CAAG,CAAE,SAAQ,gBAAe,CAC5E,EAAW,MAAM,EAAM,SAAS,EAAU,EAAY,CAC5D,GAAI,CAAC,EAAS,OAAO,KAErB,IAAM,EAAe,KAAK,OAAO,UAI3B,EAAyC,EAAE,CACjD,IAAK,IAAM,KAAO,OAAO,KAAK,EAAM,KAAK,CACvC,EAAc,GAAO,EAAQ,GAE/B,IAAM,EAAQ,EAAW,EAAe,EAAM,KAAM,EAAa,CAGjE,IAAK,IAAM,KAAQ,EACb,EAAK,OAAS,UAAY,MAAM,QAAQ,EAAK,KAAK,EAAI,MAAM,QAAQ,EAAK,GAAG,GAC9E,EAAK,WAAa,EAAW,EAAK,KAAM,EAAK,GAAG,EAIpD,MAAO,CAAE,QAAO,UAAS,QAAO,CAMlC,MAAM,eAAe,EAAkB,EAAiB,CACtD,IAAM,EAAc,MAAM,KAAK,WAAW,EAAU,EAAQ,CAC5D,GAAI,CAAC,EACH,MAAU,MAAM,WAAW,EAAQ,wBAAwB,IAAW,CAMxE,GAAM,CAAE,KAAI,YAAW,YAAW,WAAU,eAAc,GAAG,GAHhD,EAAY,KAMzB,OADc,KAAK,MACN,OAAO,EAAU,EAAW,CAM3C,MAAM,cAAc,EAAkB,EAAS,IAAK,CAClD,GAAI,CAAC,KAAK,mBAAoB,OAC9B,IAAM,EAAS,KAAK,mBAAmB,CACvC,GAAI,CAAC,EAAQ,OAEb,IAAM,EAAY,KAAK,mBAAmB,UAGpC,EAAe,MAAM,EAAO,SAAS,CACzC,MAAO,CAAE,WAAU,SAAQ,CAC3B,QAAS,CAAC,CAAE,OAAQ,UAAW,IAAK,OAAQ,CAAC,CAC7C,MAAO,EACR,CAAC,CAEF,GAAI,EAAa,OAAS,EAAW,OAErC,IAAM,EAAU,EAAa,IAAK,GAAM,EAAE,GAAa,CAGvD,MAAM,EAAO,WAAW,CACtB,WACA,SACA,GAAI,CAAE,MAAO,EAAS,CACvB,CAAC,CAUJ,MAAM,qBAAqB,EAA8C,CACvE,GAAM,CAAE,oBAAqB,MAAM,OAAO,gBACpC,EAAS,GAAkB,CAG3B,EAAqB,OAAO,QAAQ,KAAK,OAAO,UAAyC,CAC5F,QAAQ,CAAC,EAAG,KAAO,EAAE,aAAa,CAClC,KAAK,CAAC,KAAU,EAAK,CAExB,GAAI,EAAmB,SAAW,EAAG,MAAO,EAAE,CAI9C,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,MAAO,MAAM,OAAO,eAEtB,EAAmB,EAAe,IAAI,GAAG,KAAK,OAAO,KAAK,eAAe,CAC/E,GAAI,CAAC,EAAkB,MAAO,EAAE,CAEhC,IAAM,EAAe,MAAM,KAAK,GAC7B,QAAQ,CACR,KAAK,EAAiB,CACtB,MAAM,EAAG,EAAiB,SAAU,EAAS,CAAC,CAG3C,EAAiB,IAAI,IAC3B,IAAK,IAAM,KAAK,EAAc,CAC5B,IAAM,EAAM,EACN,EAAuC,EAAE,CAC/C,IAAK,IAAM,KAAa,EACtB,EAAY,GAAa,EAAI,GAE/B,EAAe,IAAI,EAAI,OAAkB,EAAY,CAGvD,IAAM,EAA4B,EAAE,CAEpC,IAAK,IAAM,KAAU,EAAO,QAAS,CACnC,GAAI,EAAO,OAAS,EAAO,cAAe,SAE1C,IAAM,EAAa,EAAe,IAAI,EAAO,KAAK,EAAI,EAAE,CAClD,EAAoB,EAAE,CAE5B,IAAK,IAAM,KAAa,GAClB,EAAW,KAAe,IAAA,IAAa,EAAW,KAAe,OACnE,EAAQ,KAAK,EAAU,CAI3B,IAAM,EAAkB,EAAmB,OAAS,EAAQ,OAE5D,EAAO,EAAO,MAAQ,CACpB,SAAU,EAAQ,SAAW,EAC7B,MAAO,EAAmB,OAC1B,WAAY,EACZ,UACD,CAGH,OAAO,EAOT,MAAM,0BAA0B,EAA8D,CAC5F,IAAM,EAAS,IAAI,IACnB,GAAI,EAAU,SAAW,EAAG,OAAO,EAEnC,GAAM,CAAE,oBAAqB,MAAM,OAAO,gBACpC,EAAS,GAAkB,CAG3B,EAAqB,OAAO,QAAQ,KAAK,OAAO,UAAyC,CAC5F,QAAQ,CAAC,EAAG,KAAO,EAAE,aAAa,CAClC,KAAK,CAAC,KAAU,EAAK,CAExB,GAAI,EAAmB,SAAW,EAAG,OAAO,EAI5C,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,WAAY,MAAM,OAAO,eAE3B,EAAmB,EAAe,IAAI,GAAG,KAAK,OAAO,KAAK,eAAe,CAC/E,GAAI,CAAC,EAAkB,OAAO,EAE9B,IAAM,EAAe,MAAM,KAAK,GAC7B,QAAQ,CACR,KAAK,EAAiB,CACtB,MAAM,EAAQ,EAAiB,SAAU,EAAU,CAAC,CAGjD,EAAuB,IAAI,IACjC,IAAK,IAAM,KAAK,EAAc,CAC5B,IAAM,EAAM,EACN,EAAW,EAAI,SACf,EAAS,EAAI,OACb,EAAuC,EAAE,CAC/C,IAAK,IAAM,KAAa,EACtB,EAAY,GAAa,EAAI,GAE1B,EAAqB,IAAI,EAAS,EACrC,EAAqB,IAAI,EAAU,IAAI,IAAM,CAE/C,EAAqB,IAAI,EAAS,CAAE,IAAI,EAAQ,EAAY,CAI9D,IAAK,IAAM,KAAY,EAAW,CAChC,IAAM,EAAY,EAAqB,IAAI,EAAS,CAC9C,EAA4B,EAAE,CAEpC,IAAK,IAAM,KAAU,EAAO,QAAS,CACnC,GAAI,EAAO,OAAS,EAAO,cAAe,SAE1C,IAAM,EAAa,GAAW,IAAI,EAAO,KAAK,EAAI,EAAE,CAC9C,EAAoB,EAAE,CAE5B,IAAK,IAAM,KAAa,GAClB,EAAW,KAAe,IAAA,IAAa,EAAW,KAAe,OACnE,EAAQ,KAAK,EAAU,CAI3B,IAAM,EAAkB,EAAmB,OAAS,EAAQ,OAE5D,EAAO,EAAO,MAAQ,CACpB,SAAU,EAAQ,SAAW,EAC7B,MAAO,EAAmB,OAC1B,WAAY,EACZ,UACD,CAGH,EAAO,IAAI,EAAU,EAAO,CAG9B,OAAO,EAMT,MAAM,qBAAqB,EAA2D,CACpF,GAAM,CAAE,oBAAqB,MAAM,OAAO,gBACpC,EAAS,GAAkB,CAE3B,EACJ,GAAS,QAAU,EAAO,QAAQ,KAAM,GAAM,EAAE,OAAS,EAAO,cAAc,EAAE,KAElF,GAAI,CAAC,EAAc,MAAO,CAAE,MAAO,EAAG,SAAU,EAAG,QAAS,EAAG,QAAS,EAAG,CAI3E,IAAM,EAAW,MADH,KAAK,MACU,UAAU,CAEnC,EAAW,EACX,EAAU,EACV,EAAU,EAEd,IAAK,IAAM,KAAU,EAAU,CAE7B,IAAM,GADS,MAAM,KAAK,qBAAqB,EAAO,GAAG,EAC7B,GAExB,CAAC,GAAgB,EAAa,aAAe,EAC/C,IACS,EAAa,SACtB,IAEA,IAIJ,MAAO,CACL,MAAO,EAAS,OAChB,WACA,UACA,UACD,CAYH,MAAM,gBAAgB,EAAkD,CACtE,IAAM,EAAS,KAAK,uBAAuB,CAK3C,OAJK,GAEQ,MAAM,EAAO,SAAS,CAAE,MAAO,CAAE,WAAU,CAAE,CAAC,EAE/C,IAAK,IAAO,CACtB,OAAQ,EAAE,OACV,OAAQ,EAAE,OACV,YAAc,EAAE,aAA+B,KAChD,EAAE,CARiB,EAAE,CAexB,MAAM,gBACJ,EACA,EACA,EACe,CACf,GAAM,CAAE,oBAAqB,MAAM,OAAO,gBAI1C,GAAI,IAHW,GAAkB,CAGX,cAAe,CACnC,IAAM,EAAQ,KAAK,MACb,EAAyC,CAAE,SAAQ,CACrD,IAAW,cACb,EAAK,YAAc,IAAI,MAEzB,MAAM,EAAM,OAAO,EAAU,EAAK,CAClC,OAGF,IAAM,EAAS,KAAK,uBAAuB,CAC3C,GAAI,CAAC,EAAQ,OAEb,IAAM,EAAc,IAAW,YAAc,IAAI,KAAS,KAE1D,MAAM,EAAO,OACX,CAAE,WAAU,SAAQ,SAAQ,cAAa,CACzC,CAAE,OAAQ,CAAC,WAAY,SAAS,CAAE,IAAK,CAAE,SAAQ,cAAa,CAAE,CACjE,CAOH,MAAM,qBACJ,EAC2D,CAC3D,IAAM,EAAS,IAAI,IACnB,GAAI,EAAU,SAAW,EAAG,OAAO,EAEnC,IAAM,EAAS,KAAK,uBAAuB,CAC3C,GAAI,CAAC,EAAQ,OAAO,EAEpB,IAAM,EAAO,MAAM,EAAO,SAAS,CAAE,MAAO,CAAE,SAAU,CAAE,GAAI,EAAW,CAAE,CAAE,CAAC,CAE9E,IAAK,IAAM,KAAK,EAAM,CACpB,IAAM,EAAM,EAAE,SACR,EAAS,EAAE,OACZ,EAAO,IAAI,EAAI,EAClB,EAAO,IAAI,EAAK,EAAE,CAAC,CAErB,EAAO,IAAI,EAAI,CAAE,GAAU,CACzB,SACA,OAAQ,EAAE,OACV,YAAc,EAAE,aAA+B,KAChD,CAGH,OAAO,EAWT,MAAM,UACJ,EACA,EACA,EAAS,IACT,EACqB,CACrB,IAAM,EAAS,KAAK,iBAAiB,CACrC,GAAI,CAAC,EACH,MAAU,MACR,sCAAsC,KAAK,OAAO,KAAK,uCACxD,CAGH,IAAM,EAAM,IAAI,KACV,EAAY,GAAM,IAAM,GACxB,EAAgB,GAAM,MAAQ,KAE9B,EAAM,MAAM,EAAO,OACvB,CACE,WACA,SACA,OACA,YACA,gBACA,UAAW,EACX,UAAW,EACZ,CACD,CACE,OAAQ,CAAC,WAAY,SAAS,CAE9B,IAAK,CAAE,OAAM,UAAW,EAAK,CAC9B,CACF,CAED,OAAO,KAAK,aAAa,EAAI,CAM/B,MAAM,SAAS,EAAkB,EAAS,IAAiC,CACzE,IAAM,EAAS,KAAK,iBAAiB,CACrC,GAAI,CAAC,EAAQ,OAAO,KAEpB,IAAM,EAAM,MAAM,EAAO,QAAQ,CAAE,WAAU,SAAQ,CAAC,CACtD,OAAO,EAAM,KAAK,aAAa,EAAI,CAAG,KAMxC,MAAM,YAAY,EAAkB,EAAS,IAAuB,CAClE,IAAM,EAAS,KAAK,iBAAiB,CAIrC,OAHK,GAEW,MAAM,EAAO,WAAW,CAAE,WAAU,SAAQ,CAAC,EAC9C,OAAS,EAHJ,GAUtB,MAAM,aACJ,EACA,EAAS,IACT,EACkC,CAClC,IAAM,EAAQ,MAAM,KAAK,SAAS,EAAU,EAAO,CACnD,GAAI,CAAC,EACH,MAAU,MAAM,6BAA6B,EAAS,UAAU,IAAS,CAG3E,IAAM,EAAQ,KAAK,MAGf,EAAgD,KAChD,KAAK,qBACP,EAAgB,MAAM,EAAM,SAAS,EAAS,EAKhD,GAAM,CAAE,KAAI,YAAW,YAAW,WAAU,eAAc,GAAG,GAD3C,EAAM,KAElB,EAAS,MAAM,EAAM,OAAO,EAAU,EAAW,CAUvD,OAPA,MAAM,KAAK,YAAY,EAAU,EAAO,CAGpC,KAAK,oBACP,MAAM,KAAK,sBAAsB,EAAU,EAAQ,EAAe,EAAQ,EAAK,CAG1E,EAMT,MAAM,SAAS,EAAkB,EAAS,IAAuB,CAC/D,IAAM,EAAS,KAAK,iBAAiB,CAErC,OADK,EACE,EAAO,OAAO,CAAE,WAAU,SAAQ,CAAC,CADtB,GAQtB,MAAM,YAAY,EAAS,IAAsB,CAC/C,IAAM,EAAS,KAAK,iBAAiB,CAErC,OADK,EACE,EAAO,MAAM,CAAE,SAAQ,CAAC,CADX,EAQtB,MAAM,cAAc,EAAqB,EAAS,IAA2B,CAC3E,IAAM,EAAS,IAAI,IACnB,GAAI,EAAU,SAAW,EAAG,OAAO,EAEnC,IAAM,EAAS,KAAK,iBAAiB,CACrC,GAAI,CAAC,EAAQ,OAAO,EAEpB,IAAM,EAAO,MAAM,EAAO,SAAS,CACjC,MAAO,CAAE,SAAU,CAAE,GAAI,EAAW,CAAE,SAAQ,CAC/C,CAAC,CAEF,IAAK,IAAM,KAAO,EAChB,EAAO,IAAI,EAAI,SAAmB,CAGpC,OAAO,EAWT,MAAM,sBACJ,EACA,EACA,EACA,EACA,EACA,CACA,GAAI,CAAC,KAAK,mBAAoB,OAC9B,IAAM,EAAiB,GAAU,IAC3B,EAAQ,EAAe,KAAK,eAAe,EAAc,EAAY,CAAG,KACxE,EAAU,MAAM,KAAK,oBACzB,EACA,EACA,EACA,GACA,EACA,EACD,CAGG,IAAY,IAAA,IAAa,IAAmB,KAC9C,MAAM,KAAK,mBAAmB,EAAU,EAAQ,CAGlD,MAAM,KAAK,cAAc,EAAU,EAAe,CAgBpD,MAAc,mBAAmB,EAAkB,EAAiB,CAClE,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,MAAO,MAAM,OAAO,eACtB,EAAQ,EAAe,IAAI,KAAK,OAAO,KAAK,CAC7C,GACL,MAAM,KAAK,GAAG,OAAO,EAAM,CAAC,IAAI,CAAE,SAAU,EAAS,CAAC,CAAC,MAAM,EAAG,EAAM,GAAI,EAAS,CAAC,CAGtF,MAAc,oBACZ,EACA,EACA,EACA,EACA,EAAS,IACT,EAC6B,CAC7B,IAAM,EAAS,KAAK,mBAAmB,CACvC,GAAI,CAAC,EAAQ,OAGb,GAAM,CAAC,GAAU,MAAM,EAAO,UAAyC,CACrE,OAAQ,CAAE,WAAY,CAAE,GAAI,MAAO,OAAQ,UAAW,CAAE,CACxD,MAAO,CAAE,WAAU,SAAQ,CAC5B,CAAC,CAEI,EAAU,OAAO,GAAQ,YAAc,EAAE,CAAG,EAC5C,EAAU,EAAK,QAAqB,KAK1C,GAAI,EAAY,CACd,IAAM,EAAW,MAAM,EAAO,QAAQ,CACpC,WACA,WAAY,GACZ,UACA,SACD,CAAC,CACF,GAAI,EAKF,OAJA,MAAM,EAAO,OACX,CAAE,GAAI,EAAS,GAAc,CAC7B,CAAE,OAAM,QAAO,SAAQ,UAAW,IAAI,KAAQ,CAC/C,CACM,EAeX,OAXA,MAAM,EAAO,OAAO,CAClB,WACA,UACA,SACA,OACA,QACA,SACA,UAAW,GAAM,IAAO,EAAK,WAAyB,EAAK,WAAwB,KACnF,cAAe,GAAM,MAAQ,KAC7B,aACD,CAAC,CACK,EAGT,aAAqB,EAA2B,CAC9C,MAAO,CACL,GAAI,EAAI,GACR,SAAU,EAAI,SACd,OAAQ,EAAI,OACZ,KAAM,EAAI,KACV,UAAW,EAAI,UACf,cAAe,EAAI,eAAiB,KACpC,UAAW,EAAI,UACf,UAAW,EAAI,UAChB,CAGH,eACE,EACA,EACyB,CACzB,IAAM,EAAiC,EAAE,CAEzC,IAAK,IAAM,KAAO,OAAO,KAAK,EAAQ,CAChC,IAAQ,MAAQ,EAAI,WAAW,IAAI,EACnC,KAAK,UAAU,EAAS,GAAK,GAAK,KAAK,UAAU,EAAQ,GAAK,GAChE,EAAM,GAAO,EAAQ,IAIzB,OAAO,IAQX,eAAsB,EAAoB,EAAwC,CAChF,GAAM,CAAE,qBAAsB,MAAM,OAAO,6BACrC,CAAE,UAAW,MAAM,OAAO,qBAEhC,OAAO,IAAI,EAAc,CACvB,MAFY,EAAkB,EAAO,CAGrC,SACA,OAAQ,GAAQ,CAAC,OAAO,MAAM,CAAE,QAAS,EAAO,KAAM,CAAC,CACxD,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
function e(e){if(!e.slug||!e.slug.match(/^[a-z][a-z0-9-]*$/))throw Error(`Block slug must be lowercase alphanumeric with hyphens, got: "${e.slug}"`);if(!e.label)throw Error(`Block label is required for slug: "${e.slug}"`);if(!e.fields||Object.keys(e.fields).length===0)throw Error(`Block "${e.slug}" must have at least one field`);return{slug:e.slug,label:e.label,fields:e.fields}}export{e as t};
|
|
2
|
-
//# sourceMappingURL=define-block-
|
|
2
|
+
//# sourceMappingURL=define-block-DpMPEEdA.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-block-
|
|
1
|
+
{"version":3,"file":"define-block-DpMPEEdA.mjs","names":[],"sources":["../src/define-block.ts"],"sourcesContent":["/**\n * Block definition API\n *\n * defineBlock() creates a reusable content block with typed fields.\n * Uses `const F` generic to preserve literal field types for inference.\n *\n * Blocks are NOT entities — they lack independent identity, CRUD, and access control.\n * They exist only as embedded data within a parent entity's blocks field.\n *\n * @example\n * const HeroBlock = defineBlock({\n * slug: 'hero',\n * label: 'Hero Section',\n * fields: {\n * heading: field.text({ required: true, translatable: true }),\n * background: field.media({ accept: ['image/*'] }),\n * },\n * })\n */\n\nimport type { FieldConfig } from '@murumets-ee/entity'\nimport type { BlockDefinition } from './types.js'\n\nexport function defineBlock<const F extends Record<string, FieldConfig>>(definition: {\n slug: string\n label: string\n fields: F\n}): BlockDefinition<F> {\n if (!definition.slug || !definition.slug.match(/^[a-z][a-z0-9-]*$/)) {\n throw new Error(\n `Block slug must be lowercase alphanumeric with hyphens, got: \"${definition.slug}\"`,\n )\n }\n\n if (!definition.label) {\n throw new Error(`Block label is required for slug: \"${definition.slug}\"`)\n }\n\n if (!definition.fields || Object.keys(definition.fields).length === 0) {\n throw new Error(`Block \"${definition.slug}\" must have at least one field`)\n }\n\n return {\n slug: definition.slug,\n label: definition.label,\n fields: definition.fields,\n }\n}\n"],"mappings":"AAuBA,SAAgB,EAAyD,EAIlD,CACrB,GAAI,CAAC,EAAW,MAAQ,CAAC,EAAW,KAAK,MAAM,oBAAoB,CACjE,MAAU,MACR,iEAAiE,EAAW,KAAK,GAClF,CAGH,GAAI,CAAC,EAAW,MACd,MAAU,MAAM,sCAAsC,EAAW,KAAK,GAAG,CAG3E,GAAI,CAAC,EAAW,QAAU,OAAO,KAAK,EAAW,OAAO,CAAC,SAAW,EAClE,MAAU,MAAM,UAAU,EAAW,KAAK,gCAAgC,CAG5E,MAAO,CACL,KAAM,EAAW,KACjB,MAAO,EAAW,MAClB,OAAQ,EAAW,OACpB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as InferBlocksUnion, c as LockInfo, d as TranslationStatus, f as VersionEntry, i as InferBlockData, l as TranslationFieldStatus, n as ContentConfig, o as LocaleConfig, p as VersionableOptions, r as DraftEntry, t as BlockDefinition, u as TranslationReport } from "./types-
|
|
2
|
-
import { a as PropDiff, i as FieldDiff, n as BlockDiffStatus, r as DiffSegment, t as BlockDiffEntry } from "./types-
|
|
1
|
+
import { a as InferBlocksUnion, c as LockInfo, d as TranslationStatus, f as VersionEntry, i as InferBlockData, l as TranslationFieldStatus, n as ContentConfig, o as LocaleConfig, p as VersionableOptions, r as DraftEntry, t as BlockDefinition, u as TranslationReport } from "./types-D-ATlx7t.mjs";
|
|
2
|
+
import { a as PropDiff, i as FieldDiff, n as BlockDiffStatus, r as DiffSegment, t as BlockDiffEntry } from "./types-COcneY5H.mjs";
|
|
3
3
|
import { Behavior, DateField, FieldConfig, NumberField, SelectField, TextField } from "@murumets-ee/entity";
|
|
4
4
|
|
|
5
5
|
//#region src/behaviors/versionable.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./block-diff-
|
|
1
|
+
import{a as e,i as t,n,o as r,r as i,s as a,t as o}from"./block-diff-BBH6FoFT.mjs";import{t as s}from"./define-block-DpMPEEdA.mjs";import{field as c}from"@murumets-ee/entity";const l=[`draft`,`in_review`,`approved`,`rejected`,`published`];function u(){return{name:`workflowable`,fields:{_workflowStatus:c.select({options:l,default:`draft`,indexed:!0,required:!0}),_workflowAssignee:c.text({required:!1}),_workflowNote:c.text({required:!1}),_workflowUpdatedAt:c.date()},hooks:{beforeCreate:async e=>(e._workflowStatus=`draft`,e._workflowUpdatedAt=new Date,e)}}}export{l as WORKFLOW_STATES,s as defineBlock,o as diffBlocks,n as diffField,i as diffFields,t as diffStringValues,e as diffText,r as getVersionableOptions,a as versionable,u as workflowable};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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/lock-service.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as LockInfo } from "./types-
|
|
1
|
+
import { c as LockInfo } from "./types-D-ATlx7t.mjs";
|
|
2
2
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
3
|
|
|
4
4
|
//#region src/lock-service.d.ts
|
|
@@ -6,14 +6,13 @@ interface LockServiceConfig {
|
|
|
6
6
|
db: PostgresJsDatabase;
|
|
7
7
|
}
|
|
8
8
|
declare class LockService {
|
|
9
|
-
private
|
|
9
|
+
private client;
|
|
10
10
|
constructor(config: LockServiceConfig);
|
|
11
|
-
private getTable;
|
|
12
11
|
/**
|
|
13
12
|
* Acquire a lock on an entity+locale.
|
|
14
13
|
*
|
|
15
14
|
* - Deletes expired locks first
|
|
16
|
-
* -
|
|
15
|
+
* - Checks for existing lock; if same user → renew (re-entrant)
|
|
17
16
|
* - Returns { acquired: true } or { acquired: false, lock: LockInfo }
|
|
18
17
|
*/
|
|
19
18
|
acquireLock(entityType: string, entityId: string, locale: string, user: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock-service.d.mts","names":[],"sources":["../src/lock-service.ts"],"mappings":";;;;
|
|
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;EAwG0D;;;;;;;EA7FxE,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;EAAtD;;;;EAsDG,SAAA,CACJ,UAAA,UACA,QAAA,UACA,MAAA,UACA,MAAA,WACC,OAAA;EALG;;;EAoBA,WAAA,CACJ,UAAA,UACA,QAAA,UACA,MAAA,UACA,MAAA,WACC,OAAA;EArBD;;;;EA8BI,gBAAA,CAAiB,UAAA,UAAoB,QAAA,UAAkB,MAAA,WAAiB,OAAA;EAX5E;;;;EAoBI,OAAA,CAAQ,UAAA,UAAoB,QAAA,UAAkB,MAAA,WAAiB,OAAA,CAAQ,QAAA;AAAA;;;;iBAwBzD,iBAAA,CAAA,GAAqB,OAAA,CAAQ,WAAA"}
|
package/dist/lock-service.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import"server-only";var
|
|
1
|
+
import{t as e}from"./locks-BkO2qMfm.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);await this.client.deleteMany({entityType:e,entityId:t,locale:n,expiresAt:{lt:i}});let o=await this.client.findOne({entityType:e,entityId:t,locale:n});return o?o.lockedBy===r.id?(await this.client.update({id:o.id},{expiresAt:a,lockedAt:i}),{acquired:!0}):{acquired:!1,lock:{lockedBy:o.lockedBy,lockedByName:o.lockedByName??null,lockedAt:o.lockedAt,expiresAt:o.expiresAt}}:(await this.client.insert({entityType:e,entityId:t,locale:n,lockedBy:r.id,lockedByName:r.name??null,lockedAt:i,expiresAt:a}),{acquired:!0})}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}};async function n(){let{getApp:e}=await import(`@murumets-ee/core`);return new t({db:e().db.readWrite})}export{t as LockService,n as createLockService};
|
|
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\nimport 'server-only'\n\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { LockInfo } from './types.js'\n\n/** Lock duration in minutes */\nconst LOCK_DURATION_MINUTES = 5\n\nexport interface LockServiceConfig {\n db: PostgresJsDatabase\n}\n\nexport class LockService {\n private
|
|
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 * - Deletes expired locks first\n * - Checks for existing lock; if same user → renew (re-entrant)\n * - Returns { acquired: true } or { acquired: false, lock: LockInfo }\n */\n async acquireLock(\n entityType: string,\n entityId: string,\n locale: string,\n user: { id: string; name?: string },\n ): Promise<{ acquired: true } | { acquired: false; lock: LockInfo }> {\n const now = new Date()\n const expiresAt = new Date(now.getTime() + LOCK_DURATION_MINUTES * 60 * 1000)\n\n // 1. Clean up expired locks for this entity\n await this.client.deleteMany({\n entityType,\n entityId,\n locale,\n expiresAt: { lt: now },\n })\n\n // 2. Check for existing lock\n const existing = await this.client.findOne({ entityType, entityId, locale })\n\n if (existing) {\n // Re-entrant: same user → renew\n if (existing.lockedBy === user.id) {\n await this.client.update(\n { id: existing.id },\n { expiresAt, lockedAt: now },\n )\n return { acquired: true }\n }\n // Different user holds lock\n return {\n acquired: false,\n lock: {\n lockedBy: existing.lockedBy,\n lockedByName: existing.lockedByName ?? null,\n lockedAt: existing.lockedAt as Date,\n expiresAt: existing.expiresAt as Date,\n },\n }\n }\n\n // 3. No lock exists — insert\n await this.client.insert({\n entityType,\n entityId,\n locale,\n lockedBy: user.id,\n lockedByName: user.name ?? null,\n lockedAt: now,\n expiresAt,\n })\n\n return { acquired: true }\n }\n\n /**\n * Renew an existing lock (heartbeat).\n * Only succeeds if the caller holds the lock and it hasn't expired.\n */\n async renewLock(\n entityType: string,\n entityId: string,\n locale: string,\n userId: string,\n ): Promise<boolean> {\n const 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\n/**\n * Factory — creates a LockService using the app's DB connection.\n */\nexport async function createLockService(): Promise<LockService> {\n const { getApp } = await import('@murumets-ee/core')\n const app = getApp()\n return new LockService({ db: app.db.readWrite })\n}\n"],"mappings":"6DAyBA,IAAa,EAAb,KAAyB,CACvB,OAEA,YAAY,EAA2B,CACrC,KAAK,OAAS,EAAkB,WAAW,EAAO,GAAG,CAUvD,MAAM,YACJ,EACA,EACA,EACA,EACmE,CACnE,IAAM,EAAM,IAAI,KACV,EAAY,IAAI,KAAK,EAAI,SAAS,CAAG,IAA6B,IAAK,CAG7E,MAAM,KAAK,OAAO,WAAW,CAC3B,aACA,WACA,SACA,UAAW,CAAE,GAAI,EAAK,CACvB,CAAC,CAGF,IAAM,EAAW,MAAM,KAAK,OAAO,QAAQ,CAAE,aAAY,WAAU,SAAQ,CAAC,CAkC5E,OAhCI,EAEE,EAAS,WAAa,EAAK,IAC7B,MAAM,KAAK,OAAO,OAChB,CAAE,GAAI,EAAS,GAAI,CACnB,CAAE,YAAW,SAAU,EAAK,CAC7B,CACM,CAAE,SAAU,GAAM,EAGpB,CACL,SAAU,GACV,KAAM,CACJ,SAAU,EAAS,SACnB,aAAc,EAAS,cAAgB,KACvC,SAAU,EAAS,SACnB,UAAW,EAAS,UACrB,CACF,EAIH,MAAM,KAAK,OAAO,OAAO,CACvB,aACA,WACA,SACA,SAAU,EAAK,GACf,aAAc,EAAK,MAAQ,KAC3B,SAAU,EACV,YACD,CAAC,CAEK,CAAE,SAAU,GAAM,EAO3B,MAAM,UACJ,EACA,EACA,EACA,EACkB,CAClB,IAAM,EAAM,IAAI,KACV,EAAY,IAAI,KAAK,EAAI,SAAS,CAAG,IAA6B,IAAK,CAO7E,OALgB,MAAM,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,OADgB,MAAM,KAAK,OAAO,OAAO,CAAE,aAAY,WAAU,SAAQ,SAAU,EAAQ,CAAC,GACzE,KAOrB,MAAM,iBAAiB,EAAoB,EAAkB,EAAkC,CAE7F,OADgB,MAAM,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,OAcrB,eAAsB,GAA0C,CAC9D,GAAM,CAAE,UAAW,MAAM,OAAO,qBAEhC,OAAO,IAAI,EAAY,CAAE,GADb,GAAQ,CACa,GAAG,UAAW,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locks-BkO2qMfm.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: [\n { on: ['entityType', 'entityId', 'locale'] },\n ],\n})\n\n/** Backwards-compatible export — existing imports of the raw pgTable still work. */\nexport const toolkitContentLocks = contentLocksTable.table\n"],"mappings":"2KAYA,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,CACN,CAAE,GAAI,CAAC,aAAc,WAAY,SAAS,CAAE,CAC7C,CACF,CAAC,CAGW,EAAsB,EAAkB"}
|
package/dist/plugin.d.mts
CHANGED
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let e=null;function t(){if(!e)throw Error(`@murumets-ee/content plugin not initialized. Add content() to your plugins array.`);return e}function n(t){if(!t.locales||t.locales.length===0)throw Error(`content() requires at least one locale`);if(!t.defaultLocale)throw Error(`content() requires a defaultLocale`);if(!t.locales.some(e=>e.code===t.defaultLocale))throw Error(`defaultLocale "${t.defaultLocale}" not found in locales: [${t.locales.map(e=>e.code).join(`, `)}]`);return{name:`@murumets-ee/content`,init:async n=>{e=t;let{schemaRegistry:r}=await import(`@murumets-ee/db`),{
|
|
1
|
+
let e=null;function t(){if(!e)throw Error(`@murumets-ee/content plugin not initialized. Add content() to your plugins array.`);return e}function n(t){if(!t.locales||t.locales.length===0)throw Error(`content() requires at least one locale`);if(!t.defaultLocale)throw Error(`content() requires a defaultLocale`);if(!t.locales.some(e=>e.code===t.defaultLocale))throw Error(`defaultLocale "${t.defaultLocale}" not found in locales: [${t.locales.map(e=>e.code).join(`, `)}]`);return{name:`@murumets-ee/content`,init:async n=>{e=t;let{schemaRegistry:r}=await import(`@murumets-ee/db`),{generateVersionsTable:i}=await import(`./versions-CO6oDyet.mjs`),{generateLocaleStatusTable:a}=await import(`./locale-status-DTyv4wcn.mjs`),{generateDraftsTable:o}=await import(`./drafts-CyOivEtr.mjs`),{registerContentTables:s}=await import(`./registry-BJSOGFEN.mjs`).then(e=>e.n),{toolkitContentLocks:c}=await import(`./locks-BkO2qMfm.mjs`).then(e=>e.n),{generateLayoutSchema:l,generateLayoutTranslationSchema:u}=await import(`@murumets-ee/entity`),d=0,f=0,p=0,m=0;r.has(`toolkit_content_locks`)||r.register(`toolkit_content_locks`,c);for(let[e,t]of n.entities){if(t.behaviors?.some(e=>e.name===`versionable`)){let n=`${e}_versions`;if(!r.has(n)){let a=i(t);r.register(n,a.table),s(e,{versions:a})}d++}let n=t.allFields;if(Object.values(n).some(e=>e.type===`blocks`)){let n=`${e}_layout`;if(!r.has(n)){let e=l(t);e&&r.register(n,e)}let i=`${e}_layout_translations`;if(!r.has(i)){let e=u(t);e&&r.register(i,e)}f++}let c=t.behaviors?.some(e=>e.name===`publishable`),h=Object.values(n).some(e=>e.translatable);if(c&&h){let n=`${e}_locale_status`;if(!r.has(n)){let i=a(t);r.register(n,i.table),s(e,{localeStatus:i})}p++}if(c){let n=`${e}_drafts`;if(!r.has(n)){let i=o(t);r.register(n,i.table),s(e,{drafts:i})}m++}}n.logger.info({locales:t.locales.map(e=>e.code),defaultLocale:t.defaultLocale,versionableEntities:d,blockEntities:f,localeStatusEntities:p,draftEntities:m},`Content plugin initialized`)}}}export{n as content,t as getContentConfig};
|
|
2
2
|
//# sourceMappingURL=plugin.mjs.map
|
package/dist/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Content plugin — registers locale configuration and version schemas.\n *\n * Usage:\n * import { content } from '@murumets-ee/content/plugin'\n *\n * export default defineConfig({\n * plugins: [\n * content({\n * locales: [\n * { code: 'en', label: 'English' },\n * { code: 'de', label: 'German' },\n * ],\n * defaultLocale: 'en',\n * }),\n * ],\n * })\n */\n\nimport type { Plugin } from '@murumets-ee/core'\nimport type { ContentConfig } from './types.js'\n\nlet _contentConfig: ContentConfig | null = null\n\n/**\n * Get the content configuration. Throws if plugin not initialized.\n */\nexport function getContentConfig(): ContentConfig {\n if (!_contentConfig) {\n throw new Error(\n '@murumets-ee/content plugin not initialized. Add content() to your plugins array.',\n )\n }\n return _contentConfig\n}\n\n/**\n * Content plugin factory.\n */\nexport function content(config: ContentConfig): Plugin {\n // Validate config\n if (!config.locales || config.locales.length === 0) {\n throw new Error('content() requires at least one locale')\n }\n\n if (!config.defaultLocale) {\n throw new Error('content() requires a defaultLocale')\n }\n\n const defaultExists = config.locales.some((l) => l.code === config.defaultLocale)\n if (!defaultExists) {\n throw new Error(\n `defaultLocale \"${config.defaultLocale}\" not found in locales: [${config.locales.map((l) => l.code).join(', ')}]`,\n )\n }\n\n return {\n name: '@murumets-ee/content',\n init: async (app) => {\n // Store config globally\n _contentConfig = config\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const {
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Content plugin — registers locale configuration and version schemas.\n *\n * Usage:\n * import { content } from '@murumets-ee/content/plugin'\n *\n * export default defineConfig({\n * plugins: [\n * content({\n * locales: [\n * { code: 'en', label: 'English' },\n * { code: 'de', label: 'German' },\n * ],\n * defaultLocale: 'en',\n * }),\n * ],\n * })\n */\n\nimport type { Plugin } from '@murumets-ee/core'\nimport type { ContentConfig } from './types.js'\n\nlet _contentConfig: ContentConfig | null = null\n\n/**\n * Get the content configuration. Throws if plugin not initialized.\n */\nexport function getContentConfig(): ContentConfig {\n if (!_contentConfig) {\n throw new Error(\n '@murumets-ee/content plugin not initialized. Add content() to your plugins array.',\n )\n }\n return _contentConfig\n}\n\n/**\n * Content plugin factory.\n */\nexport function content(config: ContentConfig): Plugin {\n // Validate config\n if (!config.locales || config.locales.length === 0) {\n throw new Error('content() requires at least one locale')\n }\n\n if (!config.defaultLocale) {\n throw new Error('content() requires a defaultLocale')\n }\n\n const defaultExists = config.locales.some((l) => l.code === config.defaultLocale)\n if (!defaultExists) {\n throw new Error(\n `defaultLocale \"${config.defaultLocale}\" not found in locales: [${config.locales.map((l) => l.code).join(', ')}]`,\n )\n }\n\n return {\n name: '@murumets-ee/content',\n init: async (app) => {\n // Store config globally\n _contentConfig = config\n\n const { schemaRegistry } = await import('@murumets-ee/db')\n const { 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"],"mappings":"AAsBA,IAAI,EAAuC,KAK3C,SAAgB,GAAkC,CAChD,GAAI,CAAC,EACH,MAAU,MACR,oFACD,CAEH,OAAO,EAMT,SAAgB,EAAQ,EAA+B,CAErD,GAAI,CAAC,EAAO,SAAW,EAAO,QAAQ,SAAW,EAC/C,MAAU,MAAM,yCAAyC,CAG3D,GAAI,CAAC,EAAO,cACV,MAAU,MAAM,qCAAqC,CAIvD,GAAI,CADkB,EAAO,QAAQ,KAAM,GAAM,EAAE,OAAS,EAAO,cAAc,CAE/E,MAAU,MACR,kBAAkB,EAAO,cAAc,2BAA2B,EAAO,QAAQ,IAAK,GAAM,EAAE,KAAK,CAAC,KAAK,KAAK,CAAC,GAChH,CAGH,MAAO,CACL,KAAM,uBACN,KAAM,KAAO,IAAQ,CAEnB,EAAiB,EAEjB,GAAM,CAAE,kBAAmB,MAAM,OAAO,mBAClC,CAAE,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"}
|