@murumets-ee/entity 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/index.d.mts +318 -71
- package/dist/admin/index.d.mts.map +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/index.mjs.map +1 -1
- package/dist/index.d.mts +78 -9
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/query/index.d.mts +67 -19
- package/dist/query/index.d.mts.map +1 -1
- package/dist/query/index.mjs +1 -1
- package/dist/query/index.mjs.map +1 -1
- package/dist/refs/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/admin/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cursor.ts","../../src/dto-shaper.ts","../../src/refs/errors.ts","../../src/refs/extract-refs.ts","../../src/refs/schema.ts","../../src/shared/entity-data-ops.ts","../../src/validation.ts","../../src/admin/client.ts"],"sourcesContent":["/**\n * Cursor-based (keyset) pagination utilities.\n *\n * Cursor pagination avoids the performance cliff of OFFSET at scale (1M+ rows).\n * Instead of `OFFSET N`, it uses a WHERE condition:\n * `WHERE (sortField < lastValue) OR (sortField = lastValue AND id < lastId)`\n * which Postgres can serve from an index in constant time.\n *\n * The cursor is opaque to the client — base64-encoded JSON.\n *\n * Security:\n * - `field` must be whitelisted against the entity's actual fields\n * - `id` must be a valid UUID\n * - `value` is parameterized (never interpolated into SQL)\n * - Malformed cursors return null (caller returns 400)\n */\n\nimport { and, eq, getTableColumns, gt, lt, or, type SQL } from 'drizzle-orm'\nimport type { PgTable } from 'drizzle-orm/pg-core'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Cursor input for keyset pagination. */\nexport interface CursorInput {\n /** Sort field name (e.g. 'createdAt'). Must be a real column on the entity. */\n field: string\n /** Last seen value of the sort field. */\n value: string | number\n /** Sort direction — must match the ORDER BY direction. */\n direction: 'asc' | 'desc'\n /** Tie-breaker: last seen entity ID. Required for non-unique sort fields. */\n id?: string\n}\n\n/** Decoded cursor (internal, after validation). */\ninterface DecodedCursor {\n field: string\n value: string | number\n direction: 'asc' | 'desc'\n id?: string\n}\n\n// ---------------------------------------------------------------------------\n// UUID validation (same format used throughout the toolkit)\n// ---------------------------------------------------------------------------\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n// ---------------------------------------------------------------------------\n// Encode / decode\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a cursor for API transport (base64url).\n * Built from the last item in a result set.\n */\nexport function encodeCursor(\n item: Record<string, unknown>,\n sortField: string,\n direction: 'asc' | 'desc',\n): string {\n const payload: CursorInput = {\n field: sortField,\n value: item[sortField] as string | number,\n direction,\n id: item.id as string | undefined,\n }\n return btoa(JSON.stringify(payload))\n}\n\n/**\n * Decode and validate a cursor string from query params.\n * Returns null if the cursor is malformed, tampered, or invalid.\n *\n * Security: the `field` value is NOT validated here — the caller must\n * whitelist it against the entity's actual columns.\n */\nexport function decodeCursor(encoded: string): DecodedCursor | null {\n try {\n const json = atob(encoded)\n const parsed: unknown = JSON.parse(json)\n\n if (typeof parsed !== 'object' || parsed === null) return null\n const obj = parsed as Record<string, unknown>\n\n // Validate field\n if (typeof obj.field !== 'string' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(obj.field)) {\n return null\n }\n\n // Validate value (string or number)\n if (typeof obj.value !== 'string' && typeof obj.value !== 'number') {\n return null\n }\n\n // Validate direction\n if (obj.direction !== 'asc' && obj.direction !== 'desc') {\n return null\n }\n\n // Validate id (optional, must be UUID if present)\n if (obj.id !== undefined) {\n if (typeof obj.id !== 'string' || !UUID_RE.test(obj.id)) {\n return null\n }\n }\n\n return {\n field: obj.field,\n value: obj.value,\n direction: obj.direction,\n id: obj.id as string | undefined,\n }\n } catch {\n return null\n }\n}\n\n// ---------------------------------------------------------------------------\n// SQL condition builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build the keyset WHERE condition from a decoded cursor.\n *\n * For DESC: `WHERE (field < value) OR (field = value AND id < cursorId)`\n * For ASC: `WHERE (field > value) OR (field = value AND id > cursorId)`\n *\n * The caller must verify that `cursor.field` exists on the table before calling.\n *\n * @param table - Drizzle table with columns\n * @param cursor - Decoded and validated cursor\n * @returns SQL condition, or null if the field doesn't exist on the table\n */\nexport function buildCursorCondition(table: PgTable, cursor: DecodedCursor): SQL | null {\n const cols = getTableColumns(table)\n const column = cols[cursor.field]\n if (!column) return null\n\n const isDesc = cursor.direction === 'desc'\n const compare = isDesc ? lt : gt\n\n // Primary condition: sort field passes the cursor value\n const fieldCondition = compare(column, cursor.value)\n\n // Without tie-breaker ID, use simple comparison\n if (!cursor.id) {\n return fieldCondition\n }\n\n // With tie-breaker: (field < value) OR (field = value AND id < cursorId)\n const idColumn = cols.id\n if (!idColumn) return fieldCondition\n\n const idCondition = compare(idColumn, cursor.id)\n // Drizzle's `or`/`and` return SQL when called with at least one defined arg.\n // Fall back to fieldCondition rather than asserting non-null.\n return or(fieldCondition, and(eq(column, cursor.value), idCondition)) ?? fieldCondition\n}\n","/**\n * DTO Shaper\n * Transforms raw DB rows into clean DTOs.\n * All fields are real columns — just read from row directly.\n */\n\nimport type { Entity } from './define-entity.js'\nimport type { FieldConfig } from './fields/base.js'\nimport type { InferEntityDTO } from './types/infer.js'\n\nexport interface ShapeDtoOptions {\n /** Explicit field selection. */\n select?: string[]\n /**\n * Include internal fields in the output. Two rules apply:\n * 1. Fields marked `internal: true` in their config (the canonical flag).\n * 2. Legacy infrastructure columns whose name starts with `_` and are not\n * declared in `entity.allFields` (e.g. `_version`, `_scopeId` added by\n * the schema generator).\n *\n * Set this on trusted server reads that need to inspect or transition\n * state-machine fields (e.g. the workflow route reading the current\n * `_workflowStatus` before deciding the next state).\n */\n includeInternal?: boolean\n}\n\n/**\n * Shape a raw DB row into a typed DTO.\n * Every field is a real column — read directly from row.\n */\nexport function shapeDto<AllFields extends Record<string, FieldConfig>>(\n entity: Entity<AllFields>,\n row: Record<string, unknown>,\n options?: ShapeDtoOptions,\n): InferEntityDTO<AllFields> | null {\n if (!row) return null\n\n const result: Record<string, unknown> = {}\n const fieldsToInclude = options?.select || Object.keys(entity.allFields)\n\n for (const fieldName of fieldsToInclude) {\n const fieldConfig = (entity.allFields as Record<string, FieldConfig>)[fieldName]\n\n if (!options?.includeInternal) {\n // Two complementary internal-field rules, both bypassed by `includeInternal`:\n // 1. Naming convention: any field whose name starts with `_` (legacy\n // infrastructure columns like `_version`, `_scopeId`, plus any\n // `_workflowStatus`-style behavior field).\n // 2. Explicit flag: `internal: true` on the field config (the canonical\n // flag — works for fields that don't follow the underscore convention).\n const internalByName = fieldName.startsWith('_')\n const internalByFlag = fieldConfig?.internal === true\n if (internalByName || internalByFlag) continue\n }\n\n if (!fieldConfig) continue\n\n // Blocks are loaded separately from layout tables\n if (fieldConfig.type === 'blocks') continue\n\n result[fieldName] = row[fieldName]\n }\n\n return result as InferEntityDTO<AllFields>\n}\n\n/**\n * Shape multiple rows\n */\nexport function shapeDtos<AllFields extends Record<string, FieldConfig>>(\n entity: Entity<AllFields>,\n rows: Record<string, unknown>[],\n options?: ShapeDtoOptions,\n): (InferEntityDTO<AllFields> | null)[] {\n return rows.map((row) => shapeDto(entity, row, options))\n}\n","/**\n * Error thrown when attempting to delete an entity that is still referenced.\n */\n\nimport type { EntityUsage } from './find-usages.js'\n\nexport class ReferencedEntityError extends Error {\n public readonly entityName: string\n public readonly entityId: string\n public readonly usages: EntityUsage[]\n\n constructor(entityName: string, entityId: string, usages: EntityUsage[]) {\n const count = usages.length\n super(\n `Cannot delete ${entityName} '${entityId}': referenced by ${count} other entit${count === 1 ? 'y' : 'ies'}`,\n )\n this.name = 'ReferencedEntityError'\n this.entityName = entityName\n this.entityId = entityId\n this.usages = usages\n }\n}\n","/**\n * Schema-aware reference extraction.\n *\n * Walks entity field definitions and data to find all outgoing references.\n * Handles:\n * - field.media() → target = 'media', single UUID\n * - field.reference() → target = config.entity, single UUID or UUID[]\n * - field.blocks() → inspects block instance data for media/reference fields\n */\n\nimport type { BlocksField, FieldConfig, ReferenceField } from '../fields/base.js'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nexport interface ExtractedRef {\n sourceField: string\n targetEntity: string\n targetId: string\n}\n\n/**\n * Extract all outgoing references from entity data.\n *\n * @param allFields - The entity's complete field map (entity.allFields)\n * @param data - The entity data (full on create, partial on update)\n * @returns Deduplicated array of extracted references\n */\nexport function extractRefs(\n allFields: Record<string, FieldConfig>,\n data: Record<string, unknown>,\n): ExtractedRef[] {\n const refs: ExtractedRef[] = []\n const seen = new Set<string>()\n\n function add(sourceField: string, targetEntity: string, targetId: string) {\n if (!UUID_RE.test(targetId)) return\n const key = `${sourceField}|${targetEntity}|${targetId}`\n if (seen.has(key)) return\n seen.add(key)\n refs.push({ sourceField, targetEntity, targetId })\n }\n\n for (const [fieldName, config] of Object.entries(allFields)) {\n const value = data[fieldName]\n if (value == null) continue\n\n switch (config.type) {\n case 'media': {\n if (typeof value === 'string') {\n add(fieldName, 'media', value)\n }\n break\n }\n\n case 'reference': {\n const refConfig = config as ReferenceField\n if (refConfig.cardinality === 'many' && Array.isArray(value)) {\n for (const id of value) {\n if (typeof id === 'string') {\n add(fieldName, refConfig.entity, id)\n }\n }\n } else if (typeof value === 'string') {\n add(fieldName, refConfig.entity, value)\n }\n break\n }\n\n case 'blocks': {\n const blocksConfig = config as BlocksField\n if (!Array.isArray(value)) break\n\n for (const block of value) {\n const inst = block as Record<string, unknown>\n const blockType = inst._block as string | undefined\n if (!blockType) continue\n\n // Find the block definition to know its field types\n const blockDef = blocksConfig.blocks.find((b) => b.slug === blockType)\n if (!blockDef) continue\n\n // Scan block fields for media/reference values\n for (const [blockFieldName, blockFieldConfig] of Object.entries(blockDef.fields)) {\n const blockValue = inst[blockFieldName]\n if (blockValue == null) continue\n\n if (blockFieldConfig.type === 'media' && typeof blockValue === 'string') {\n add(fieldName, 'media', blockValue)\n }\n\n if (blockFieldConfig.type === 'reference') {\n const blockRefConfig = blockFieldConfig as ReferenceField\n if (blockRefConfig.cardinality === 'many' && Array.isArray(blockValue)) {\n for (const id of blockValue) {\n if (typeof id === 'string') {\n add(fieldName, blockRefConfig.entity, id)\n }\n }\n } else if (typeof blockValue === 'string') {\n add(fieldName, blockRefConfig.entity, blockValue)\n }\n }\n }\n }\n break\n }\n }\n }\n\n return refs\n}\n\n/**\n * Get the names of all ref-bearing fields in the entity.\n * Used to scope partial-update ref deletion.\n */\nexport function getRefBearingFields(allFields: Record<string, FieldConfig>): string[] {\n const names: string[] = []\n for (const [fieldName, config] of Object.entries(allFields)) {\n if (config.type === 'media' || config.type === 'reference' || config.type === 'blocks') {\n names.push(fieldName)\n }\n }\n return names\n}\n","/**\n * entity_refs — Universal reference tracking table.\n *\n * Every reference between entities (field.media(), field.reference(), block media fields)\n * gets a row here at write time. This enables:\n * - Instant \"where is this used?\" queries (one indexed lookup)\n * - Universal delete protection (can't delete referenced entities)\n * - Correct results (schema-aware, no LIKE text search)\n */\n\nimport { index, pgTable, unique, uuid, varchar } from 'drizzle-orm/pg-core'\n\nexport const entityRefs = pgTable(\n 'entity_refs',\n {\n sourceEntity: varchar('source_entity', { length: 100 }).notNull(),\n sourceId: uuid('source_id').notNull(),\n sourceField: varchar('source_field', { length: 100 }).notNull(),\n targetEntity: varchar('target_entity', { length: 100 }).notNull(),\n targetId: uuid('target_id').notNull(),\n },\n (t) => [\n unique('uq_entity_refs').on(\n t.sourceEntity,\n t.sourceId,\n t.sourceField,\n t.targetEntity,\n t.targetId,\n ),\n index('idx_entity_refs_target').on(t.targetEntity, t.targetId),\n index('idx_entity_refs_source').on(t.sourceEntity, t.sourceId),\n ],\n)\n","/**\n * Shared entity data operations.\n *\n * Extracted from AdminClient and QueryClient to eliminate duplication.\n * These are standalone functions that take an EntityContext — both clients\n * satisfy this interface via `this`.\n */\n\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { and, eq, inArray, isNull, or, type SQL } from 'drizzle-orm'\nimport type { PgTableWithColumns } from 'drizzle-orm/pg-core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { BehaviorContext } from '../behaviors/types.js'\nimport type { Entity } from '../define-entity.js'\nimport type { FieldConfig } from '../fields/base.js'\n\n// ---------------------------------------------------------------------------\n// Context interface — satisfied by both AdminClient and QueryClient via `this`\n// ---------------------------------------------------------------------------\n\n/**\n * Security context resolved per-request. Provided by the consumer\n * (e.g., via React.cache() in Next.js, or runAsCli for CLI).\n * The entity package has zero knowledge of how this is resolved.\n */\nexport interface SecurityContext {\n user: { id: string; groups: string[]; name?: string; email?: string }\n checker: (role: string, resource: string, action: string) => boolean\n scope?: { type: string; id: string }\n}\n\n/**\n * Function that resolves the current request's security context.\n * Injected at construction time — the entity package never imports\n * @murumets-ee/core or any framework-specific code.\n *\n * Returns undefined only when intentionally skipping enforcement\n * (should not happen in production — throw ForbiddenError instead).\n */\nexport type ContextResolver = () =>\n | SecurityContext\n | undefined\n | Promise<SecurityContext | undefined>\n\nexport interface EntityContext {\n entity: Entity\n db: PostgresJsDatabase\n // biome-ignore lint/suspicious/noExplicitAny: dynamic table columns require PgTableWithColumns<any>\n table: PgTableWithColumns<any>\n /** Resolves the current request's security context. */\n resolveContext?: ContextResolver\n}\n\n// ---------------------------------------------------------------------------\n// Field accessors\n// ---------------------------------------------------------------------------\n\n/** Get the entity's full field map with proper typing. Eliminates repeated casts. */\nexport function getAllFields(ctx: EntityContext): Record<string, FieldConfig> {\n return ctx.entity.allFields as Record<string, FieldConfig>\n}\n\n/** Get all blocks field definitions for this entity. */\nexport function getBlocksFields(ctx: EntityContext): Array<{ name: string; config: FieldConfig }> {\n return Object.entries(getAllFields(ctx))\n .filter(([_, config]) => config.type === 'blocks')\n .map(([name, config]) => ({ name, config }))\n}\n\n// ---------------------------------------------------------------------------\n// Translation merging\n// ---------------------------------------------------------------------------\n\n/**\n * Merge translations into entities for the specified locale.\n * Reads translatable field values from real columns on the translation row.\n */\nexport async function mergeTranslations<T extends Record<string, unknown>>(\n ctx: EntityContext,\n entities: T[],\n locale: string,\n): Promise<T[]> {\n if (!entities.length) return entities\n\n const translationTableName = `${ctx.entity.name}_translations`\n const translationTable = schemaRegistry.get(translationTableName)\n\n if (!translationTable) return entities\n\n const entityIds = entities.map((e) => e.id)\n\n const translations = await ctx.db\n .select()\n .from(translationTable)\n .where(and(inArray(translationTable.entityId, entityIds), eq(translationTable.locale, locale)))\n\n // Get translatable field names\n const translatableFields = Object.entries(getAllFields(ctx))\n .filter(([_, config]) => config.translatable)\n .map(([name]) => name)\n\n const translationMap = new Map<unknown, Record<string, unknown>>()\n for (const translation of translations) {\n const translatedValues: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n const value = (translation as Record<string, unknown>)[fieldName]\n if (value !== undefined && value !== null) {\n translatedValues[fieldName] = value\n }\n }\n translationMap.set(translation.entityId, translatedValues)\n }\n\n return entities.map((entity) => {\n const translatedFields = translationMap.get(entity.id)\n if (!translatedFields) return entity\n return { ...entity, ...translatedFields }\n })\n}\n\n// ---------------------------------------------------------------------------\n// Block loading\n// ---------------------------------------------------------------------------\n\nexport interface LoadBlocksOptions {\n defaultLocale?: string\n /**\n * When true (admin editing), clears untranslated translatable block fields\n * to empty string — signals incomplete translation in the editing UI.\n * When false (visitor-facing), preserves base data as fallback.\n */\n strictTranslations?: boolean\n}\n\n/**\n * Load blocks for one or more entities from the layout table.\n *\n * Handles both block translation modes:\n * - Shared layout (localized: false): loads locale=NULL rows, merges translations\n * - Per-locale layout (localized: true): loads rows matching the provided locale only\n */\nexport async function loadBlocks(\n ctx: EntityContext,\n entityIds: string[],\n locale?: string,\n options?: LoadBlocksOptions,\n): Promise<Map<string, Record<string, unknown[]>>> {\n const blocksFields = getBlocksFields(ctx)\n if (blocksFields.length === 0 || entityIds.length === 0) {\n return new Map()\n }\n\n const layoutTable = schemaRegistry.get(`${ctx.entity.name}_layout`)\n if (!layoutTable) return new Map()\n\n // Determine block field modes\n const sharedFields = blocksFields.filter(\n ({ config }) => !('localized' in config && config.localized),\n )\n const localizedFields = blocksFields.filter(\n ({ config }) => 'localized' in config && config.localized,\n )\n\n const result = new Map<string, Record<string, unknown[]>>()\n\n // ---- Shared blocks: locale IS NULL, translations from layout_translations ----\n if (sharedFields.length > 0) {\n const rows = await ctx.db\n .select()\n .from(layoutTable)\n .where(and(inArray(layoutTable.entityId, entityIds), isNull(layoutTable.locale)))\n .orderBy(layoutTable.sortOrder)\n\n // Load translations if locale provided\n let blockTransMap: Map<string, Record<string, unknown>> | undefined\n if (locale && rows.length > 0) {\n const layoutTransTable = schemaRegistry.get(`${ctx.entity.name}_layout_translations`)\n if (layoutTransTable) {\n const layoutIds = rows.map((r) => r.id as string)\n const translations = await ctx.db\n .select()\n .from(layoutTransTable)\n .where(\n and(inArray(layoutTransTable.layoutId, layoutIds), eq(layoutTransTable.locale, locale)),\n )\n\n blockTransMap = new Map()\n for (const t of translations) {\n blockTransMap.set(t.layoutId as string, (t.fields as Record<string, unknown>) ?? {})\n }\n }\n }\n\n // Build block definition lookup for translatable field clearing (strict mode only)\n let blockDefMap: Map<string, Record<string, FieldConfig>> | undefined\n if (options?.strictTranslations) {\n blockDefMap = new Map()\n for (const { config } of sharedFields) {\n if (config.type !== 'blocks') continue\n for (const def of config.blocks) {\n blockDefMap.set(def.slug, def.fields)\n }\n }\n }\n\n for (const row of rows) {\n const eid = row.entityId as string\n const fname = row.fieldName as string\n\n let entityBlocks = result.get(eid)\n if (!entityBlocks) {\n entityBlocks = {}\n result.set(eid, entityBlocks)\n }\n if (!entityBlocks[fname]) entityBlocks[fname] = []\n\n const blockData = (row.data as Record<string, unknown>) ?? {}\n const translatedFields = blockTransMap?.get(row.id as string)\n\n // Build the block object\n const block: Record<string, unknown> = {\n _block: row.blockType,\n _id: row.id,\n ...blockData,\n ...(translatedFields ?? {}),\n }\n\n // Strict mode: clear untranslated translatable fields when loading for non-default locale\n if (locale && blockDefMap) {\n const blockType = row.blockType as string\n const fieldDefs = blockDefMap.get(blockType)\n if (fieldDefs) {\n for (const [fieldName, fieldConfig] of Object.entries(fieldDefs)) {\n if (fieldConfig.translatable && !translatedFields?.[fieldName]) {\n block[fieldName] = ''\n }\n }\n }\n }\n\n entityBlocks[fname].push(block)\n }\n }\n\n // ---- Localized blocks: filter by locale column ----\n // NULL rows (from initial create) only fall back for the default locale.\n // Non-default locales without locale-specific rows get an empty array.\n if (localizedFields.length > 0) {\n const isDefault = !locale || locale === options?.defaultLocale\n let localeCondition: SQL\n if (locale) {\n if (isDefault) {\n // Both args defined → drizzle's `or` always returns SQL; fall back defensively\n localeCondition =\n or(eq(layoutTable.locale, locale), isNull(layoutTable.locale)) ??\n eq(layoutTable.locale, locale)\n } else {\n localeCondition = eq(layoutTable.locale, locale)\n }\n } else {\n localeCondition = isNull(layoutTable.locale)\n }\n\n const rows = await ctx.db\n .select()\n .from(layoutTable)\n .where(and(inArray(layoutTable.entityId, entityIds), localeCondition))\n .orderBy(layoutTable.sortOrder)\n\n // Group by entityId + fieldName, prefer locale-specific rows over NULL\n const grouped = new Map<string, { localeRows: typeof rows; nullRows: typeof rows }>()\n for (const row of rows) {\n const key = `${row.entityId}::${row.fieldName}`\n let group = grouped.get(key)\n if (!group) {\n group = { localeRows: [], nullRows: [] }\n grouped.set(key, group)\n }\n if (row.locale) {\n group.localeRows.push(row)\n } else {\n group.nullRows.push(row)\n }\n }\n\n for (const [, { localeRows, nullRows }] of grouped) {\n // Use locale-specific rows if available, otherwise fall back to NULL\n const effective = localeRows.length > 0 ? localeRows : nullRows\n\n for (const row of effective) {\n const eid = row.entityId as string\n const fname = row.fieldName as string\n\n let entityBlocks = result.get(eid)\n if (!entityBlocks) {\n entityBlocks = {}\n result.set(eid, entityBlocks)\n }\n if (!entityBlocks[fname]) entityBlocks[fname] = []\n\n const blockData = (row.data as Record<string, unknown>) ?? {}\n\n entityBlocks[fname].push({\n _block: row.blockType,\n _id: row.id,\n ...blockData,\n })\n }\n }\n }\n\n return result\n}\n\n/**\n * Attach loaded blocks to shaped DTOs.\n */\nexport function attachBlocks(\n ctx: EntityContext,\n entities: Record<string, unknown>[],\n blocksMap: Map<string, Record<string, unknown[]>>,\n): void {\n const blocksFields = getBlocksFields(ctx)\n if (blocksFields.length === 0) return\n\n for (const entity of entities) {\n const eid = entity.id as string\n const entityBlocks = blocksMap.get(eid) ?? {}\n\n for (const { name } of blocksFields) {\n entity[name] = entityBlocks[name] ?? []\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Count cache key\n// ---------------------------------------------------------------------------\n\n/**\n * Build a cache key for a count query.\n * @param prefix - Optional namespace prefix (e.g. 'query' for QueryClient)\n * @param scopeId - Optional scope ID for multi-tenant isolation\n */\nexport function buildCountCacheKey(\n entityName: string,\n where?: SQL,\n prefix?: string,\n scopeId?: string,\n): string {\n let base = prefix ? `${prefix}:${entityName}` : entityName\n if (scopeId) base = `${base}@${scopeId}`\n if (!where) return base\n // Use SQL's toString() representation as a stable key.\n // This is a display string, not executed — safe for cache keying.\n return `${base}:${String(where)}`\n}\n\n// ---------------------------------------------------------------------------\n// ForbiddenError\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when a user lacks permission for an entity operation.\n * Consumers (api-handler) catch this and return HTTP 403.\n */\nexport class ForbiddenError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\n// ---------------------------------------------------------------------------\n// Context helpers — dynamic imports to avoid entity → core circular dep\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Context resolution — framework-agnostic\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the security context from the resolver on EntityContext.\n * Throws ForbiddenError if no resolver is configured or if it returns undefined.\n */\nasync function resolveSecurityContext(\n resolver: ContextResolver | undefined,\n): Promise<SecurityContext> {\n if (!resolver) {\n throw new ForbiddenError(\n 'No context resolver configured on AdminClient/QueryClient. ' +\n 'Use createAdminClient() from @murumets-ee/core/clients, or pass a contextResolver in config.',\n )\n }\n\n const ctx = await resolver()\n\n if (!ctx) {\n throw new ForbiddenError(\n 'Context resolver returned no context. ' +\n 'Ensure auth is configured in your request handler, or use runAsCli() for CLI scripts.',\n )\n }\n\n return ctx\n}\n\n// ---------------------------------------------------------------------------\n// Permission enforcement\n// ---------------------------------------------------------------------------\n\n/**\n * Assert the current user has permission for the given action on the entity.\n * Uses the PermissionChecker from the resolved SecurityContext.\n */\nexport async function assertEntityAccess(\n entity: Entity,\n resolver: ContextResolver | undefined,\n action: 'view' | 'create' | 'update' | 'delete',\n): Promise<void> {\n const ctx = await resolveSecurityContext(resolver)\n\n const role = ctx.user.groups[0]\n if (!role) {\n throw new ForbiddenError(`User '${ctx.user.id}' has no role assigned.`)\n }\n\n if (!ctx.checker(role, entity.name, action)) {\n throw new ForbiddenError(`Forbidden: role '${role}' cannot ${action} '${entity.name}'`)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Scope filtering\n// ---------------------------------------------------------------------------\n\n/**\n * Get the current scope ID for a scoped entity.\n * Returns undefined for global entities.\n */\nexport async function getScopeId(\n entity: Entity,\n resolver: ContextResolver | undefined,\n): Promise<string | undefined> {\n if (!entity.scope || entity.scope === 'global') return undefined\n\n const ctx = await resolveSecurityContext(resolver)\n return ctx.scope?.id\n}\n\n/**\n * Build a WHERE condition for scope filtering.\n */\nexport function buildScopeCondition(ctx: EntityContext, scopeId: string): SQL {\n return eq(ctx.table._scopeId, scopeId)\n}\n\n/**\n * Require scope for a scoped entity. Throws if no scope is in context.\n * Returns undefined for global entities.\n */\nexport async function requireScopeForEntity(\n entity: Entity,\n resolver: ContextResolver | undefined,\n): Promise<string | undefined> {\n if (!entity.scope || entity.scope === 'global') return undefined\n\n const ctx = await resolveSecurityContext(resolver)\n const scopeId = ctx.scope?.id\n\n if (!scopeId) {\n throw new ForbiddenError(\n `Entity '${entity.name}' requires scope '${entity.scope}' but no scope is set in context.`,\n )\n }\n\n return scopeId\n}\n\n// ---------------------------------------------------------------------------\n// Combined auth context resolution (single resolver call)\n// ---------------------------------------------------------------------------\n\nexport interface ResolvedAuthContext {\n /** The full SecurityContext returned by the resolver. */\n security: SecurityContext\n /** Scope ID for scoped entities; undefined for global. */\n scopeId: string | undefined\n /** BehaviorContext to thread into lifecycle hooks. */\n behaviorCtx: BehaviorContext\n}\n\n/**\n * Resolve security context, check permission, derive scope ID, and build\n * BehaviorContext — all from a single resolver invocation.\n *\n * Use instead of calling `assertEntityAccess` + `getScopeId`/`requireScopeForEntity`\n * + manually building a behavior context. Reduces 2-3 resolver calls per write\n * to one, while keeping the granular helpers available for special cases\n * (e.g. cascade delete, which checks permission on a different entity).\n *\n * @param strictScope When true and entity is scoped without a scope in context,\n * throws ForbiddenError. Set on write paths; leave false for\n * reads (callers receive scopeId=undefined and skip filtering).\n */\nexport async function resolveAuthContext(\n entity: Entity,\n resolver: ContextResolver | undefined,\n action: 'view' | 'create' | 'update' | 'delete',\n options?: { strictScope?: boolean },\n): Promise<ResolvedAuthContext> {\n const security = await resolveSecurityContext(resolver)\n\n // Permission check — same logic as assertEntityAccess()\n const role = security.user.groups[0]\n if (!role) {\n throw new ForbiddenError(`User '${security.user.id}' has no role assigned.`)\n }\n if (!security.checker(role, entity.name, action)) {\n throw new ForbiddenError(`Forbidden: role '${role}' cannot ${action} '${entity.name}'`)\n }\n\n // Scope resolution — same logic as getScopeId()/requireScopeForEntity()\n let scopeId: string | undefined\n if (entity.scope && entity.scope !== 'global') {\n scopeId = security.scope?.id\n if (!scopeId && options?.strictScope) {\n throw new ForbiddenError(\n `Entity '${entity.name}' requires scope '${entity.scope}' but no scope is set in context.`,\n )\n }\n }\n\n const behaviorCtx: BehaviorContext = {\n user: { id: security.user.id, name: security.user.name, email: security.user.email },\n }\n\n return { security, scopeId, behaviorCtx }\n}\n","/**\n * Validation schema generator\n * Converts entity field definitions to Zod schemas for runtime validation\n */\n\nimport { z } from 'zod'\nimport type { Entity } from './define-entity.js'\nimport type { FieldConfig } from './fields/base.js'\n\n/**\n * Convert a field definition to a Zod schema\n */\nfunction fieldToZod(fieldConfig: FieldConfig): z.ZodType {\n let schema: z.ZodType\n\n switch (fieldConfig.type) {\n case 'id':\n schema = z.string().uuid()\n break\n\n case 'text': {\n let s = z.string()\n if (fieldConfig.maxLength) s = s.max(fieldConfig.maxLength)\n if (fieldConfig.minLength) s = s.min(fieldConfig.minLength)\n if (fieldConfig.pattern) s = s.regex(fieldConfig.pattern)\n schema = s\n break\n }\n\n case 'number': {\n let n = z.number()\n if (fieldConfig.integer) n = n.int()\n if (fieldConfig.min !== undefined) n = n.min(fieldConfig.min)\n if (fieldConfig.max !== undefined) n = n.max(fieldConfig.max)\n schema = n\n break\n }\n\n case 'boolean':\n schema = z.boolean()\n break\n\n case 'date':\n schema = z.date().or(z.string().datetime())\n // Allow either Date object or ISO string, coerce to Date\n break\n\n case 'select':\n schema = z.enum(fieldConfig.options as [string, ...string[]])\n break\n\n case 'reference':\n if (fieldConfig.cardinality === 'many') {\n schema = z.array(z.string().uuid())\n } else {\n schema = z.string().uuid()\n }\n break\n\n case 'media':\n schema = z.string().uuid() // Media entity ID\n break\n\n case 'richtext':\n // HTML string (TipTap/Puck) or Slate JSON array (Plate/entity forms)\n schema = z.union([z.string(), z.array(z.record(z.unknown()))])\n break\n\n case 'slug':\n schema = z.string().regex(/^[a-z0-9-]+$/)\n break\n\n case 'json':\n // JSON fields accept any JSON-compatible value (objects, arrays, primitives).\n // Mirrors the JsonValue type in types/infer.ts and what Postgres jsonb stores.\n schema = z.union([\n z.record(z.unknown()),\n z.array(z.unknown()),\n z.string(),\n z.number(),\n z.boolean(),\n ])\n break\n\n case 'blocks':\n // Array of block instances with _block discriminator.\n // .passthrough() is required because each block type has different dynamic props\n // (title, image, content, etc.) that can't be validated generically here.\n // Per-block-type validation happens in the block editor's converter layer.\n // Security: extra props are harmless at storage level since rendering maps to\n // known component definitions and ignores unknown fields.\n schema = z.array(\n z\n .object({\n _block: z.string(),\n })\n .passthrough(),\n )\n break\n\n default:\n schema = z.unknown()\n }\n\n // Apply required/optional — non-required fields accept both undefined and null\n if (!fieldConfig.required) {\n schema = schema.nullable().optional()\n }\n\n // Apply default value\n if (fieldConfig.default !== undefined) {\n schema = schema.default(fieldConfig.default)\n }\n\n return schema\n}\n\n/**\n * Options accepted by the create/update schema generators.\n *\n * `includeInternal` controls whether fields marked `internal: true` are part\n * of the input schema. Default: `false` — public-surface schemas (used by\n * `AdminClient.create()` / `update()`) reject caller writes to internal\n * fields. The `updateInternal()` escape hatch sets it to `true` so trusted\n * server code can transition state-machine fields directly.\n */\nexport interface SchemaOptions {\n includeInternal?: boolean\n}\n\nfunction isOmittedFromUpdate(fieldName: string): boolean {\n // Immutable + audit fields are managed by hooks/server, not callers.\n return (\n fieldName === 'id' ||\n fieldName === 'createdAt' ||\n fieldName === 'createdBy' ||\n fieldName === 'updatedAt' ||\n fieldName === 'updatedBy' ||\n fieldName === '_version'\n )\n}\n\nfunction isOmittedFromCreate(fieldName: string): boolean {\n // Auto-generated + audit fields. `updatedBy` allowed at create-time\n // (unlike updates) because some flows seed it from the creator.\n return (\n fieldName === 'id' ||\n fieldName === 'createdAt' ||\n fieldName === 'updatedAt' ||\n fieldName === 'createdBy' ||\n fieldName === '_version'\n )\n}\n\n/**\n * Generate complete validation schema for an entity\n */\nexport function generateValidationSchema(entity: Entity): z.AnyZodObject {\n const shape: Record<string, z.ZodType> = {}\n\n for (const [fieldName, fieldConfig] of Object.entries(entity.allFields)) {\n shape[fieldName] = fieldToZod(fieldConfig)\n }\n\n return z.object(shape)\n}\n\n/**\n * Generate schema for entity creation. Internal fields are omitted unless\n * `includeInternal: true` is passed (see `SchemaOptions`). The `id` /\n * audit-trail fields are always omitted.\n */\nexport function generateCreateSchema(\n entity: Entity,\n options: SchemaOptions = {},\n): z.AnyZodObject {\n const shape: Record<string, z.ZodType> = {}\n\n for (const [fieldName, fieldConfig] of Object.entries(entity.allFields)) {\n if (isOmittedFromCreate(fieldName)) continue\n if (fieldConfig.internal && !options.includeInternal) continue\n shape[fieldName] = fieldToZod(fieldConfig)\n }\n\n return z.object(shape)\n}\n\n/**\n * Generate schema for entity updates (all fields optional). Internal fields\n * are omitted unless `includeInternal: true` is passed (see `SchemaOptions`).\n * The `id` / audit-trail fields are always omitted.\n */\nexport function generateUpdateSchema(\n entity: Entity,\n options: SchemaOptions = {},\n): z.AnyZodObject {\n const shape: Record<string, z.ZodType> = {}\n\n for (const [fieldName, fieldConfig] of Object.entries(entity.allFields)) {\n if (isOmittedFromUpdate(fieldName)) continue\n if (fieldConfig.internal && !options.includeInternal) continue\n shape[fieldName] = fieldToZod(fieldConfig).optional()\n }\n\n return z.object(shape)\n}\n","/**\n * AdminClient - Full CRUD with server-only enforcement\n * CRITICAL: This file MUST NOT be imported in client code\n */\n\n// NOTE: We use runtime check instead of 'server-only' import to allow CLI scripts\n// The 'server-only' package blocks ALL non-Next.js contexts (including Node.js scripts)\n// Layer 3: Runtime check (catches what bundlers miss, allows CLI usage)\n\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { and, eq, inArray, isNull, type SQL, sql } from 'drizzle-orm'\nimport type { PgTableWithColumns } from 'drizzle-orm/pg-core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { ZodType } from 'zod'\nimport type { BehaviorContext } from '../behaviors/types.js'\nimport type { CountCacheLike } from '../count-cache.js'\nimport type { CursorInput } from '../cursor.js'\nimport { buildCursorCondition } from '../cursor.js'\nimport type { Entity } from '../define-entity.js'\nimport { shapeDto, shapeDtos } from '../dto-shaper.js'\nimport type { FieldConfig } from '../fields/base.js'\nimport { ReferencedEntityError } from '../refs/errors.js'\nimport { extractRefs } from '../refs/extract-refs.js'\nimport { entityRefs } from '../refs/schema.js'\nimport {\n assertEntityAccess,\n attachBlocks,\n buildCountCacheKey,\n buildScopeCondition,\n type ContextResolver,\n type EntityContext,\n getAllFields,\n getBlocksFields,\n loadBlocks,\n mergeTranslations,\n resolveAuthContext,\n} from '../shared/entity-data-ops.js'\nimport type { InferCreateInput, InferEntityDTO, InferUpdateInput } from '../types/infer.js'\nimport type { Logger } from '../types/logger.js'\nimport { generateCreateSchema, generateUpdateSchema } from '../validation.js'\n\n/**\n * Resolves the registry of all entities in the running app — used by cascade\n * delete to look up `onDelete` strategy on referencing fields. Injected as a\n * function so the entity package never has to import `@murumets-ee/core`\n * (which would create a circular dependency and force runtime tricks that\n * break under bundlers like Turbopack).\n */\nexport type EntityResolver = () => Map<string, Entity> | undefined\n\nexport interface AdminClientConfig<\n AllFields extends Record<string, FieldConfig> = Record<string, FieldConfig>,\n> {\n entity: Entity<AllFields>\n db: PostgresJsDatabase\n logger?: Logger\n /** Optional count cache for COUNT(*) query optimization. */\n countCache?: CountCacheLike\n /**\n * Resolves the current request's security context (user, role checker, scope).\n * Provided automatically by `createAdminClient()` from @murumets-ee/core/clients.\n * For direct `new AdminClient()` usage, pass your own resolver or use `runAsCli()`.\n */\n contextResolver?: ContextResolver\n /**\n * Resolves the running app's entity registry. Used by cascade delete to\n * read each referencing field's `onDelete` strategy. When omitted, all\n * incoming references are treated as `restrict` — a safe default that\n * blocks deletes rather than guessing.\n */\n entityResolver?: EntityResolver\n}\n\nexport interface FindManyOptions {\n where?: SQL | undefined\n limit?: number\n offset?: number\n orderBy?: SQL | SQL[]\n locale?: string\n /** Default content locale. For localized blocks, NULL rows (from initial create)\n * are only returned as fallback when locale matches defaultLocale. */\n defaultLocale?: string\n /**\n * Cursor-based (keyset) pagination. When provided, replaces OFFSET with a\n * WHERE condition for O(1) page access at any depth. The `offset` option\n * is ignored when `cursor` is set.\n *\n * The cursor `field` must be a real column on the entity table.\n */\n cursor?: CursorInput\n /**\n * Include fields marked `internal: true` (and legacy `_`-prefixed\n * infrastructure columns) in the returned DTOs. Use only on trusted\n * server-side reads that need to inspect state-machine fields.\n */\n includeInternal?: boolean\n}\n\nexport interface CountOptions {\n where?: SQL | undefined\n}\n\n/**\n * AdminClient - Full CRUD operations with data integrity + security enforcement\n *\n * **Requires RequestContext.** All operations check permissions and scope via\n * the context established by `runWithContextAsync()`. CLI scripts use `runAsCli()`.\n *\n * Security & integrity layers:\n * 1. Runtime check: `typeof window !== 'undefined'` → throw (prevents browser usage)\n * 2. Permission enforcement: `assertEntityAccess()` checks `checker(role, entity, action)` from context\n * 3. Scope filtering: scoped entities auto-filter by `_scopeId` from context\n * 4. Zod validation: every write (create, update, updateMany) validated before DB\n * 5. Column whitelist: `prepareDataForInsert/Update` drops unknown fields\n * 6. Hook execution: `beforeCreate`, `afterCreate`, etc. from entity behaviors\n * 7. Reference tracking: `entity_refs` table for delete protection\n *\n * Note: We don't use 'server-only' import because it blocks CLI scripts.\n * Next.js bundler protection comes from subpath exports (@murumets-ee/core/clients).\n *\n * @typeParam AllFields - The entity's complete field map. Inferred automatically\n * when constructing via `createAdminClient(entity)`.\n */\nexport class AdminClient<\n AllFields extends Record<string, FieldConfig> = Record<string, FieldConfig>,\n> {\n private entity: Entity<AllFields>\n private db: PostgresJsDatabase\n private logger?: Logger\n /** Public-surface create schema — internal fields excluded. */\n private createSchema: ZodType\n /** Public-surface update schema — internal fields excluded. */\n private updateSchema: ZodType\n /** Trusted-surface update schema — internal fields included. Used by `updateInternal()`. */\n private updateInternalSchema: ZodType\n /** Names of fields marked `internal: true`. Cached for O(1) strip / pick. */\n private internalFieldNames: ReadonlySet<string>\n // biome-ignore lint/suspicious/noExplicitAny: dynamic table columns require PgTableWithColumns<any> for property access\n private table: PgTableWithColumns<any>\n private countCache?: CountCacheLike\n private contextResolver?: ContextResolver\n private entityResolver?: EntityResolver\n\n /** Shared context for entity-data-ops functions. */\n private get ctx(): EntityContext {\n return {\n entity: this.entity,\n db: this.db,\n table: this.table,\n resolveContext: this.contextResolver,\n }\n }\n\n constructor(config: AdminClientConfig<AllFields>) {\n // Runtime enforcement: prevent usage in browser contexts\n if (typeof window !== 'undefined') {\n throw new Error(\n 'AdminClient cannot be used in browser code. ' +\n 'Use QueryClient for frontend data access.',\n )\n }\n\n this.entity = config.entity\n this.db = config.db\n this.logger = config.logger\n this.countCache = config.countCache\n this.contextResolver = config.contextResolver\n this.entityResolver = config.entityResolver\n\n // Pre-generate validation schemas for performance.\n // Public schemas exclude `internal: true` fields so the public surface\n // (HTTP PATCH, programmatic update()) cannot poison state-machine fields\n // like `_workflowStatus`. Trusted code uses `updateInternal()` which\n // routes through the internal-inclusive schema.\n this.createSchema = generateCreateSchema(config.entity)\n this.updateSchema = generateUpdateSchema(config.entity)\n this.updateInternalSchema = generateUpdateSchema(config.entity, { includeInternal: true })\n\n this.internalFieldNames = new Set(\n Object.entries(config.entity.allFields)\n .filter(([, fieldConfig]) => fieldConfig.internal)\n .map(([name]) => name),\n )\n\n // Get table from schema registry\n const table = schemaRegistry.get(config.entity.name)\n if (!table) {\n throw new Error(\n `Schema for entity '${config.entity.name}' not found in registry. ` +\n 'Ensure schemas are generated and registered before creating AdminClient.',\n )\n }\n this.table = table\n }\n\n /**\n * Strip caller-provided values for internal fields from the input.\n *\n * Returns a NEW object — does not mutate the caller's input. Hooks run\n * AFTER this strip, so they can still set internal fields legitimately;\n * those values are then preserved through validation by `pickInternalFields`.\n */\n private stripCallerInternals(data: Record<string, unknown>): Record<string, unknown> {\n if (this.internalFieldNames.size === 0) return data\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(data)) {\n if (this.internalFieldNames.has(key)) continue\n out[key] = value\n }\n return out\n }\n\n /**\n * Pick the internal-field values that hooks set on `data`.\n * Used to re-attach them after schema validation strips unknown keys.\n */\n private pickInternalFields(data: Record<string, unknown>): Record<string, unknown> {\n if (this.internalFieldNames.size === 0) return {}\n const out: Record<string, unknown> = {}\n for (const key of this.internalFieldNames) {\n if (data[key] !== undefined) out[key] = data[key]\n }\n return out\n }\n\n /**\n * Create a new entity\n *\n * Flow:\n * 1. Validate input with Zod\n * 2. Execute beforeCreate hooks\n * 3. Prepare data for insert\n * 4. Insert into database\n * 5. Execute afterCreate hooks\n * 6. Shape DTO and return\n */\n async create(data: InferCreateInput<AllFields>): Promise<InferEntityDTO<AllFields>> {\n this.logger?.info({ entity: this.entity.name }, 'Creating entity')\n\n // Permission + scope + behavior context (one resolver call)\n const { scopeId, behaviorCtx } = await resolveAuthContext(\n this.entity,\n this.contextResolver,\n 'create',\n { strictScope: true },\n )\n\n // Strip caller-provided internal fields BEFORE hooks. Hooks may then set\n // them legitimately (e.g. workflowable's beforeCreate sets `_workflowStatus`\n // = 'draft'); those values are picked back up after validation.\n let dataToInsert = this.stripCallerInternals(data as Record<string, unknown>)\n\n // Execute beforeCreate hooks BEFORE validation — hooks may populate required fields\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeCreate) {\n const hookResult = await b.hooks.beforeCreate(dataToInsert, behaviorCtx)\n if (hookResult !== undefined) dataToInsert = hookResult\n }\n }\n\n // Preserve server-set fields that hooks added but createSchema omits\n // (e.g. createdAt/updatedAt from timestamped(), createdBy/updatedBy from auditable())\n const serverFields: Record<string, unknown> = {}\n for (const key of ['createdAt', 'updatedAt', 'createdBy', 'updatedBy']) {\n if (dataToInsert[key] !== undefined) serverFields[key] = dataToInsert[key]\n }\n // Preserve internal fields that hooks set (validation schema omits them).\n const internalFields = this.pickInternalFields(dataToInsert)\n\n // Validate after hooks (hooks may add required fields like senderEmail)\n dataToInsert = {\n ...this.createSchema.parse(dataToInsert),\n ...serverFields,\n ...internalFields,\n }\n\n // Auto-set scope ID for scoped entities\n if (scopeId) {\n dataToInsert._scopeId = scopeId\n }\n\n // Prepare data for insert\n const columns = this.prepareDataForInsert(dataToInsert)\n\n // Insert into database\n const [row] = await this.db.insert(this.table).values(columns).returning()\n\n // Save blocks to layout table\n await this.saveBlocks(row.id, dataToInsert)\n\n // Track outgoing references (entity_refs)\n await this.syncRefs(row.id as string, dataToInsert, 'create')\n\n // Execute afterCreate hooks\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.afterCreate) await b.hooks.afterCreate(row, behaviorCtx)\n }\n\n // Invalidate count cache after creation\n this.invalidateCountCache()\n\n // Shape DTO and attach blocks\n const shaped = shapeDto(this.entity, row) as Record<string, unknown>\n if (shaped && getBlocksFields(this.ctx).length > 0) {\n const blocksMap = await loadBlocks(this.ctx, [shaped.id as string], undefined, {\n strictTranslations: true,\n })\n attachBlocks(this.ctx, [shaped], blocksMap)\n }\n return shaped as InferEntityDTO<AllFields>\n }\n\n /**\n * Find entity by ID.\n *\n * Pass `includeInternal: true` from trusted server code (workflow\n * transitions, behavior implementations) when you need to read fields\n * marked `internal: true` — by default they are stripped from the DTO.\n */\n async findById(\n id: string,\n options?: { locale?: string; defaultLocale?: string; includeInternal?: boolean },\n ): Promise<InferEntityDTO<AllFields> | null> {\n this.logger?.info({ entity: this.entity.name, id }, 'Finding entity by ID')\n\n // Permission + scope (one resolver call; reads don't need strict scope)\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'view')\n\n // Query database (with scope filter if applicable)\n const conditions = [eq(this.table.id, id)]\n if (scopeId) conditions.push(buildScopeCondition(this.ctx, scopeId))\n const [row] = await this.db\n .select()\n .from(this.table)\n .where(and(...conditions))\n\n if (!row) return null\n\n const shapeOpts = options?.includeInternal ? { includeInternal: true } : undefined\n const shaped = shapeDto(this.entity, row, shapeOpts) as Record<string, unknown>\n if (!shaped) return null\n\n // Attach blocks from layout table (with locale for block translations)\n if (getBlocksFields(this.ctx).length > 0) {\n const blocksMap = await loadBlocks(this.ctx, [shaped.id as string], options?.locale, {\n defaultLocale: options?.defaultLocale,\n strictTranslations: true,\n })\n attachBlocks(this.ctx, [shaped], blocksMap)\n }\n\n if (options?.locale) {\n const [merged] = await mergeTranslations(this.ctx, [shaped], options.locale)\n return merged as InferEntityDTO<AllFields>\n }\n return shaped as InferEntityDTO<AllFields>\n }\n\n /**\n * Find multiple entities\n */\n async findMany(options?: FindManyOptions): Promise<InferEntityDTO<AllFields>[]> {\n this.logger?.info({ entity: this.entity.name, options }, 'Finding entities')\n\n // Permission + scope (one resolver call; reads don't need strict scope)\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'view')\n\n // Build and execute query\n let query = this.db.select().from(this.table).$dynamic()\n\n // Collect WHERE conditions\n const conditions: SQL[] = []\n\n // Scope filter\n if (scopeId) conditions.push(buildScopeCondition(this.ctx, scopeId))\n\n if (options?.where) {\n conditions.push(options.where)\n }\n\n // Cursor-based pagination: add keyset WHERE condition (replaces OFFSET)\n if (options?.cursor) {\n // Whitelist: cursor field must be a real column on the entity\n const allFields = getAllFields(this.ctx)\n if (!(options.cursor.field in allFields) && options.cursor.field !== 'id') {\n throw new Error(\n `Invalid cursor field: '${options.cursor.field}' is not a field on '${this.entity.name}'`,\n )\n }\n const cursorCondition = buildCursorCondition(this.table, options.cursor)\n if (cursorCondition) {\n conditions.push(cursorCondition)\n }\n }\n\n if (conditions.length > 0) {\n query = query.where(and(...conditions))\n }\n\n if (options?.limit) {\n query = query.limit(options.limit)\n }\n // Cursor takes precedence over offset — skip offset when cursor is active\n if (options?.offset && !options?.cursor) {\n query = query.offset(options.offset)\n }\n if (options?.orderBy) {\n const cols = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]\n query = query.orderBy(...cols)\n }\n\n const rows = await query\n\n const shapeOpts = options?.includeInternal ? { includeInternal: true } : undefined\n const shaped = shapeDtos(this.entity, rows, shapeOpts).filter(\n (e): e is NonNullable<typeof e> => e !== null,\n ) as Record<string, unknown>[]\n\n // Attach blocks from layout table (with locale for block translations)\n if (getBlocksFields(this.ctx).length > 0 && shaped.length > 0) {\n const entityIds = shaped.map((e) => e.id as string)\n const blocksMap = await loadBlocks(this.ctx, entityIds, options?.locale, {\n defaultLocale: options?.defaultLocale,\n strictTranslations: true,\n })\n attachBlocks(this.ctx, shaped, blocksMap)\n }\n\n if (options?.locale) {\n return (await mergeTranslations(\n this.ctx,\n shaped,\n options.locale,\n )) as InferEntityDTO<AllFields>[]\n }\n\n return shaped as InferEntityDTO<AllFields>[]\n }\n\n /**\n * Count entities matching optional conditions\n */\n async count(options?: CountOptions): Promise<number> {\n this.logger?.info({ entity: this.entity.name, options }, 'Counting entities')\n\n // Permission + scope (one resolver call; reads don't need strict scope)\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'view')\n\n // Build cache key (includes scopeId for per-tenant isolation)\n const cacheKey = buildCountCacheKey(this.entity.name, options?.where, undefined, scopeId)\n\n const compute = async (): Promise<number> => {\n let query = this.db.select({ count: sql<number>`count(*)` }).from(this.table).$dynamic()\n const conditions: SQL[] = []\n if (scopeId) conditions.push(buildScopeCondition(this.ctx, scopeId))\n if (options?.where) conditions.push(options.where)\n if (conditions.length > 0) query = query.where(and(...conditions))\n const [result] = await query\n return Number(result.count)\n }\n\n // No cache configured → run the query directly (back-compat).\n if (!this.countCache) return compute()\n\n // Single-flight: concurrent callers crossing the TTL gap share one query.\n return this.countCache.getOrCompute(cacheKey, compute)\n }\n\n /**\n * Update entity by ID\n *\n * Flow:\n * 1. Validate input with Zod\n * 2. Execute beforeUpdate hooks\n * 3. Prepare data for update\n * 4. Update in database\n * 5. Execute afterUpdate hooks\n * 6. Shape DTO and return\n */\n async update(\n id: string,\n data: InferUpdateInput<AllFields>,\n options?: { locale?: string },\n ): Promise<InferEntityDTO<AllFields>> {\n return this.updateImpl(id, data as Record<string, unknown>, options, {\n allowInternal: false,\n })\n }\n\n /**\n * Update entity by ID, allowing writes to fields marked `internal: true`.\n *\n * Use this from trusted server code that has already authorized the\n * transition out-of-band — typical example: workflow transitions invoked\n * from an admin route that has already checked the `publish` permission.\n * The HTTP `PATCH` surface uses the public {@link update} method, which\n * silently strips internal fields so untrusted callers cannot poison\n * state-machine values like `_workflowStatus`.\n *\n * The validation schema for this method INCLUDES internal fields — values\n * are still type-checked and constrained (e.g. select-field options).\n *\n * **Security**: never call this from a code path that forwards request body\n * fields directly. The caller must construct the internal-field values\n * server-side from authorized state transitions.\n */\n async updateInternal(\n id: string,\n data: Record<string, unknown>,\n options?: { locale?: string },\n ): Promise<InferEntityDTO<AllFields>> {\n return this.updateImpl(id, data, options, { allowInternal: true })\n }\n\n private async updateImpl(\n id: string,\n data: Record<string, unknown>,\n options: { locale?: string } | undefined,\n flags: { allowInternal: boolean },\n ): Promise<InferEntityDTO<AllFields>> {\n this.logger?.info(\n { entity: this.entity.name, id, internal: flags.allowInternal || undefined },\n 'Updating entity',\n )\n\n // Permission + scope + behavior context (one resolver call; strict scope for writes)\n const { scopeId, behaviorCtx } = await resolveAuthContext(\n this.entity,\n this.contextResolver,\n 'update',\n { strictScope: true },\n )\n\n // Strip caller-provided internal fields on the public path. Trusted\n // callers using updateInternal() bypass the strip — they're presumed\n // to have authorized the internal transition out-of-band.\n let dataToUpdate = flags.allowInternal ? data : this.stripCallerInternals(data)\n\n // Lazy, memoized loader for the *current* entity row. Hooks that need\n // to validate transitions against the existing state (e.g. workflowable)\n // call this; hooks that don't, don't pay for the round trip. Memoized\n // so multiple hooks share one fetch within the same update.\n let cachedCurrent: Record<string, unknown> | null | undefined\n const loadCurrent = async (): Promise<Record<string, unknown> | null> => {\n if (cachedCurrent === undefined) {\n cachedCurrent = (await this.findById(id)) as Record<string, unknown> | null\n }\n return cachedCurrent\n }\n const hookCtx = {\n ...behaviorCtx,\n loadCurrent,\n viaInternal: flags.allowInternal,\n }\n\n // Execute beforeUpdate hooks BEFORE validation — hooks may modify fields\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeUpdate) {\n const hookResult = await b.hooks.beforeUpdate(id, dataToUpdate, hookCtx)\n if (hookResult !== undefined) dataToUpdate = hookResult\n }\n }\n\n // Preserve server-set fields that hooks added but updateSchema omits\n // (e.g. updatedAt from timestamped(), updatedBy from auditable()).\n const serverFields: Record<string, unknown> = {}\n for (const key of ['updatedAt', 'updatedBy']) {\n if (dataToUpdate[key] !== undefined) serverFields[key] = dataToUpdate[key]\n }\n\n // Validate. The internal-inclusive schema is used when the caller is\n // trusted (updateInternal); the public schema is used otherwise — its\n // strip behavior also drops any caller-internals that survived (defense\n // in depth alongside `stripCallerInternals`).\n const schema = flags.allowInternal ? this.updateInternalSchema : this.updateSchema\n const internalFields = flags.allowInternal ? {} : this.pickInternalFields(dataToUpdate)\n dataToUpdate = { ...schema.parse(dataToUpdate), ...serverFields, ...internalFields }\n\n // Prepare data for update\n const columns = this.prepareDataForUpdate(dataToUpdate)\n\n // Build WHERE with scope filter\n const idCondition = eq(this.table.id, id)\n const updateWhere = scopeId\n ? (and(idCondition, buildScopeCondition(this.ctx, scopeId)) ?? idCondition)\n : idCondition\n\n // Update in database (only if there are non-blocks fields to update)\n let row: Record<string, unknown>\n if (Object.keys(columns).length > 0) {\n const [updated] = await this.db.update(this.table).set(columns).where(updateWhere).returning()\n row = updated\n } else {\n // Only blocks changed — fetch current row\n const [current] = await this.db.select().from(this.table).where(updateWhere)\n row = current\n }\n\n // Update blocks in layout table (thread locale for per-locale blocks)\n await this.saveBlocks(id, dataToUpdate, options)\n\n // Update outgoing references (entity_refs) — only for changed fields\n await this.syncRefs(id, dataToUpdate, 'update')\n\n // Execute afterUpdate hooks\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.afterUpdate) await b.hooks.afterUpdate(row, behaviorCtx)\n }\n\n // Shape DTO and attach blocks. `updateInternal()` returns internal fields\n // alongside public ones — the caller is trusted, just wrote them, and would\n // otherwise need an extra findById to inspect the result.\n const shapeOpts = flags.allowInternal ? { includeInternal: true } : undefined\n const shaped = shapeDto(this.entity, row, shapeOpts) as Record<string, unknown>\n if (shaped && getBlocksFields(this.ctx).length > 0) {\n const blocksMap = await loadBlocks(this.ctx, [id], undefined, { strictTranslations: true })\n attachBlocks(this.ctx, [shaped], blocksMap)\n }\n return shaped as InferEntityDTO<AllFields>\n }\n\n /**\n * Delete entity by ID.\n *\n * Wraps cascade + delete in a transaction so cascaded deletes are rolled\n * back if the final entity delete fails (no partial data loss).\n *\n * Checks entity_refs for incoming references first — throws\n * ReferencedEntityError if this entity is still used somewhere.\n */\n async delete(id: string): Promise<void> {\n this.logger?.info({ entity: this.entity.name, id }, 'Deleting entity')\n\n // Permission + scope + behavior context (one resolver call; strict scope for writes)\n const { scopeId, behaviorCtx } = await resolveAuthContext(\n this.entity,\n this.contextResolver,\n 'delete',\n { strictScope: true },\n )\n\n // Verify entity is in current scope before deleting\n if (scopeId) {\n const [row] = await this.db\n .select({ id: this.table.id })\n .from(this.table)\n .where(and(eq(this.table.id, id), buildScopeCondition(this.ctx, scopeId)))\n if (!row) return // Not found in this scope — no-op\n }\n\n await this.db.transaction(async (tx) => {\n // Handle incoming references — cascade or restrict based on field config\n await this.handleIncomingRefsForDelete(tx, [id])\n\n // Execute beforeDelete hooks\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeDelete) await b.hooks.beforeDelete(id, behaviorCtx)\n }\n\n // Delete from database\n await tx.delete(this.table).where(eq(this.table.id, id))\n\n // Clean up outgoing references from entity_refs\n await tx\n .delete(entityRefs)\n .where(and(eq(entityRefs.sourceEntity, this.entity.name), eq(entityRefs.sourceId, id)))\n })\n\n // Execute afterDelete hooks (outside transaction — side effects should not block commit)\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.afterDelete) await b.hooks.afterDelete(id, behaviorCtx)\n }\n\n // Invalidate count cache after deletion\n this.invalidateCountCache()\n }\n\n /**\n * Delete multiple entities matching a WHERE condition.\n *\n * Wraps cascade + delete in a transaction so cascaded deletes are rolled\n * back if the final entity delete fails (no partial data loss).\n *\n * Respects `onDelete` config on referencing fields:\n * - `cascade`: recursively deletes referencing entities\n * - `set-null`: nullifies the reference column\n * - `restrict` (default): blocks deletion if references exist\n *\n * Runs beforeDelete/afterDelete hooks for each entity.\n *\n * @returns Number of rows deleted\n */\n async deleteMany(where: SQL): Promise<number> {\n this.logger?.info({ entity: this.entity.name }, 'Bulk deleting entities')\n\n // Permission + scope + behavior context (one resolver call; strict scope for writes)\n const { scopeId, behaviorCtx } = await resolveAuthContext(\n this.entity,\n this.contextResolver,\n 'delete',\n { strictScope: true },\n )\n\n // Get IDs first ��� needed for ref handling and hooks (scoped to current tenant)\n const deleteConditions = [where]\n if (scopeId) deleteConditions.push(buildScopeCondition(this.ctx, scopeId))\n const rows = await this.db\n .select({ id: this.table.id })\n .from(this.table)\n .where(and(...deleteConditions))\n const ids = rows.map((r) => (r as Record<string, unknown>).id as string)\n if (ids.length === 0) return 0\n\n await this.db.transaction(async (tx) => {\n // Handle incoming references — cascade or restrict\n await this.handleIncomingRefsForDelete(tx, ids)\n\n // Run beforeDelete hooks\n for (const id of ids) {\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeDelete) await b.hooks.beforeDelete(id, behaviorCtx)\n }\n }\n\n // Delete from database\n await tx.delete(this.table).where(inArray(this.table.id, ids))\n\n // Clean up outgoing references from entity_refs\n await tx\n .delete(entityRefs)\n .where(\n and(eq(entityRefs.sourceEntity, this.entity.name), inArray(entityRefs.sourceId, ids)),\n )\n })\n\n // Run afterDelete hooks (outside transaction — side effects should not block commit)\n for (const id of ids) {\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.afterDelete) await b.hooks.afterDelete(id, behaviorCtx)\n }\n }\n\n this.invalidateCountCache()\n return ids.length\n }\n\n /** Maximum cascade depth to prevent infinite recursion from circular references. */\n private static readonly MAX_CASCADE_DEPTH = 10\n\n /**\n * Handle incoming references before deleting entities.\n *\n * For each referencing entity/field:\n * - `onDelete: 'cascade'` → recursively delete referencing entities\n * - `onDelete: 'set-null'` → nullify the FK column on referencing rows\n * - `onDelete: 'restrict'` → throw ReferencedEntityError\n *\n * Looks up referencing entity definitions from the app's entity registry\n * to determine the onDelete strategy. Falls back to 'restrict' if the\n * entity registry is unavailable.\n *\n * @param tx - Transaction handle for atomicity (cascade + delete in same tx)\n * @param ids - Entity IDs being deleted\n * @param depth - Current recursion depth (guards against circular references)\n */\n private async handleIncomingRefsForDelete(\n tx: PostgresJsDatabase,\n ids: string[],\n depth = 0,\n ): Promise<void> {\n if (depth >= AdminClient.MAX_CASCADE_DEPTH) {\n throw new Error(\n `Cascade delete exceeded maximum depth of ${AdminClient.MAX_CASCADE_DEPTH} ` +\n `while deleting '${this.entity.name}'. This likely indicates a circular reference chain.`,\n )\n }\n\n // Find all incoming references for all IDs in a single query\n const usages = await tx\n .select({\n sourceEntity: entityRefs.sourceEntity,\n sourceId: entityRefs.sourceId,\n sourceField: entityRefs.sourceField,\n })\n .from(entityRefs)\n .where(and(eq(entityRefs.targetEntity, this.entity.name), inArray(entityRefs.targetId, ids)))\n\n if (usages.length === 0) return\n\n // Group usages by source entity + field to determine strategy per group\n const groups = new Map<\n string,\n { sourceEntity: string; sourceField: string; sourceIds: string[] }\n >()\n for (const u of usages) {\n const key = `${u.sourceEntity}:${u.sourceField}`\n let group = groups.get(key)\n if (!group) {\n group = { sourceEntity: u.sourceEntity, sourceField: u.sourceField, sourceIds: [] }\n groups.set(key, group)\n }\n group.sourceIds.push(u.sourceId)\n }\n\n // Look up entity registry for onDelete config. When no resolver is\n // configured, all incoming refs are treated as `restrict`.\n const entityMap = this.entityResolver?.()\n\n for (const [, group] of groups) {\n const refEntity = entityMap?.get(group.sourceEntity)\n const refField = refEntity?.fields[group.sourceField] as\n | { type: string; onDelete?: string }\n | undefined\n const strategy = refField?.onDelete ?? 'restrict'\n\n if (strategy === 'restrict') {\n throw new ReferencedEntityError(\n this.entity.name,\n ids[0],\n group.sourceIds.map((sid) => ({\n sourceEntity: group.sourceEntity,\n sourceId: sid,\n sourceField: group.sourceField,\n })),\n )\n }\n\n if (strategy === 'cascade') {\n // Recursively delete referencing entities via their own AdminClient.\n // Propagates contextResolver so permission + scope checks apply to the\n // referenced entity too — deleting an Article shouldn't silently delete\n // Comments the caller has no permission for, nor cross-tenant comments.\n const sourceTable = schemaRegistry.get(group.sourceEntity)\n if (sourceTable && refEntity) {\n const sourceClient = new AdminClient({\n entity: refEntity,\n db: tx,\n logger: this.logger,\n contextResolver: this.contextResolver,\n })\n // Enforce 'delete' permission on the referenced entity before recursing\n // (one resolver call covers permission + scope + behavior context)\n const refAuth = await resolveAuthContext(refEntity, this.contextResolver, 'delete')\n let sourceWhere: SQL = inArray(sourceTable.id, group.sourceIds)\n if (refAuth.scopeId) {\n const scopedCtx: EntityContext = {\n entity: refEntity,\n db: tx,\n table: sourceTable,\n resolveContext: this.contextResolver,\n }\n const scoped = and(sourceWhere, buildScopeCondition(scopedCtx, refAuth.scopeId))\n if (scoped) sourceWhere = scoped\n }\n await sourceClient.deleteManyInTx(tx, sourceWhere, depth + 1, refAuth.behaviorCtx)\n }\n } else if (strategy === 'set-null') {\n // Nullify the FK column on referencing rows.\n // Enforce 'update' permission + scope on the referenced entity — setting\n // a FK to null is a mutation the caller may not be authorized for.\n const sourceTable = schemaRegistry.get(group.sourceEntity)\n if (sourceTable && refEntity) {\n const col = sourceTable[group.sourceField]\n if (col) {\n const refAuth = await resolveAuthContext(refEntity, this.contextResolver, 'update')\n const sourceScopeId = refAuth.scopeId\n let updateWhere: SQL = inArray(sourceTable.id, group.sourceIds)\n if (sourceScopeId) {\n const scopedCtx: EntityContext = {\n entity: refEntity,\n db: tx,\n table: sourceTable,\n resolveContext: this.contextResolver,\n }\n const scoped = and(updateWhere, buildScopeCondition(scopedCtx, sourceScopeId))\n if (scoped) updateWhere = scoped\n }\n await tx\n .update(sourceTable)\n .set({ [group.sourceField]: null })\n .where(updateWhere)\n // Clean up the ref rows\n await tx\n .delete(entityRefs)\n .where(\n and(\n eq(entityRefs.sourceEntity, group.sourceEntity),\n inArray(entityRefs.sourceId, group.sourceIds),\n eq(entityRefs.sourceField, group.sourceField),\n ),\n )\n }\n }\n }\n }\n }\n\n /**\n * Internal: delete within an existing transaction (used by cascade).\n * Skips wrapping in a new transaction — the caller already has one.\n *\n * Receives `behaviorCtx` from the caller — auth was already enforced on\n * this entity by the cascade orchestrator, so no extra resolver call here.\n */\n private async deleteManyInTx(\n tx: PostgresJsDatabase,\n where: SQL,\n depth: number,\n behaviorCtx: BehaviorContext,\n ): Promise<number> {\n const rows = await tx.select({ id: this.table.id }).from(this.table).where(where)\n const ids = rows.map((r) => (r as Record<string, unknown>).id as string)\n if (ids.length === 0) return 0\n\n await this.handleIncomingRefsForDelete(tx, ids, depth)\n\n for (const id of ids) {\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeDelete) await b.hooks.beforeDelete(id, behaviorCtx)\n }\n }\n\n await tx.delete(this.table).where(inArray(this.table.id, ids))\n await tx\n .delete(entityRefs)\n .where(and(eq(entityRefs.sourceEntity, this.entity.name), inArray(entityRefs.sourceId, ids)))\n\n // afterDelete hooks run outside the caller's transaction scope\n // (the caller — deleteMany — handles them after tx commits)\n\n this.invalidateCountCache()\n return ids.length\n }\n\n // -------------------------------------------------------------------------\n // Bulk operations\n // -------------------------------------------------------------------------\n\n /**\n * Update multiple entities matching a WHERE condition.\n *\n * Unlike `update(id, data)`, this does NOT run hooks (beforeUpdate/afterUpdate),\n * does NOT validate with Zod, and does NOT update blocks/translations.\n * It's a direct bulk column update — use for operational changes like\n * status transitions, assignments, and cleanup jobs.\n *\n * @returns Number of rows updated\n *\n * @example Close all resolved tickets older than 30 days\n * ```ts\n * const closed = await ticketClient.updateMany(\n * and(eq(table.status, 'resolved'), lte(table.updatedAt, cutoff)),\n * { status: 'closed', updatedAt: new Date() },\n * )\n * ```\n *\n * @example Cascade materialized path updates with SQL expressions\n * ```ts\n * const t = taxonomyClient.getTable()\n * await taxonomyClient.updateMany(\n * like(t.path, `${oldPath}/%`),\n * {},\n * {\n * expressions: {\n * path: sql`replace(${t.path}, ${oldPath}, ${newPath})`,\n * depth: sql`length(replace(${t.path}, ${oldPath}, ${newPath}))\n * - length(replace(replace(${t.path}, ${oldPath}, ${newPath}), '/', ''))`,\n * },\n * },\n * )\n * ```\n */\n async updateMany(\n where: SQL,\n data: Partial<InferUpdateInput<AllFields>>,\n options?: { expressions?: Record<string, SQL>; allowInternal?: boolean },\n ): Promise<number> {\n this.logger?.info({ entity: this.entity.name }, 'Bulk updating entities')\n\n // Permission + scope (one resolver call; strict scope for writes)\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'update', {\n strictScope: true,\n })\n\n // Strip caller-provided internal fields on the public path. Trusted\n // callers using `allowInternal: true` bypass the strip — same contract\n // as `update()` vs `updateInternal()`, applied here at the bulk surface.\n const safeData = options?.allowInternal\n ? (data as Record<string, unknown>)\n : this.stripCallerInternals(data as Record<string, unknown>)\n\n // Validate data shape. Internal-inclusive schema only on the trusted path.\n const schema = options?.allowInternal ? this.updateInternalSchema : this.updateSchema\n const validated = schema.parse(safeData)\n const columns = this.prepareDataForUpdate(validated as Record<string, unknown>)\n\n // Merge SQL expressions — bypass validation (computed values, not user input).\n // Guard against silent clobbering: reject collisions, unknown columns, AND\n // (on the public path) any internal-flagged columns. Otherwise a caller could\n // poison a state-machine field via `expressions: { _workflowStatus: sql\\`'approved'\\` }`,\n // bypassing the same boundary the public `update()` installs.\n if (options?.expressions) {\n const allFields = getAllFields(this.ctx)\n for (const [key, expr] of Object.entries(options.expressions)) {\n if (key in columns) {\n throw new Error(\n `updateMany: field '${key}' appears in both \\`data\\` and \\`expressions\\`. ` +\n `Choose one — expressions silently overriding validated values is unsafe.`,\n )\n }\n // Allow real entity fields + the infrastructure _scopeId column.\n if (!(key in allFields) && key !== '_scopeId') {\n throw new Error(\n `updateMany.expressions: unknown column '${key}' on entity '${this.entity.name}'`,\n )\n }\n // Block internal fields unless trusted-caller opt-in.\n if (!options.allowInternal && this.internalFieldNames.has(key)) {\n throw new Error(\n `updateMany.expressions: '${key}' is internal — pass \\`allowInternal: true\\` ` +\n `from a trusted server context that has authorized the transition out-of-band.`,\n )\n }\n columns[key] = expr\n }\n }\n\n const updateConditions = [where]\n if (scopeId) updateConditions.push(buildScopeCondition(this.ctx, scopeId))\n const result = await this.db\n .update(this.table)\n .set(columns)\n .where(and(...updateConditions))\n .returning({ id: this.table.id })\n\n this.invalidateCountCache()\n return result.length\n }\n\n // -------------------------------------------------------------------------\n // Aggregation\n // -------------------------------------------------------------------------\n\n /**\n * Run an aggregate query on this entity's table.\n *\n * Supports GROUP BY with standard aggregate functions (count, sum, avg,\n * min, max). This is the entity-level equivalent of Drupal's\n * `EntityQueryAggregate` — standardized stats without raw SQL.\n *\n * @example Count tickets by status\n * ```ts\n * const stats = await ticketClient.aggregate({\n * select: { count: sql<number>`count(*)::int` },\n * groupBy: [table.status],\n * })\n * // → [{ status: 'open', count: 12 }, { status: 'closed', count: 45 }]\n * ```\n *\n * @example Dashboard stats with conditional counts\n * ```ts\n * const [stats] = await ticketClient.aggregate({\n * select: {\n * open: sql<number>`count(case when ${table.status} = ${'open'} then 1 end)::int`,\n * pending: sql<number>`count(case when ${table.status} = ${'pending'} then 1 end)::int`,\n * },\n * })\n * ```\n */\n async aggregate<TResult extends Record<string, unknown> = Record<string, unknown>>(options: {\n /** Named select expressions — each key becomes an output column. Use `sql<T>` for aggregates. */\n select: Record<string, SQL | ReturnType<typeof eq>>\n /** Columns to GROUP BY. */\n groupBy?: SQL[]\n /** WHERE filter applied before aggregation. */\n where?: SQL\n /** ORDER BY for the result. */\n orderBy?: SQL | SQL[]\n /** Limit the number of rows returned. */\n limit?: number\n }): Promise<TResult[]> {\n this.logger?.info({ entity: this.entity.name }, 'Aggregate query')\n\n // Permission + scope (one resolver call; reads don't need strict scope)\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'view')\n\n let query = this.db.select(options.select).from(this.table).$dynamic()\n\n const conditions: SQL[] = []\n if (scopeId) conditions.push(buildScopeCondition(this.ctx, scopeId))\n if (options.where) conditions.push(options.where)\n if (conditions.length > 0) query = query.where(and(...conditions))\n if (options.groupBy && options.groupBy.length > 0) {\n query = query.groupBy(...options.groupBy)\n }\n if (options.orderBy) {\n const cols = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]\n query = query.orderBy(...cols)\n }\n if (options.limit) {\n query = query.limit(options.limit)\n }\n\n return (await query) as TResult[]\n }\n\n /**\n * Expose the underlying Drizzle table for typed Drizzle queries.\n *\n * Use this when you need column references for `.where()`, `.orderBy()`,\n * aggregate expressions, or JOINs with other entity tables. This is the\n * standardized way to access entity columns — never use string table names\n * or `sql.identifier()`.\n *\n * @example\n * ```ts\n * const t = ticketClient.getTable()\n * const stats = await ticketClient.aggregate({\n * select: { count: sql<number>`count(*)::int` },\n * groupBy: [t.status],\n * })\n * ```\n */\n // biome-ignore lint/suspicious/noExplicitAny: entity tables have dynamic columns not known at compile time\n getTable(): PgTableWithColumns<any> {\n return this.table\n }\n\n /**\n * Expose the database handle for sibling infrastructure.\n *\n * Wrapper clients (MediaClient, ContentClient, TaxonomyClient) use this\n * to create `TableClient` instances for infrastructure tables (versions,\n * drafts, locks) that sit alongside entity tables. Not intended for\n * end-user code — wrapper packages are trusted consumers.\n */\n getDb(): PostgresJsDatabase {\n return this.db\n }\n\n /**\n * Prepare data for insert — every field is a real column.\n */\n private prepareDataForInsert(data: Record<string, unknown>): Record<string, unknown> {\n const columns: Record<string, unknown> = {}\n\n for (const [fieldName, value] of Object.entries(data)) {\n const fieldConfig = getAllFields(this.ctx)[fieldName]\n if (!fieldConfig) continue\n if (fieldName === 'id') continue\n if (fieldConfig.type === 'blocks') continue\n columns[fieldName] = value\n }\n\n // Pass through infrastructure column (not in allFields, added by schema generator)\n if (data._scopeId !== undefined) columns._scopeId = data._scopeId\n\n return columns\n }\n\n /**\n * Prepare data for update — every field is a real column.\n * Standard column SET, no JSONB merge needed.\n */\n private prepareDataForUpdate(data: Record<string, unknown>): Record<string, unknown> {\n const columns: Record<string, unknown> = {}\n\n for (const [fieldName, value] of Object.entries(data)) {\n const fieldConfig = getAllFields(this.ctx)[fieldName]\n if (!fieldConfig) continue\n if (fieldName === 'id') continue\n if (fieldConfig.type === 'blocks') continue\n columns[fieldName] = value\n }\n\n return columns\n }\n\n /**\n * Save translation for an entity.\n * Each translatable field is a real column on the translation table.\n */\n async saveTranslation(\n entityId: string,\n locale: string,\n translations: Record<string, unknown>,\n ): Promise<void> {\n await assertEntityAccess(this.entity, this.contextResolver, 'update')\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Saving translation')\n\n const translationTableName = `${this.entity.name}_translations`\n const translationTable = schemaRegistry.get(translationTableName)\n\n if (!translationTable) {\n throw new Error(`Translation table ${translationTableName} not found in schema registry`)\n }\n\n // Get translatable field names\n const translatableFields = Object.entries(getAllFields(this.ctx))\n .filter(([_, config]) => config.translatable)\n .map(([name]) => name)\n\n if (translatableFields.length === 0) {\n throw new Error(`Entity ${this.entity.name} has no translatable fields`)\n }\n\n // Extract only translatable fields — these are real columns on the translation table\n const translationData: Record<string, unknown> = { entityId, locale }\n const updateData: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n if (translations[fieldName] !== undefined) {\n translationData[fieldName] = translations[fieldName]\n updateData[fieldName] = translations[fieldName]\n }\n }\n\n // Upsert: insert or update on conflict\n await this.db\n .insert(translationTable)\n .values(translationData)\n .onConflictDoUpdate({\n target: [translationTable.entityId, translationTable.locale],\n set: updateData,\n })\n\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Translation saved')\n }\n\n /**\n * PHASE 3: Delete translation(s) for an entity\n * If locale is provided, deletes specific locale; otherwise deletes all translations\n */\n async deleteTranslation(entityId: string, locale?: string): Promise<void> {\n await assertEntityAccess(this.entity, this.contextResolver, 'update')\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Deleting translation')\n\n const translationTableName = `${this.entity.name}_translations`\n const translationTable = schemaRegistry.get(translationTableName)\n\n if (!translationTable) {\n throw new Error(`Translation table ${translationTableName} not found in schema registry`)\n }\n\n if (locale) {\n // Delete specific locale\n await this.db\n .delete(translationTable)\n .where(and(eq(translationTable.entityId, entityId), eq(translationTable.locale, locale)))\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Translation deleted')\n } else {\n // Delete all translations for entity\n await this.db.delete(translationTable).where(eq(translationTable.entityId, entityId))\n this.logger?.info({ entity: this.entity.name, entityId }, 'All translations deleted')\n }\n }\n\n /**\n * Save block-level translations for an entity.\n * For each block in each blocks field, extracts translatable field values\n * and upserts them into {entity}_layout_translations.\n *\n * Block _id must match an existing layout row ID.\n */\n async saveBlockTranslations(\n entityId: string,\n locale: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n await assertEntityAccess(this.entity, this.contextResolver, 'update')\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Saving block translations')\n\n const blocksFields = getBlocksFields(this.ctx)\n if (blocksFields.length === 0) return\n\n const layoutTransTable = schemaRegistry.get(`${this.entity.name}_layout_translations`)\n if (!layoutTransTable) return\n\n const layoutTable = schemaRegistry.get(`${this.entity.name}_layout`)\n if (!layoutTable) return\n\n for (const { name: fieldName, config } of blocksFields) {\n const blocks = data[fieldName]\n if (!Array.isArray(blocks)) continue\n\n // Get block definitions to identify translatable fields\n if (config.type !== 'blocks') continue\n const blockDefs = config.blocks\n if (!blockDefs) continue\n\n // Load actual layout row IDs from DB (ordered by sort_order to match request array order).\n // The _id from the request may be a Puck-generated ID (e.g. \"hero-abc123\") rather than\n // the real DB UUID, so we match by position instead.\n const layoutRows = await this.db\n .select({ id: layoutTable.id, blockType: layoutTable.blockType })\n .from(layoutTable)\n .where(\n and(\n eq(layoutTable.entityId, entityId),\n eq(layoutTable.fieldName, fieldName),\n isNull(layoutTable.locale),\n ),\n )\n .orderBy(layoutTable.sortOrder)\n\n for (let i = 0; i < (blocks as Record<string, unknown>[]).length; i++) {\n const block = (blocks as Record<string, unknown>[])[i]\n const blockType = block._block as string\n if (!blockType) continue\n\n // Match by position — layout rows and request blocks are in the same sort order\n const layoutRow = layoutRows[i]\n if (!layoutRow || layoutRow.blockType !== blockType) continue\n\n const blockDef = blockDefs.find((b) => b.slug === blockType)\n if (!blockDef) continue\n\n // Extract only translatable fields\n const translatedFields: Record<string, unknown> = {}\n for (const [fname, fconfig] of Object.entries(blockDef.fields)) {\n if (fconfig.translatable && block[fname] !== undefined) {\n translatedFields[fname] = block[fname]\n }\n }\n\n if (Object.keys(translatedFields).length === 0) continue\n\n // Upsert using the real DB layout row ID\n await this.db\n .insert(layoutTransTable)\n .values({ layoutId: layoutRow.id, locale, fields: translatedFields })\n .onConflictDoUpdate({\n target: [layoutTransTable.layoutId, layoutTransTable.locale],\n set: { fields: translatedFields },\n })\n }\n }\n\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Block translations saved')\n }\n\n /**\n * Save per-locale blocks for an entity.\n * Used for entities with `localized: true` on their blocks field — each locale gets\n * its own independent block layout rows in the layout table.\n */\n async saveLocalizedBlocks(\n entityId: string,\n locale: string,\n data: Record<string, unknown>,\n ): Promise<void> {\n await assertEntityAccess(this.entity, this.contextResolver, 'update')\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Saving localized blocks')\n await this.saveBlocks(entityId, data, { locale })\n }\n\n // ---------------------------------------------------------------\n // Block CRUD (layout table operations)\n // ---------------------------------------------------------------\n\n /**\n * Save blocks for an entity after create/update.\n * For each blocks field, writes rows to {entity}_layout table.\n */\n private async saveBlocks(\n entityId: string,\n data: Record<string, unknown>,\n options?: { locale?: string },\n ): Promise<void> {\n const blocksFields = getBlocksFields(this.ctx)\n if (blocksFields.length === 0) return\n\n const layoutTable = schemaRegistry.get(`${this.entity.name}_layout`)\n if (!layoutTable) return\n\n for (const { name: fieldName, config } of blocksFields) {\n const blocks = data[fieldName]\n if (!Array.isArray(blocks)) continue\n\n const isLocalized = 'localized' in config && config.localized === true\n const locale = isLocalized ? (options?.locale ?? null) : null\n\n // Delete existing layout rows for this field + locale\n const deleteConditions = [\n eq(layoutTable.entityId, entityId),\n eq(layoutTable.fieldName, fieldName),\n ]\n if (locale) {\n deleteConditions.push(eq(layoutTable.locale, locale))\n } else if (!isLocalized) {\n // For shared layout, delete rows where locale IS NULL\n deleteConditions.push(isNull(layoutTable.locale))\n }\n await this.db.delete(layoutTable).where(and(...deleteConditions))\n\n // Insert new layout rows\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i] as Record<string, unknown>\n const blockType = block._block as string\n if (!blockType) continue\n\n // Separate _block and _id from block data\n const { _block, _id, ...blockData } = block\n\n await this.db.insert(layoutTable).values({\n entityId,\n fieldName,\n blockType,\n sortOrder: i,\n data: blockData,\n locale,\n })\n }\n }\n }\n\n // ---------------------------------------------------------------\n // Reference tracking (entity_refs)\n // ---------------------------------------------------------------\n\n /**\n * Sync outgoing references in entity_refs after create/update.\n *\n * - 'create': inserts all refs (entity is new, no existing refs)\n * - 'update': deletes refs for changed ref-bearing fields, then inserts new ones\n *\n * Gracefully skips if entity_refs table is not registered (e.g. before migration).\n */\n private async syncRefs(\n entityId: string,\n data: Record<string, unknown>,\n mode: 'create' | 'update',\n ): Promise<void> {\n // Graceful degradation: skip if entity_refs table doesn't exist yet\n if (!schemaRegistry.has('entity_refs')) return\n\n const allFields = getAllFields(this.ctx)\n\n if (mode === 'update') {\n // Only delete refs for fields present in the update data\n const changedRefFields = Object.keys(data).filter((k) => {\n const config = allFields[k]\n return (\n config &&\n (config.type === 'media' || config.type === 'reference' || config.type === 'blocks')\n )\n })\n if (changedRefFields.length > 0) {\n await this.db\n .delete(entityRefs)\n .where(\n and(\n eq(entityRefs.sourceEntity, this.entity.name),\n eq(entityRefs.sourceId, entityId),\n inArray(entityRefs.sourceField, changedRefFields),\n ),\n )\n }\n }\n\n // Extract refs from the data and insert\n const refs = extractRefs(allFields, data)\n if (refs.length > 0) {\n await this.db\n .insert(entityRefs)\n .values(\n refs.map((ref) => ({\n sourceEntity: this.entity.name,\n sourceId: entityId,\n sourceField: ref.sourceField,\n targetEntity: ref.targetEntity,\n targetId: ref.targetId,\n })),\n )\n .onConflictDoNothing()\n }\n }\n\n /**\n * Invalidate all count cache entries for this entity.\n */\n private invalidateCountCache(): void {\n this.countCache?.invalidate(this.entity.name)\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cursor.ts","../../src/dto-shaper.ts","../../src/refs/errors.ts","../../src/refs/extract-refs.ts","../../src/refs/schema.ts","../../src/shared/entity-data-ops.ts","../../src/validation.ts","../../src/admin/client.ts"],"sourcesContent":["/**\n * Cursor-based (keyset) pagination utilities.\n *\n * Cursor pagination avoids the performance cliff of OFFSET at scale (1M+ rows).\n * Instead of `OFFSET N`, it uses a WHERE condition:\n * `WHERE (sortField < lastValue) OR (sortField = lastValue AND id < lastId)`\n * which Postgres can serve from an index in constant time.\n *\n * The cursor is opaque to the client — base64-encoded JSON.\n *\n * Security:\n * - `field` must be whitelisted against the entity's actual fields\n * - `id` must be a valid UUID\n * - `value` is parameterized (never interpolated into SQL)\n * - Malformed cursors return null (caller returns 400)\n */\n\nimport { and, eq, getTableColumns, gt, lt, or, type SQL } from 'drizzle-orm'\nimport type { PgTable } from 'drizzle-orm/pg-core'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Cursor input for keyset pagination. */\nexport interface CursorInput {\n /** Sort field name (e.g. 'createdAt'). Must be a real column on the entity. */\n field: string\n /** Last seen value of the sort field. */\n value: string | number\n /** Sort direction — must match the ORDER BY direction. */\n direction: 'asc' | 'desc'\n /** Tie-breaker: last seen entity ID. Required for non-unique sort fields. */\n id?: string | undefined\n}\n\n/** Decoded cursor (internal, after validation). */\ninterface DecodedCursor {\n field: string\n value: string | number\n direction: 'asc' | 'desc'\n id?: string | undefined\n}\n\n// ---------------------------------------------------------------------------\n// UUID validation (same format used throughout the toolkit)\n// ---------------------------------------------------------------------------\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n// ---------------------------------------------------------------------------\n// Encode / decode\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a cursor for API transport (base64url).\n * Built from the last item in a result set.\n */\nexport function encodeCursor(\n item: Record<string, unknown>,\n sortField: string,\n direction: 'asc' | 'desc',\n): string {\n const payload: CursorInput = {\n field: sortField,\n value: item[sortField] as string | number,\n direction,\n id: item.id as string | undefined,\n }\n return btoa(JSON.stringify(payload))\n}\n\n/**\n * Decode and validate a cursor string from query params.\n * Returns null if the cursor is malformed, tampered, or invalid.\n *\n * Security: the `field` value is NOT validated here — the caller must\n * whitelist it against the entity's actual columns.\n */\nexport function decodeCursor(encoded: string): DecodedCursor | null {\n try {\n const json = atob(encoded)\n const parsed: unknown = JSON.parse(json)\n\n if (typeof parsed !== 'object' || parsed === null) return null\n const obj = parsed as Record<string, unknown>\n\n // Validate field\n if (typeof obj.field !== 'string' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(obj.field)) {\n return null\n }\n\n // Validate value (string or number)\n if (typeof obj.value !== 'string' && typeof obj.value !== 'number') {\n return null\n }\n\n // When the cursor sorts by `id`, the value lands in a UUID-typed\n // column. Reject non-UUID strings here so the caller doesn't surface\n // a Postgres cast error (and side-channel \"valid format vs invalid\n // format\") on the next request.\n if (obj.field === 'id' && (typeof obj.value !== 'string' || !UUID_RE.test(obj.value))) {\n return null\n }\n\n // Validate direction\n if (obj.direction !== 'asc' && obj.direction !== 'desc') {\n return null\n }\n\n // Validate id (optional, must be UUID if present)\n if (obj.id !== undefined) {\n if (typeof obj.id !== 'string' || !UUID_RE.test(obj.id)) {\n return null\n }\n }\n\n return {\n field: obj.field,\n value: obj.value,\n direction: obj.direction,\n id: obj.id as string | undefined,\n }\n } catch {\n return null\n }\n}\n\n// ---------------------------------------------------------------------------\n// SQL condition builder\n// ---------------------------------------------------------------------------\n\n/**\n * Build the keyset WHERE condition from a decoded cursor.\n *\n * For DESC: `WHERE (field < value) OR (field = value AND id < cursorId)`\n * For ASC: `WHERE (field > value) OR (field = value AND id > cursorId)`\n *\n * The caller must verify that `cursor.field` exists on the table before calling.\n *\n * @param table - Drizzle table with columns\n * @param cursor - Decoded and validated cursor\n * @returns SQL condition, or null if the field doesn't exist on the table\n */\nexport function buildCursorCondition(table: PgTable, cursor: DecodedCursor): SQL | null {\n const cols = getTableColumns(table)\n const column = cols[cursor.field]\n if (!column) return null\n\n const isDesc = cursor.direction === 'desc'\n const compare = isDesc ? lt : gt\n\n // Primary condition: sort field passes the cursor value\n const fieldCondition = compare(column, cursor.value)\n\n // Without tie-breaker ID, use simple comparison\n if (!cursor.id) {\n return fieldCondition\n }\n\n // With tie-breaker: (field < value) OR (field = value AND id < cursorId)\n const idColumn = cols.id\n if (!idColumn) return fieldCondition\n\n const idCondition = compare(idColumn, cursor.id)\n // Drizzle's `or`/`and` return SQL when called with at least one defined arg.\n // Fall back to fieldCondition rather than asserting non-null.\n return or(fieldCondition, and(eq(column, cursor.value), idCondition)) ?? fieldCondition\n}\n","/**\n * DTO Shaper\n * Transforms raw DB rows into clean DTOs.\n * All fields are real columns — just read from row directly.\n */\n\nimport type { Entity } from './define-entity.js'\nimport type { FieldConfig } from './fields/base.js'\nimport type { InferEntityDTO } from './types/infer.js'\n\nexport interface ShapeDtoOptions {\n /** Explicit field selection. */\n select?: string[] | undefined\n /**\n * Include internal fields in the output. Two rules apply:\n * 1. Fields marked `internal: true` in their config (the canonical flag).\n * 2. Legacy infrastructure columns whose name starts with `_` and are not\n * declared in `entity.allFields` (e.g. `_version`, `_scopeId` added by\n * the schema generator).\n *\n * Set this on trusted server reads that need to inspect or transition\n * state-machine fields (e.g. the workflow route reading the current\n * `_workflowStatus` before deciding the next state).\n */\n includeInternal?: boolean | undefined\n}\n\n/**\n * Shape a raw DB row into a typed DTO.\n * Every field is a real column — read directly from row.\n */\nexport function shapeDto<AllFields extends Record<string, FieldConfig>>(\n entity: Entity<AllFields>,\n row: Record<string, unknown>,\n options?: ShapeDtoOptions,\n): InferEntityDTO<AllFields> | null {\n if (!row) return null\n\n const result: Record<string, unknown> = {}\n const fieldsToInclude = options?.select || Object.keys(entity.allFields)\n\n for (const fieldName of fieldsToInclude) {\n const fieldConfig = (entity.allFields as Record<string, FieldConfig>)[fieldName]\n\n if (!options?.includeInternal) {\n // Two complementary internal-field rules, both bypassed by `includeInternal`:\n // 1. Naming convention: any field whose name starts with `_` (legacy\n // infrastructure columns like `_version`, `_scopeId`, plus any\n // `_workflowStatus`-style behavior field).\n // 2. Explicit flag: `internal: true` on the field config (the canonical\n // flag — works for fields that don't follow the underscore convention).\n const internalByName = fieldName.startsWith('_')\n const internalByFlag = fieldConfig?.internal === true\n if (internalByName || internalByFlag) continue\n }\n\n if (!fieldConfig) continue\n\n // Blocks are loaded separately from layout tables\n if (fieldConfig.type === 'blocks') continue\n\n result[fieldName] = row[fieldName]\n }\n\n return result as InferEntityDTO<AllFields>\n}\n\n/**\n * Shape multiple rows\n */\nexport function shapeDtos<AllFields extends Record<string, FieldConfig>>(\n entity: Entity<AllFields>,\n rows: Record<string, unknown>[],\n options?: ShapeDtoOptions,\n): (InferEntityDTO<AllFields> | null)[] {\n return rows.map((row) => shapeDto(entity, row, options))\n}\n","/**\n * Error thrown when attempting to delete an entity that is still referenced.\n */\n\nimport type { EntityUsage } from './find-usages.js'\n\nexport class ReferencedEntityError extends Error {\n public readonly entityName: string\n public readonly entityId: string\n public readonly usages: EntityUsage[]\n\n constructor(entityName: string, entityId: string, usages: EntityUsage[]) {\n const count = usages.length\n super(\n `Cannot delete ${entityName} '${entityId}': referenced by ${count} other entit${count === 1 ? 'y' : 'ies'}`,\n )\n this.name = 'ReferencedEntityError'\n this.entityName = entityName\n this.entityId = entityId\n this.usages = usages\n }\n}\n","/**\n * Schema-aware reference extraction.\n *\n * Walks entity field definitions and data to find all outgoing references.\n * Handles:\n * - field.media() → target = 'media', single UUID\n * - field.reference() → target = config.entity, single UUID or UUID[]\n * - field.blocks() → inspects block instance data for media/reference fields\n */\n\nimport type { BlocksField, FieldConfig, ReferenceField } from '../fields/base.js'\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nexport interface ExtractedRef {\n sourceField: string\n targetEntity: string\n targetId: string\n}\n\n/**\n * Extract all outgoing references from entity data.\n *\n * @param allFields - The entity's complete field map (entity.allFields)\n * @param data - The entity data (full on create, partial on update)\n * @returns Deduplicated array of extracted references\n */\nexport function extractRefs(\n allFields: Record<string, FieldConfig>,\n data: Record<string, unknown>,\n): ExtractedRef[] {\n const refs: ExtractedRef[] = []\n const seen = new Set<string>()\n\n function add(sourceField: string, targetEntity: string, targetId: string) {\n if (!UUID_RE.test(targetId)) return\n const key = `${sourceField}|${targetEntity}|${targetId}`\n if (seen.has(key)) return\n seen.add(key)\n refs.push({ sourceField, targetEntity, targetId })\n }\n\n for (const [fieldName, config] of Object.entries(allFields)) {\n const value = data[fieldName]\n if (value == null) continue\n\n switch (config.type) {\n case 'media': {\n if (typeof value === 'string') {\n add(fieldName, 'media', value)\n }\n break\n }\n\n case 'reference': {\n const refConfig = config as ReferenceField\n if (refConfig.cardinality === 'many' && Array.isArray(value)) {\n for (const id of value) {\n if (typeof id === 'string') {\n add(fieldName, refConfig.entity, id)\n }\n }\n } else if (typeof value === 'string') {\n add(fieldName, refConfig.entity, value)\n }\n break\n }\n\n case 'blocks': {\n const blocksConfig = config as BlocksField\n if (!Array.isArray(value)) break\n\n for (const block of value) {\n const inst = block as Record<string, unknown>\n const blockType = inst._block as string | undefined\n if (!blockType) continue\n\n // Find the block definition to know its field types\n const blockDef = blocksConfig.blocks.find((b) => b.slug === blockType)\n if (!blockDef) continue\n\n // Scan block fields for media/reference values\n for (const [blockFieldName, blockFieldConfig] of Object.entries(blockDef.fields)) {\n const blockValue = inst[blockFieldName]\n if (blockValue == null) continue\n\n if (blockFieldConfig.type === 'media' && typeof blockValue === 'string') {\n add(fieldName, 'media', blockValue)\n }\n\n if (blockFieldConfig.type === 'reference') {\n const blockRefConfig = blockFieldConfig as ReferenceField\n if (blockRefConfig.cardinality === 'many' && Array.isArray(blockValue)) {\n for (const id of blockValue) {\n if (typeof id === 'string') {\n add(fieldName, blockRefConfig.entity, id)\n }\n }\n } else if (typeof blockValue === 'string') {\n add(fieldName, blockRefConfig.entity, blockValue)\n }\n }\n }\n }\n break\n }\n }\n }\n\n return refs\n}\n\n/**\n * Get the names of all ref-bearing fields in the entity.\n * Used to scope partial-update ref deletion.\n */\nexport function getRefBearingFields(allFields: Record<string, FieldConfig>): string[] {\n const names: string[] = []\n for (const [fieldName, config] of Object.entries(allFields)) {\n if (config.type === 'media' || config.type === 'reference' || config.type === 'blocks') {\n names.push(fieldName)\n }\n }\n return names\n}\n","/**\n * entity_refs — Universal reference tracking table.\n *\n * Every reference between entities (field.media(), field.reference(), block media fields)\n * gets a row here at write time. This enables:\n * - Instant \"where is this used?\" queries (one indexed lookup)\n * - Universal delete protection (can't delete referenced entities)\n * - Correct results (schema-aware, no LIKE text search)\n */\n\nimport { index, pgTable, unique, uuid, varchar } from 'drizzle-orm/pg-core'\n\nexport const entityRefs = pgTable(\n 'entity_refs',\n {\n sourceEntity: varchar('source_entity', { length: 100 }).notNull(),\n sourceId: uuid('source_id').notNull(),\n sourceField: varchar('source_field', { length: 100 }).notNull(),\n targetEntity: varchar('target_entity', { length: 100 }).notNull(),\n targetId: uuid('target_id').notNull(),\n },\n (t) => [\n unique('uq_entity_refs').on(\n t.sourceEntity,\n t.sourceId,\n t.sourceField,\n t.targetEntity,\n t.targetId,\n ),\n index('idx_entity_refs_target').on(t.targetEntity, t.targetId),\n index('idx_entity_refs_source').on(t.sourceEntity, t.sourceId),\n ],\n)\n","/**\n * Shared entity data operations.\n *\n * Extracted from AdminClient and QueryClient to eliminate duplication.\n * These are standalone functions that take an EntityContext — both clients\n * satisfy this interface via `this`.\n */\n\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { and, eq, inArray, isNull, or, type SQL } from 'drizzle-orm'\nimport type { PgTableWithColumns } from 'drizzle-orm/pg-core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { BehaviorContext } from '../behaviors/types.js'\nimport type { Entity } from '../define-entity.js'\nimport type { FieldConfig } from '../fields/base.js'\n\n// ---------------------------------------------------------------------------\n// Context interface — satisfied by both AdminClient and QueryClient via `this`\n// ---------------------------------------------------------------------------\n\n/**\n * Security context resolved per-request. Provided by the consumer\n * (e.g., via React.cache() in Next.js, or runAsCli for CLI).\n * The entity package has zero knowledge of how this is resolved.\n */\nexport interface SecurityContext {\n user: { id: string; groups: string[]; name?: string; email?: string }\n checker: (role: string, resource: string, action: string) => boolean\n scope?: { type: string; id: string }\n}\n\n/**\n * Function that resolves the current request's security context.\n * Injected at construction time — the entity package never imports\n * @murumets-ee/core or any framework-specific code.\n *\n * Returns undefined only when intentionally skipping enforcement\n * (should not happen in production — throw ForbiddenError instead).\n */\nexport type ContextResolver = () =>\n | SecurityContext\n | undefined\n | Promise<SecurityContext | undefined>\n\nexport interface EntityContext {\n entity: Entity\n db: PostgresJsDatabase\n // biome-ignore lint/suspicious/noExplicitAny: dynamic table columns require PgTableWithColumns<any>\n table: PgTableWithColumns<any>\n /** Resolves the current request's security context. */\n resolveContext?: ContextResolver | undefined\n}\n\n// ---------------------------------------------------------------------------\n// Field accessors\n// ---------------------------------------------------------------------------\n\n/** Get the entity's full field map with proper typing. Eliminates repeated casts. */\nexport function getAllFields(ctx: EntityContext): Record<string, FieldConfig> {\n return ctx.entity.allFields as Record<string, FieldConfig>\n}\n\n/** Get all blocks field definitions for this entity. */\nexport function getBlocksFields(ctx: EntityContext): Array<{ name: string; config: FieldConfig }> {\n return Object.entries(getAllFields(ctx))\n .filter(([_, config]) => config.type === 'blocks')\n .map(([name, config]) => ({ name, config }))\n}\n\n// ---------------------------------------------------------------------------\n// Translation merging\n// ---------------------------------------------------------------------------\n\n/**\n * Merge translations into entities for the specified locale.\n * Reads translatable field values from real columns on the translation row.\n */\nexport async function mergeTranslations<T extends Record<string, unknown>>(\n ctx: EntityContext,\n entities: T[],\n locale: string,\n): Promise<T[]> {\n if (!entities.length) return entities\n\n const translationTableName = `${ctx.entity.name}_translations`\n const translationTable = schemaRegistry.get(translationTableName)\n\n if (!translationTable) return entities\n\n const entityIds = entities.map((e) => e.id)\n\n const translations = await ctx.db\n .select()\n .from(translationTable)\n .where(and(inArray(translationTable.entityId, entityIds), eq(translationTable.locale, locale)))\n\n // Get translatable field names\n const translatableFields = Object.entries(getAllFields(ctx))\n .filter(([_, config]) => config.translatable)\n .map(([name]) => name)\n\n const translationMap = new Map<unknown, Record<string, unknown>>()\n for (const translation of translations) {\n const translatedValues: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n const value = (translation as Record<string, unknown>)[fieldName]\n if (value !== undefined && value !== null) {\n translatedValues[fieldName] = value\n }\n }\n translationMap.set(translation.entityId, translatedValues)\n }\n\n return entities.map((entity) => {\n const translatedFields = translationMap.get(entity.id)\n if (!translatedFields) return entity\n return { ...entity, ...translatedFields }\n })\n}\n\n// ---------------------------------------------------------------------------\n// Block loading\n// ---------------------------------------------------------------------------\n\nexport interface LoadBlocksOptions {\n defaultLocale?: string | undefined\n /**\n * When true (admin editing), clears untranslated translatable block fields\n * to empty string — signals incomplete translation in the editing UI.\n * When false (visitor-facing), preserves base data as fallback.\n */\n strictTranslations?: boolean | undefined\n}\n\n/**\n * Load blocks for one or more entities from the layout table.\n *\n * Handles both block translation modes:\n * - Shared layout (localized: false): loads locale=NULL rows, merges translations\n * - Per-locale layout (localized: true): loads rows matching the provided locale only\n */\nexport async function loadBlocks(\n ctx: EntityContext,\n entityIds: string[],\n locale?: string,\n options?: LoadBlocksOptions,\n): Promise<Map<string, Record<string, unknown[]>>> {\n const blocksFields = getBlocksFields(ctx)\n if (blocksFields.length === 0 || entityIds.length === 0) {\n return new Map()\n }\n\n const layoutTable = schemaRegistry.get(`${ctx.entity.name}_layout`)\n if (!layoutTable) return new Map()\n\n // Determine block field modes\n const sharedFields = blocksFields.filter(\n ({ config }) => !('localized' in config && config.localized),\n )\n const localizedFields = blocksFields.filter(\n ({ config }) => 'localized' in config && config.localized,\n )\n\n const result = new Map<string, Record<string, unknown[]>>()\n\n // ---- Shared blocks: locale IS NULL, translations from layout_translations ----\n if (sharedFields.length > 0) {\n const rows = await ctx.db\n .select()\n .from(layoutTable)\n .where(and(inArray(layoutTable.entityId, entityIds), isNull(layoutTable.locale)))\n .orderBy(layoutTable.sortOrder)\n\n // Load translations if locale provided\n let blockTransMap: Map<string, Record<string, unknown>> | undefined\n if (locale && rows.length > 0) {\n const layoutTransTable = schemaRegistry.get(`${ctx.entity.name}_layout_translations`)\n if (layoutTransTable) {\n const layoutIds = rows.map((r) => r.id as string)\n const translations = await ctx.db\n .select()\n .from(layoutTransTable)\n .where(\n and(inArray(layoutTransTable.layoutId, layoutIds), eq(layoutTransTable.locale, locale)),\n )\n\n blockTransMap = new Map()\n for (const t of translations) {\n blockTransMap.set(t.layoutId as string, (t.fields as Record<string, unknown>) ?? {})\n }\n }\n }\n\n // Build block definition lookup for translatable field clearing (strict mode only)\n let blockDefMap: Map<string, Record<string, FieldConfig>> | undefined\n if (options?.strictTranslations) {\n blockDefMap = new Map()\n for (const { config } of sharedFields) {\n if (config.type !== 'blocks') continue\n for (const def of config.blocks) {\n blockDefMap.set(def.slug, def.fields)\n }\n }\n }\n\n for (const row of rows) {\n const eid = row.entityId as string\n const fname = row.fieldName as string\n\n let entityBlocks = result.get(eid)\n if (!entityBlocks) {\n entityBlocks = {}\n result.set(eid, entityBlocks)\n }\n if (!entityBlocks[fname]) entityBlocks[fname] = []\n\n const blockData = (row.data as Record<string, unknown>) ?? {}\n const translatedFields = blockTransMap?.get(row.id as string)\n\n // Build the block object\n const block: Record<string, unknown> = {\n _block: row.blockType,\n _id: row.id,\n ...blockData,\n ...(translatedFields ?? {}),\n }\n\n // Strict mode: clear untranslated translatable fields when loading for non-default locale\n if (locale && blockDefMap) {\n const blockType = row.blockType as string\n const fieldDefs = blockDefMap.get(blockType)\n if (fieldDefs) {\n for (const [fieldName, fieldConfig] of Object.entries(fieldDefs)) {\n if (fieldConfig.translatable && !translatedFields?.[fieldName]) {\n block[fieldName] = ''\n }\n }\n }\n }\n\n entityBlocks[fname].push(block)\n }\n }\n\n // ---- Localized blocks: filter by locale column ----\n // NULL rows (from initial create) only fall back for the default locale.\n // Non-default locales without locale-specific rows get an empty array.\n if (localizedFields.length > 0) {\n const isDefault = !locale || locale === options?.defaultLocale\n let localeCondition: SQL\n if (locale) {\n if (isDefault) {\n // Both args defined → drizzle's `or` always returns SQL; fall back defensively\n localeCondition =\n or(eq(layoutTable.locale, locale), isNull(layoutTable.locale)) ??\n eq(layoutTable.locale, locale)\n } else {\n localeCondition = eq(layoutTable.locale, locale)\n }\n } else {\n localeCondition = isNull(layoutTable.locale)\n }\n\n const rows = await ctx.db\n .select()\n .from(layoutTable)\n .where(and(inArray(layoutTable.entityId, entityIds), localeCondition))\n .orderBy(layoutTable.sortOrder)\n\n // Group by entityId + fieldName, prefer locale-specific rows over NULL\n const grouped = new Map<string, { localeRows: typeof rows; nullRows: typeof rows }>()\n for (const row of rows) {\n const key = `${row.entityId}::${row.fieldName}`\n let group = grouped.get(key)\n if (!group) {\n group = { localeRows: [], nullRows: [] }\n grouped.set(key, group)\n }\n if (row.locale) {\n group.localeRows.push(row)\n } else {\n group.nullRows.push(row)\n }\n }\n\n for (const [, { localeRows, nullRows }] of grouped) {\n // Use locale-specific rows if available, otherwise fall back to NULL\n const effective = localeRows.length > 0 ? localeRows : nullRows\n\n for (const row of effective) {\n const eid = row.entityId as string\n const fname = row.fieldName as string\n\n let entityBlocks = result.get(eid)\n if (!entityBlocks) {\n entityBlocks = {}\n result.set(eid, entityBlocks)\n }\n if (!entityBlocks[fname]) entityBlocks[fname] = []\n\n const blockData = (row.data as Record<string, unknown>) ?? {}\n\n entityBlocks[fname].push({\n _block: row.blockType,\n _id: row.id,\n ...blockData,\n })\n }\n }\n }\n\n return result\n}\n\n/**\n * Attach loaded blocks to shaped DTOs.\n */\nexport function attachBlocks(\n ctx: EntityContext,\n entities: Record<string, unknown>[],\n blocksMap: Map<string, Record<string, unknown[]>>,\n): void {\n const blocksFields = getBlocksFields(ctx)\n if (blocksFields.length === 0) return\n\n for (const entity of entities) {\n const eid = entity.id as string\n const entityBlocks = blocksMap.get(eid) ?? {}\n\n for (const { name } of blocksFields) {\n entity[name] = entityBlocks[name] ?? []\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Count cache key\n// ---------------------------------------------------------------------------\n\n/**\n * Build a cache key for a count query.\n * @param prefix - Optional namespace prefix (e.g. 'query' for QueryClient)\n * @param scopeId - Optional scope ID for multi-tenant isolation\n */\nexport function buildCountCacheKey(\n entityName: string,\n where?: SQL,\n prefix?: string,\n scopeId?: string,\n): string {\n let base = prefix ? `${prefix}:${entityName}` : entityName\n if (scopeId) base = `${base}@${scopeId}`\n if (!where) return base\n // Use SQL's toString() representation as a stable key.\n // This is a display string, not executed — safe for cache keying.\n return `${base}:${String(where)}`\n}\n\n// ---------------------------------------------------------------------------\n// ForbiddenError\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when a user lacks permission for an entity operation.\n * Consumers (api-handler) catch this and return HTTP 403.\n */\nexport class ForbiddenError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ForbiddenError'\n }\n}\n\n// ---------------------------------------------------------------------------\n// Context helpers — dynamic imports to avoid entity → core circular dep\n// ---------------------------------------------------------------------------\n\n// ---------------------------------------------------------------------------\n// Context resolution — framework-agnostic\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve the security context from the resolver on EntityContext.\n * Throws ForbiddenError if no resolver is configured or if it returns undefined.\n */\nasync function resolveSecurityContext(\n resolver: ContextResolver | undefined,\n): Promise<SecurityContext> {\n if (!resolver) {\n throw new ForbiddenError(\n 'No context resolver configured on AdminClient/QueryClient. ' +\n 'Use createAdminClient() from @murumets-ee/core/clients, or pass a contextResolver in config.',\n )\n }\n\n const ctx = await resolver()\n\n if (!ctx) {\n throw new ForbiddenError(\n 'Context resolver returned no context. ' +\n 'Ensure auth is configured in your request handler, or use runAsCli() for CLI scripts.',\n )\n }\n\n return ctx\n}\n\n// ---------------------------------------------------------------------------\n// Permission enforcement\n// ---------------------------------------------------------------------------\n\n/**\n * Assert the current user has permission for the given action on the entity.\n * Uses the PermissionChecker from the resolved SecurityContext.\n */\nexport async function assertEntityAccess(\n entity: Entity,\n resolver: ContextResolver | undefined,\n action: 'view' | 'create' | 'update' | 'delete',\n): Promise<void> {\n const ctx = await resolveSecurityContext(resolver)\n\n const role = ctx.user.groups[0]\n if (!role) {\n throw new ForbiddenError(`User '${ctx.user.id}' has no role assigned.`)\n }\n\n if (!ctx.checker(role, entity.name, action)) {\n throw new ForbiddenError(`Forbidden: role '${role}' cannot ${action} '${entity.name}'`)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Scope filtering\n// ---------------------------------------------------------------------------\n\n/**\n * Get the current scope ID for a scoped entity.\n * Returns undefined for global entities.\n */\nexport async function getScopeId(\n entity: Entity,\n resolver: ContextResolver | undefined,\n): Promise<string | undefined> {\n if (!entity.scope || entity.scope === 'global') return undefined\n\n const ctx = await resolveSecurityContext(resolver)\n return ctx.scope?.id\n}\n\n/**\n * Build a WHERE condition for scope filtering.\n */\nexport function buildScopeCondition(ctx: EntityContext, scopeId: string): SQL {\n return eq(ctx.table._scopeId, scopeId)\n}\n\n/**\n * Require scope for a scoped entity. Throws if no scope is in context.\n * Returns undefined for global entities.\n */\nexport async function requireScopeForEntity(\n entity: Entity,\n resolver: ContextResolver | undefined,\n): Promise<string | undefined> {\n if (!entity.scope || entity.scope === 'global') return undefined\n\n const ctx = await resolveSecurityContext(resolver)\n const scopeId = ctx.scope?.id\n\n if (!scopeId) {\n throw new ForbiddenError(\n `Entity '${entity.name}' requires scope '${entity.scope}' but no scope is set in context.`,\n )\n }\n\n return scopeId\n}\n\n// ---------------------------------------------------------------------------\n// Combined auth context resolution (single resolver call)\n// ---------------------------------------------------------------------------\n\nexport interface ResolvedAuthContext {\n /** The full SecurityContext returned by the resolver. */\n security: SecurityContext\n /** Scope ID for scoped entities; undefined for global. */\n scopeId: string | undefined\n /** BehaviorContext to thread into lifecycle hooks. */\n behaviorCtx: BehaviorContext\n}\n\n/**\n * Resolve security context, check permission, derive scope ID, and build\n * BehaviorContext — all from a single resolver invocation.\n *\n * Use instead of calling `assertEntityAccess` + `getScopeId`/`requireScopeForEntity`\n * + manually building a behavior context. Reduces 2-3 resolver calls per write\n * to one, while keeping the granular helpers available for special cases\n * (e.g. cascade delete, which checks permission on a different entity).\n *\n * @param strictScope When true and entity is scoped without a scope in context,\n * throws ForbiddenError. Set on write paths; leave false for\n * reads (callers receive scopeId=undefined and skip filtering).\n * @param enqueueOnCommit Resolved enqueue function from\n * `AdminClientConfig.enqueueOnCommit`. When provided, this\n * is set on `behaviorCtx.enqueueOnCommit` so projection\n * behaviors can fire jobs without importing the queue\n * package. PR C of PLAN-OUTBOX.\n */\nexport async function resolveAuthContext(\n entity: Entity,\n resolver: ContextResolver | undefined,\n action: 'view' | 'create' | 'update' | 'delete',\n options?: {\n strictScope?: boolean\n enqueueOnCommit?: BehaviorContext['enqueueOnCommit']\n },\n): Promise<ResolvedAuthContext> {\n const security = await resolveSecurityContext(resolver)\n\n // Permission check — same logic as assertEntityAccess()\n const role = security.user.groups[0]\n if (!role) {\n throw new ForbiddenError(`User '${security.user.id}' has no role assigned.`)\n }\n if (!security.checker(role, entity.name, action)) {\n throw new ForbiddenError(`Forbidden: role '${role}' cannot ${action} '${entity.name}'`)\n }\n\n // Scope resolution — same logic as getScopeId()/requireScopeForEntity()\n let scopeId: string | undefined\n if (entity.scope && entity.scope !== 'global') {\n scopeId = security.scope?.id\n if (!scopeId && options?.strictScope) {\n throw new ForbiddenError(\n `Entity '${entity.name}' requires scope '${entity.scope}' but no scope is set in context.`,\n )\n }\n }\n\n const behaviorCtx: BehaviorContext = {\n user: {\n id: security.user.id,\n ...(security.user.name !== undefined && { name: security.user.name }),\n ...(security.user.email !== undefined && { email: security.user.email }),\n },\n ...(options?.enqueueOnCommit !== undefined && { enqueueOnCommit: options.enqueueOnCommit }),\n }\n\n return { security, scopeId, behaviorCtx }\n}\n","/**\n * Validation schema generator\n * Converts entity field definitions to Zod schemas for runtime validation\n */\n\nimport { z } from 'zod'\nimport type { Entity } from './define-entity.js'\nimport type { FieldConfig } from './fields/base.js'\n\n/**\n * Convert a field definition to a Zod schema\n */\nfunction fieldToZod(fieldConfig: FieldConfig): z.ZodType {\n let schema: z.ZodType\n\n switch (fieldConfig.type) {\n case 'id':\n schema = z.string().uuid()\n break\n\n case 'text': {\n let s = z.string()\n if (fieldConfig.maxLength) s = s.max(fieldConfig.maxLength)\n if (fieldConfig.minLength) s = s.min(fieldConfig.minLength)\n if (fieldConfig.pattern) s = s.regex(fieldConfig.pattern)\n schema = s\n break\n }\n\n case 'number': {\n let n = z.number()\n if (fieldConfig.integer) n = n.int()\n if (fieldConfig.min !== undefined) n = n.min(fieldConfig.min)\n if (fieldConfig.max !== undefined) n = n.max(fieldConfig.max)\n schema = n\n break\n }\n\n case 'boolean':\n schema = z.boolean()\n break\n\n case 'date':\n schema = z.date().or(z.string().datetime())\n // Allow either Date object or ISO string, coerce to Date\n break\n\n case 'select':\n schema = z.enum(fieldConfig.options as [string, ...string[]])\n break\n\n case 'reference':\n if (fieldConfig.cardinality === 'many') {\n schema = z.array(z.string().uuid())\n } else {\n schema = z.string().uuid()\n }\n break\n\n case 'media':\n schema = z.string().uuid() // Media entity ID\n break\n\n case 'richtext':\n // HTML string (TipTap/Puck) or Slate JSON array (Plate/entity forms)\n schema = z.union([z.string(), z.array(z.record(z.unknown()))])\n break\n\n case 'slug':\n schema = z.string().regex(/^[a-z0-9-]+$/)\n break\n\n case 'json':\n // JSON fields accept any JSON-compatible value (objects, arrays, primitives).\n // Mirrors the JsonValue type in types/infer.ts and what Postgres jsonb stores.\n schema = z.union([\n z.record(z.unknown()),\n z.array(z.unknown()),\n z.string(),\n z.number(),\n z.boolean(),\n ])\n break\n\n case 'blocks':\n // Array of block instances with _block discriminator.\n // .passthrough() is required because each block type has different dynamic props\n // (title, image, content, etc.) that can't be validated generically here.\n // Per-block-type validation happens in the block editor's converter layer.\n // Security: extra props are harmless at storage level since rendering maps to\n // known component definitions and ignores unknown fields.\n schema = z.array(\n z\n .object({\n _block: z.string(),\n })\n .passthrough(),\n )\n break\n\n default:\n schema = z.unknown()\n }\n\n // Apply required/optional — non-required fields accept both undefined and null\n if (!fieldConfig.required) {\n schema = schema.nullable().optional()\n }\n\n // Apply default value\n if (fieldConfig.default !== undefined) {\n schema = schema.default(fieldConfig.default)\n }\n\n return schema\n}\n\n/**\n * Options accepted by the create/update schema generators.\n *\n * `includeInternal` controls whether fields marked `internal: true` are part\n * of the input schema. Default: `false` — public-surface schemas (used by\n * `AdminClient.create()` / `update()`) reject caller writes to internal\n * fields. The `updateInternal()` escape hatch sets it to `true` so trusted\n * server code can transition state-machine fields directly.\n */\nexport interface SchemaOptions {\n includeInternal?: boolean\n}\n\nfunction isOmittedFromUpdate(fieldName: string): boolean {\n // Immutable + audit fields are managed by hooks/server, not callers.\n return (\n fieldName === 'id' ||\n fieldName === 'createdAt' ||\n fieldName === 'createdBy' ||\n fieldName === 'updatedAt' ||\n fieldName === 'updatedBy' ||\n fieldName === '_version'\n )\n}\n\nfunction isOmittedFromCreate(fieldName: string): boolean {\n // Auto-generated + audit fields. `updatedBy` allowed at create-time\n // (unlike updates) because some flows seed it from the creator.\n return (\n fieldName === 'id' ||\n fieldName === 'createdAt' ||\n fieldName === 'updatedAt' ||\n fieldName === 'createdBy' ||\n fieldName === '_version'\n )\n}\n\n/**\n * Generate complete validation schema for an entity\n */\nexport function generateValidationSchema(entity: Entity): z.AnyZodObject {\n const shape: Record<string, z.ZodType> = {}\n\n for (const [fieldName, fieldConfig] of Object.entries(entity.allFields)) {\n shape[fieldName] = fieldToZod(fieldConfig)\n }\n\n return z.object(shape)\n}\n\n/**\n * Generate schema for entity creation. Internal fields are omitted unless\n * `includeInternal: true` is passed (see `SchemaOptions`). The `id` /\n * audit-trail fields are always omitted.\n */\nexport function generateCreateSchema(\n entity: Entity,\n options: SchemaOptions = {},\n): z.AnyZodObject {\n const shape: Record<string, z.ZodType> = {}\n\n for (const [fieldName, fieldConfig] of Object.entries(entity.allFields)) {\n if (isOmittedFromCreate(fieldName)) continue\n if (fieldConfig.internal && !options.includeInternal) continue\n shape[fieldName] = fieldToZod(fieldConfig)\n }\n\n return z.object(shape)\n}\n\n/**\n * Generate schema for entity updates (all fields optional). Internal fields\n * are omitted unless `includeInternal: true` is passed (see `SchemaOptions`).\n * The `id` / audit-trail fields are always omitted.\n */\nexport function generateUpdateSchema(\n entity: Entity,\n options: SchemaOptions = {},\n): z.AnyZodObject {\n const shape: Record<string, z.ZodType> = {}\n\n for (const [fieldName, fieldConfig] of Object.entries(entity.allFields)) {\n if (isOmittedFromUpdate(fieldName)) continue\n if (fieldConfig.internal && !options.includeInternal) continue\n shape[fieldName] = fieldToZod(fieldConfig).optional()\n }\n\n return z.object(shape)\n}\n","/**\n * AdminClient - Full CRUD with server-only enforcement\n * CRITICAL: This file MUST NOT be imported in client code\n */\n\n// NOTE: We use runtime check instead of 'server-only' import to allow CLI scripts\n// The 'server-only' package blocks ALL non-Next.js contexts (including Node.js scripts)\n// Layer 3: Runtime check (catches what bundlers miss, allows CLI usage)\n\nimport { schemaRegistry } from '@murumets-ee/db'\nimport { and, eq, inArray, isNull, type SQL, sql } from 'drizzle-orm'\nimport type { PgTableWithColumns } from 'drizzle-orm/pg-core'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { ZodType } from 'zod'\nimport type { BehaviorContext, EnqueueOnCommitFactory } from '../behaviors/types.js'\nimport type { CountCacheLike } from '../count-cache.js'\nimport type { CursorInput } from '../cursor.js'\nimport { buildCursorCondition } from '../cursor.js'\nimport type { Entity } from '../define-entity.js'\nimport { shapeDto, shapeDtos } from '../dto-shaper.js'\nimport type { FieldConfig } from '../fields/base.js'\nimport { ReferencedEntityError } from '../refs/errors.js'\nimport { extractRefs } from '../refs/extract-refs.js'\nimport { entityRefs } from '../refs/schema.js'\nimport {\n assertEntityAccess,\n attachBlocks,\n buildCountCacheKey,\n buildScopeCondition,\n type ContextResolver,\n type EntityContext,\n getAllFields,\n getBlocksFields,\n loadBlocks,\n mergeTranslations,\n resolveAuthContext,\n} from '../shared/entity-data-ops.js'\nimport type { InferCreateInput, InferEntityDTO, InferUpdateInput } from '../types/infer.js'\nimport type { Logger } from '../types/logger.js'\nimport { generateCreateSchema, generateUpdateSchema } from '../validation.js'\n\n/**\n * Resolves the registry of all entities in the running app — used by cascade\n * delete to look up `onDelete` strategy on referencing fields. Injected as a\n * function so the entity package never has to import `@murumets-ee/core`\n * (which would create a circular dependency and force runtime tricks that\n * break under bundlers like Turbopack).\n */\nexport type EntityResolver = () => Map<string, Entity> | undefined\n\nexport interface AdminClientConfig<\n AllFields extends Record<string, FieldConfig> = Record<string, FieldConfig>,\n> {\n entity: Entity<AllFields>\n db: PostgresJsDatabase\n logger?: Logger | undefined\n /** Optional count cache for COUNT(*) query optimization. */\n countCache?: CountCacheLike | undefined\n /**\n * Resolves the current request's security context (user, role checker, scope).\n * Provided automatically by `createAdminClient()` from @murumets-ee/core/clients.\n * For direct `new AdminClient()` usage, pass your own resolver or use `runAsCli()`.\n */\n contextResolver?: ContextResolver | undefined\n /**\n * Resolves the running app's entity registry. Used by cascade delete to\n * read each referencing field's `onDelete` strategy. When omitted, all\n * incoming references are treated as `restrict` — a safe default that\n * blocks deletes rather than guessing.\n */\n entityResolver?: EntityResolver | undefined\n /**\n * Outbox entry point exposed on `BehaviorContext.enqueueOnCommit`\n * (PR C of PLAN-OUTBOX). When provided, projection-style behaviors can\n * fire side-effect jobs from their hooks without importing the queue\n * package — the entity package stays a leaf in the dependency graph.\n *\n * Used as a fallback when `enqueueOnCommitFactory` is not supplied:\n * AdminClient passes this resolver verbatim into hook contexts, and\n * the row INSERT is NOT bound to the operation's transaction. Tests\n * that need to capture call-site behavior without wiring queue can\n * pass this directly.\n *\n * In production wiring (`createAdminClient` in\n * `@murumets-ee/core/clients`), prefer `enqueueOnCommitFactory` —\n * that's the form that delivers PLAN-OUTBOX §4.2's \"rollback removes\n * the job\" guarantee.\n */\n enqueueOnCommit?: BehaviorContext['enqueueOnCommit'] | undefined\n /**\n * Outbox factory exposed on `BehaviorContext.enqueueOnCommit` (PR D\n * of PLAN-OUTBOX). When provided, AdminClient invokes the factory\n * once per CRUD operation with the active Drizzle transaction so the\n * resulting fire-and-forget resolver routes its INSERT through that\n * tx. Hook-fired enqueues commit (or roll back) atomically with the\n * entity write — the canonical PLAN-OUTBOX §4.2 semantics.\n *\n * Wired by `createAdminClient()` in `@murumets-ee/core/clients`\n * against the running app's queue plugin. Takes precedence over the\n * older `enqueueOnCommit` field when both are set; CLI scripts that\n * don't load the queue plugin omit both.\n */\n enqueueOnCommitFactory?: EnqueueOnCommitFactory | undefined\n}\n\nexport interface FindManyOptions {\n where?: SQL | undefined\n limit?: number | undefined\n offset?: number | undefined\n orderBy?: SQL | SQL[] | undefined\n locale?: string | undefined\n /** Default content locale. For localized blocks, NULL rows (from initial create)\n * are only returned as fallback when locale matches defaultLocale. */\n defaultLocale?: string | undefined\n /**\n * Cursor-based (keyset) pagination. When provided, replaces OFFSET with a\n * WHERE condition for O(1) page access at any depth. The `offset` option\n * is ignored when `cursor` is set.\n *\n * The cursor `field` must be a real column on the entity table.\n */\n cursor?: CursorInput | undefined\n /**\n * Include fields marked `internal: true` (and legacy `_`-prefixed\n * infrastructure columns) in the returned DTOs. Use only on trusted\n * server-side reads that need to inspect state-machine fields.\n */\n includeInternal?: boolean | undefined\n}\n\nexport interface CountOptions {\n where?: SQL | undefined\n}\n\n/**\n * AdminClient - Full CRUD operations with data integrity + security enforcement\n *\n * **Requires RequestContext.** All operations check permissions and scope via\n * the context established by `runWithContextAsync()`. CLI scripts use `runAsCli()`.\n *\n * Security & integrity layers:\n * 1. Runtime check: `typeof window !== 'undefined'` → throw (prevents browser usage)\n * 2. Permission enforcement: `assertEntityAccess()` checks `checker(role, entity, action)` from context\n * 3. Scope filtering: scoped entities auto-filter by `_scopeId` from context\n * 4. Zod validation: every write (create, update, updateMany) validated before DB\n * 5. Column whitelist: `prepareDataForInsert/Update` drops unknown fields\n * 6. Hook execution: `beforeCreate`, `afterCreate`, etc. from entity behaviors\n * 7. Reference tracking: `entity_refs` table for delete protection\n *\n * Note: We don't use 'server-only' import because it blocks CLI scripts.\n * Next.js bundler protection comes from subpath exports (@murumets-ee/core/clients).\n *\n * @typeParam AllFields - The entity's complete field map. Inferred automatically\n * when constructing via `createAdminClient(entity)`.\n */\nexport class AdminClient<\n AllFields extends Record<string, FieldConfig> = Record<string, FieldConfig>,\n> {\n private entity: Entity<AllFields>\n private db: PostgresJsDatabase\n private logger?: Logger | undefined\n /** Public-surface create schema — internal fields excluded. */\n private createSchema: ZodType\n /** Public-surface update schema — internal fields excluded. */\n private updateSchema: ZodType\n /** Trusted-surface update schema — internal fields included. Used by `updateInternal()`. */\n private updateInternalSchema: ZodType\n /** Names of fields marked `internal: true`. Cached for O(1) strip / pick. */\n private internalFieldNames: ReadonlySet<string>\n // biome-ignore lint/suspicious/noExplicitAny: dynamic table columns require PgTableWithColumns<any> for property access\n private table: PgTableWithColumns<any>\n private countCache?: CountCacheLike | undefined\n private contextResolver?: ContextResolver | undefined\n private entityResolver?: EntityResolver | undefined\n private enqueueOnCommit?: BehaviorContext['enqueueOnCommit'] | undefined\n private enqueueOnCommitFactory?: EnqueueOnCommitFactory | undefined\n\n /** Shared context for entity-data-ops functions, bound to the default db. */\n private get ctx(): EntityContext {\n return this.ctxFor(this.db)\n }\n\n /**\n * Build an {@link EntityContext} bound to a specific Drizzle executor.\n *\n * Used by the public CRUD methods so that when the caller threads in\n * `{ tx }` (PR D of PLAN-OUTBOX) every block-load / translation\n * merge / scope-condition lookup hits the SAME tx as the entity write.\n * Without this, a `loadBlocks(this.ctx, ...)` call inside an active tx\n * would query `this.db` (the AdminClient's owned connection) and miss\n * the in-flight INSERT — `entity_refs` / blocks rows wouldn't be\n * visible until commit.\n *\n * For callers that don't pass a tx, `exec === this.db` and the ctx\n * matches today's behaviour exactly.\n */\n private ctxFor(exec: PostgresJsDatabase): EntityContext {\n return {\n entity: this.entity,\n db: exec,\n table: this.table,\n resolveContext: this.contextResolver,\n }\n }\n\n /**\n * Build the `enqueueOnCommit` slot of a {@link BehaviorContext} for\n * the active tx, ready to spread into `resolveAuthContext`'s options\n * (PR D of PLAN-OUTBOX).\n *\n * - When the queue plugin's factory is wired, invoke it with `tx` so\n * the resulting fire-and-forget resolver routes its INSERT through\n * the caller's transaction (atomic with the entity write).\n * - When only the legacy static `enqueueOnCommit` option is set, pass\n * it through verbatim — used by tests and callers that wired their\n * own resolver directly.\n * - When neither is configured (CLI scripts, no queue plugin), return\n * `{}` so the BehaviorContext's `enqueueOnCommit` field stays\n * undefined and behaviors guard accordingly.\n */\n private buildAuthOptions(\n tx: PostgresJsDatabase | undefined,\n ): { enqueueOnCommit?: BehaviorContext['enqueueOnCommit'] } {\n if (this.enqueueOnCommitFactory) {\n return { enqueueOnCommit: this.enqueueOnCommitFactory(tx) }\n }\n if (this.enqueueOnCommit) {\n return { enqueueOnCommit: this.enqueueOnCommit }\n }\n return {}\n }\n\n /**\n * Re-bind `behaviorCtx.enqueueOnCommit` to the supplied executor.\n *\n * Used by `delete` / `deleteMany` when they open an internal tx\n * because no caller tx was supplied: the auth context was resolved\n * BEFORE the tx existed, so its `enqueueOnCommit` (if a factory is\n * wired) was bound to `undefined` and would commit the queue row\n * outside the internal tx. Rebinding here ensures the queue write\n * rolls back with the cascade if anything in `runDelete` throws.\n *\n * Spreads conditionally to satisfy `exactOptionalPropertyTypes` —\n * never assigns `enqueueOnCommit: undefined` (which would clear an\n * existing static resolver if the factory wasn't configured).\n */\n private rebindEnqueueOnCommit(\n ctx: BehaviorContext,\n tx: PostgresJsDatabase | undefined,\n ): BehaviorContext {\n const opts = this.buildAuthOptions(tx)\n if (!opts.enqueueOnCommit) return ctx\n return { ...ctx, enqueueOnCommit: opts.enqueueOnCommit }\n }\n\n constructor(config: AdminClientConfig<AllFields>) {\n // Runtime enforcement: prevent usage in browser contexts\n if (typeof window !== 'undefined') {\n throw new Error(\n 'AdminClient cannot be used in browser code. ' +\n 'Use QueryClient for frontend data access.',\n )\n }\n\n this.entity = config.entity\n this.db = config.db\n this.logger = config.logger\n this.countCache = config.countCache\n this.contextResolver = config.contextResolver\n this.entityResolver = config.entityResolver\n this.enqueueOnCommit = config.enqueueOnCommit\n this.enqueueOnCommitFactory = config.enqueueOnCommitFactory\n\n // Pre-generate validation schemas for performance.\n // Public schemas exclude `internal: true` fields so the public surface\n // (HTTP PATCH, programmatic update()) cannot poison state-machine fields\n // like `_workflowStatus`. Trusted code uses `updateInternal()` which\n // routes through the internal-inclusive schema.\n this.createSchema = generateCreateSchema(config.entity)\n this.updateSchema = generateUpdateSchema(config.entity)\n this.updateInternalSchema = generateUpdateSchema(config.entity, { includeInternal: true })\n\n this.internalFieldNames = new Set(\n Object.entries(config.entity.allFields)\n .filter(([, fieldConfig]) => fieldConfig.internal)\n .map(([name]) => name),\n )\n\n // Get table from schema registry\n const table = schemaRegistry.get(config.entity.name)\n if (!table) {\n throw new Error(\n `Schema for entity '${config.entity.name}' not found in registry. ` +\n 'Ensure schemas are generated and registered before creating AdminClient.',\n )\n }\n this.table = table\n }\n\n /**\n * Strip caller-provided values for internal fields from the input.\n *\n * Returns a NEW object — does not mutate the caller's input. Hooks run\n * AFTER this strip, so they can still set internal fields legitimately;\n * those values are then preserved through validation by `pickInternalFields`.\n */\n private stripCallerInternals(data: Record<string, unknown>): Record<string, unknown> {\n if (this.internalFieldNames.size === 0) return data\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(data)) {\n if (this.internalFieldNames.has(key)) continue\n out[key] = value\n }\n return out\n }\n\n /**\n * Pick the internal-field values that hooks set on `data`.\n * Used to re-attach them after schema validation strips unknown keys.\n */\n private pickInternalFields(data: Record<string, unknown>): Record<string, unknown> {\n if (this.internalFieldNames.size === 0) return {}\n const out: Record<string, unknown> = {}\n for (const key of this.internalFieldNames) {\n if (data[key] !== undefined) out[key] = data[key]\n }\n return out\n }\n\n /**\n * Create a new entity.\n *\n * Flow:\n * 1. Validate input with Zod\n * 2. Execute beforeCreate hooks\n * 3. Prepare data for insert\n * 4. Insert into database\n * 5. Execute afterCreate hooks\n * 6. Shape DTO and return\n *\n * **Caller transaction (`options.tx`)** — PR D of PLAN-OUTBOX. When\n * supplied, the row INSERT, blocks save, refs sync, and any hook-fired\n * `enqueueOnCommit` participate in the caller's transaction:\n *\n * ```ts\n * await db.transaction(async (tx) => {\n * const order = await orders.create({ ... }, { tx })\n * // afterCreate hooks fired with this tx — enqueueOnCommit's INSERT\n * // routes through it. Outer rollback removes both atomically.\n * })\n * ```\n *\n * When omitted, behaviour matches pre-PR-D exactly: each statement\n * auto-commits, hooks run after commit, and the static\n * `enqueueOnCommit` resolver (if any) writes outside the entity tx.\n */\n async create(\n data: InferCreateInput<AllFields>,\n options?: { tx?: PostgresJsDatabase },\n ): Promise<InferEntityDTO<AllFields>> {\n this.logger?.info({ entity: this.entity.name }, 'Creating entity')\n\n const exec = options?.tx ?? this.db\n\n // Permission + scope + behavior context (one resolver call). Pass the\n // caller's tx into `buildAuthOptions` so `behaviorCtx.enqueueOnCommit`\n // routes its INSERT through the same tx as the entity write — the\n // canonical PLAN-OUTBOX §4.2 atomicity guarantee.\n const { scopeId, behaviorCtx } = await resolveAuthContext(\n this.entity,\n this.contextResolver,\n 'create',\n { strictScope: true, ...this.buildAuthOptions(options?.tx) },\n )\n\n // Strip caller-provided internal fields BEFORE hooks. Hooks may then set\n // them legitimately (e.g. workflowable's beforeCreate sets `_workflowStatus`\n // = 'draft'); those values are picked back up after validation.\n let dataToInsert = this.stripCallerInternals(data as Record<string, unknown>)\n\n // Execute beforeCreate hooks BEFORE validation — hooks may populate required fields\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeCreate) {\n const hookResult = await b.hooks.beforeCreate(dataToInsert, behaviorCtx)\n if (hookResult !== undefined) dataToInsert = hookResult\n }\n }\n\n // Preserve server-set fields that hooks added but createSchema omits\n // (e.g. createdAt/updatedAt from timestamped(), createdBy/updatedBy from auditable())\n const serverFields: Record<string, unknown> = {}\n for (const key of ['createdAt', 'updatedAt', 'createdBy', 'updatedBy']) {\n if (dataToInsert[key] !== undefined) serverFields[key] = dataToInsert[key]\n }\n // Preserve internal fields that hooks set (validation schema omits them).\n const internalFields = this.pickInternalFields(dataToInsert)\n\n // Validate after hooks (hooks may add required fields like senderEmail)\n dataToInsert = {\n ...this.createSchema.parse(dataToInsert),\n ...serverFields,\n ...internalFields,\n }\n\n // Auto-set scope ID for scoped entities\n if (scopeId) {\n dataToInsert._scopeId = scopeId\n }\n\n // Prepare data for insert\n const columns = this.prepareDataForInsert(dataToInsert)\n\n // Insert into database\n const [row] = await exec.insert(this.table).values(columns).returning()\n if (!row) throw new Error(`Insert into \"${this.entity.name}\" returned no row`)\n\n // Save blocks to layout table\n await this.saveBlocks(exec, row.id, dataToInsert)\n\n // Track outgoing references (entity_refs)\n await this.syncRefs(exec, row.id as string, dataToInsert, 'create')\n\n // Execute afterCreate hooks\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.afterCreate) await b.hooks.afterCreate(row, behaviorCtx)\n }\n\n // Invalidate count cache after creation. Skip when the caller\n // threaded `{ tx }` — they haven't committed yet, so eager\n // invalidation would re-prime the cache with the pre-tx count.\n this.invalidateCountCache(options?.tx !== undefined)\n\n // Shape DTO and attach blocks. Use a tx-scoped ctx when the caller\n // threaded one in — `loadBlocks` issues a SELECT that must see the\n // INSERTs above, which would be invisible across connections until\n // the caller's tx commits.\n const ctx = this.ctxFor(exec)\n const shaped = shapeDto(this.entity, row) as Record<string, unknown>\n if (shaped && getBlocksFields(ctx).length > 0) {\n const blocksMap = await loadBlocks(ctx, [shaped.id as string], undefined, {\n strictTranslations: true,\n })\n attachBlocks(ctx, [shaped], blocksMap)\n }\n return shaped as InferEntityDTO<AllFields>\n }\n\n /**\n * Find entity by ID.\n *\n * Pass `includeInternal: true` from trusted server code (workflow\n * transitions, behavior implementations) when you need to read fields\n * marked `internal: true` — by default they are stripped from the DTO.\n */\n async findById(\n id: string,\n options?: { locale?: string; defaultLocale?: string; includeInternal?: boolean },\n ): Promise<InferEntityDTO<AllFields> | null> {\n this.logger?.info({ entity: this.entity.name, id }, 'Finding entity by ID')\n\n // Permission + scope (one resolver call; reads don't need strict scope)\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'view')\n\n // Query database (with scope filter if applicable)\n const conditions = [eq(this.table.id, id)]\n if (scopeId) conditions.push(buildScopeCondition(this.ctx, scopeId))\n const [row] = await this.db\n .select()\n .from(this.table)\n .where(and(...conditions))\n\n if (!row) return null\n\n const shapeOpts = options?.includeInternal ? { includeInternal: true } : undefined\n const shaped = shapeDto(this.entity, row, shapeOpts) as Record<string, unknown>\n if (!shaped) return null\n\n // Attach blocks from layout table (with locale for block translations)\n if (getBlocksFields(this.ctx).length > 0) {\n const blocksMap = await loadBlocks(this.ctx, [shaped.id as string], options?.locale, {\n defaultLocale: options?.defaultLocale,\n strictTranslations: true,\n })\n attachBlocks(this.ctx, [shaped], blocksMap)\n }\n\n if (options?.locale) {\n const [merged] = await mergeTranslations(this.ctx, [shaped], options.locale)\n return merged as InferEntityDTO<AllFields>\n }\n return shaped as InferEntityDTO<AllFields>\n }\n\n /**\n * Find multiple entities\n */\n async findMany(options?: FindManyOptions): Promise<InferEntityDTO<AllFields>[]> {\n this.logger?.info({ entity: this.entity.name, options }, 'Finding entities')\n\n // Permission + scope (one resolver call; reads don't need strict scope)\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'view')\n\n // Build and execute query\n let query = this.db.select().from(this.table).$dynamic()\n\n // Collect WHERE conditions\n const conditions: SQL[] = []\n\n // Scope filter\n if (scopeId) conditions.push(buildScopeCondition(this.ctx, scopeId))\n\n if (options?.where) {\n conditions.push(options.where)\n }\n\n // Cursor-based pagination: add keyset WHERE condition (replaces OFFSET)\n if (options?.cursor) {\n // Whitelist: cursor field must be a real column on the entity.\n // `Object.hasOwn` (vs the `in` operator) rejects prototype-chain keys\n // like `__proto__` / `constructor` / `toString`.\n const allFields = getAllFields(this.ctx)\n if (!Object.hasOwn(allFields, options.cursor.field) && options.cursor.field !== 'id') {\n throw new Error(\n `Invalid cursor field: '${options.cursor.field}' is not a field on '${this.entity.name}'`,\n )\n }\n const cursorCondition = buildCursorCondition(this.table, options.cursor)\n if (cursorCondition) {\n conditions.push(cursorCondition)\n }\n }\n\n if (conditions.length > 0) {\n query = query.where(and(...conditions))\n }\n\n if (options?.limit) {\n query = query.limit(options.limit)\n }\n // Cursor takes precedence over offset — skip offset when cursor is active\n if (options?.offset && !options?.cursor) {\n query = query.offset(options.offset)\n }\n if (options?.orderBy) {\n const cols = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]\n query = query.orderBy(...cols)\n }\n\n const rows = await query\n\n const shapeOpts = options?.includeInternal ? { includeInternal: true } : undefined\n const shaped = shapeDtos(this.entity, rows, shapeOpts).filter(\n (e): e is NonNullable<typeof e> => e !== null,\n ) as Record<string, unknown>[]\n\n // Attach blocks from layout table (with locale for block translations)\n if (getBlocksFields(this.ctx).length > 0 && shaped.length > 0) {\n const entityIds = shaped.map((e) => e.id as string)\n const blocksMap = await loadBlocks(this.ctx, entityIds, options?.locale, {\n defaultLocale: options?.defaultLocale,\n strictTranslations: true,\n })\n attachBlocks(this.ctx, shaped, blocksMap)\n }\n\n if (options?.locale) {\n return (await mergeTranslations(\n this.ctx,\n shaped,\n options.locale,\n )) as InferEntityDTO<AllFields>[]\n }\n\n return shaped as InferEntityDTO<AllFields>[]\n }\n\n /**\n * Count entities matching optional conditions\n */\n async count(options?: CountOptions): Promise<number> {\n this.logger?.info({ entity: this.entity.name, options }, 'Counting entities')\n\n // Permission + scope (one resolver call; reads don't need strict scope)\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'view')\n\n // Build cache key (includes scopeId for per-tenant isolation)\n const cacheKey = buildCountCacheKey(this.entity.name, options?.where, undefined, scopeId)\n\n const compute = async (): Promise<number> => {\n let query = this.db.select({ count: sql<number>`count(*)` }).from(this.table).$dynamic()\n const conditions: SQL[] = []\n if (scopeId) conditions.push(buildScopeCondition(this.ctx, scopeId))\n if (options?.where) conditions.push(options.where)\n if (conditions.length > 0) query = query.where(and(...conditions))\n const [result] = await query\n if (!result) throw new Error(`count query returned no row for ${this.entity.name}`)\n return Number(result.count)\n }\n\n // No cache configured → run the query directly (back-compat).\n if (!this.countCache) return compute()\n\n // Single-flight: concurrent callers crossing the TTL gap share one query.\n return this.countCache.getOrCompute(cacheKey, compute)\n }\n\n /**\n * Update entity by ID\n *\n * Flow:\n * 1. Validate input with Zod\n * 2. Execute beforeUpdate hooks\n * 3. Prepare data for update\n * 4. Update in database\n * 5. Execute afterUpdate hooks\n * 6. Shape DTO and return\n */\n async update(\n id: string,\n data: InferUpdateInput<AllFields>,\n options?: { locale?: string; tx?: PostgresJsDatabase },\n ): Promise<InferEntityDTO<AllFields>> {\n return this.updateImpl(id, data as Record<string, unknown>, options, {\n allowInternal: false,\n })\n }\n\n /**\n * Update entity by ID, allowing writes to fields marked `internal: true`.\n *\n * Use this from trusted server code that has already authorized the\n * transition out-of-band — typical example: workflow transitions invoked\n * from an admin route that has already checked the `publish` permission.\n * The HTTP `PATCH` surface uses the public {@link update} method, which\n * silently strips internal fields so untrusted callers cannot poison\n * state-machine values like `_workflowStatus`.\n *\n * The validation schema for this method INCLUDES internal fields — values\n * are still type-checked and constrained (e.g. select-field options).\n *\n * **Security**: never call this from a code path that forwards request body\n * fields directly. The caller must construct the internal-field values\n * server-side from authorized state transitions.\n */\n async updateInternal(\n id: string,\n data: Record<string, unknown>,\n options?: { locale?: string; tx?: PostgresJsDatabase },\n ): Promise<InferEntityDTO<AllFields>> {\n return this.updateImpl(id, data, options, { allowInternal: true })\n }\n\n private async updateImpl(\n id: string,\n data: Record<string, unknown>,\n options: { locale?: string; tx?: PostgresJsDatabase } | undefined,\n flags: { allowInternal: boolean },\n ): Promise<InferEntityDTO<AllFields>> {\n this.logger?.info(\n { entity: this.entity.name, id, internal: flags.allowInternal || undefined },\n 'Updating entity',\n )\n\n const exec = options?.tx ?? this.db\n\n // Permission + scope + behavior context (one resolver call; strict scope for writes).\n // Pass the caller's tx into `buildAuthOptions` so hook-fired `enqueueOnCommit`\n // INSERTs route through the same tx as the entity write (PR D of PLAN-OUTBOX).\n const { scopeId, behaviorCtx } = await resolveAuthContext(\n this.entity,\n this.contextResolver,\n 'update',\n { strictScope: true, ...this.buildAuthOptions(options?.tx) },\n )\n\n // Strip caller-provided internal fields on the public path. Trusted\n // callers using updateInternal() bypass the strip — they're presumed\n // to have authorized the internal transition out-of-band.\n let dataToUpdate = flags.allowInternal ? data : this.stripCallerInternals(data)\n\n // Eagerly load the pre-update row and bind it to a memoized loader.\n // Doing this BEFORE the DB write guarantees `loadCurrent()` returns the\n // pre-update snapshot from both `beforeUpdate` AND `afterUpdate` —\n // making the contract documented on `BehaviorContext.loadCurrent`\n // unconditional rather than \"if a sibling hook primed the cache.\"\n //\n // Cost: one extra `findById` per update on the cold path. Updates are\n // not a hot path in this codebase (admin write surface); the\n // consistency win — no module-level Maps to track pre-update state,\n // no foot-gun for future hook authors — is worth the read.\n //\n // The `scopeId` MUST be threaded in here — the eventual UPDATE below\n // is scope-gated via its WHERE, but if we let the pre-load see\n // cross-tenant rows, hooks with side effects (audit logs, system\n // messages, projection enqueues) would leak data even when the\n // gated UPDATE legitimately matches no row.\n const cachedCurrent = (await this.findByIdInternal(exec, id, scopeId)) as Record<\n string,\n unknown\n > | null\n const loadCurrent = async (): Promise<Record<string, unknown> | null> => cachedCurrent\n const hookCtx = {\n ...behaviorCtx,\n loadCurrent,\n viaInternal: flags.allowInternal,\n }\n\n // Execute beforeUpdate hooks BEFORE validation — hooks may modify fields\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeUpdate) {\n const hookResult = await b.hooks.beforeUpdate(id, dataToUpdate, hookCtx)\n if (hookResult !== undefined) dataToUpdate = hookResult\n }\n }\n\n // Preserve server-set fields that hooks added but updateSchema omits\n // (e.g. updatedAt from timestamped(), updatedBy from auditable()).\n const serverFields: Record<string, unknown> = {}\n for (const key of ['updatedAt', 'updatedBy']) {\n if (dataToUpdate[key] !== undefined) serverFields[key] = dataToUpdate[key]\n }\n\n // Validate. The internal-inclusive schema is used when the caller is\n // trusted (updateInternal); the public schema is used otherwise — its\n // strip behavior also drops any caller-internals that survived (defense\n // in depth alongside `stripCallerInternals`).\n const schema = flags.allowInternal ? this.updateInternalSchema : this.updateSchema\n const internalFields = flags.allowInternal ? {} : this.pickInternalFields(dataToUpdate)\n dataToUpdate = { ...schema.parse(dataToUpdate), ...serverFields, ...internalFields }\n\n // Prepare data for update\n const columns = this.prepareDataForUpdate(dataToUpdate)\n\n // Build WHERE with scope filter\n const idCondition = eq(this.table.id, id)\n const updateWhere = scopeId\n ? (and(idCondition, buildScopeCondition(this.ctx, scopeId)) ?? idCondition)\n : idCondition\n\n // Update in database (only if there are non-blocks fields to update)\n let row: Record<string, unknown>\n if (Object.keys(columns).length > 0) {\n const [updated] = await exec.update(this.table).set(columns).where(updateWhere).returning()\n if (!updated) throw new Error(`Update of \"${this.entity.name}\" id=${id} returned no row`)\n row = updated\n } else {\n // Only blocks changed — fetch current row\n const [current] = await exec.select().from(this.table).where(updateWhere)\n if (!current) throw new Error(`\"${this.entity.name}\" row not found for id=${id}`)\n row = current\n }\n\n // Update blocks in layout table (thread locale for per-locale blocks)\n await this.saveBlocks(exec, id, dataToUpdate, options)\n\n // Update outgoing references (entity_refs) — only for changed fields\n await this.syncRefs(exec, id, dataToUpdate, 'update')\n\n // Execute afterUpdate hooks. Pass `hookCtx` (not the bare `behaviorCtx`)\n // so afterUpdate can call `ctx.loadCurrent()` and get the pre-update\n // row — `loadCurrent` is memoized once `beforeUpdate` has primed the\n // cache, which is the only reliable way for an `afterUpdate` hook to\n // diff old vs new state without a module-level WeakMap.\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.afterUpdate) await b.hooks.afterUpdate(row, hookCtx)\n }\n\n // Shape DTO and attach blocks. Use a tx-scoped ctx so `loadBlocks`\n // sees the in-flight UPDATE/INSERTs above, which would otherwise be\n // invisible across connections until the caller's tx commits.\n const ctx = this.ctxFor(exec)\n const shapeOpts = flags.allowInternal ? { includeInternal: true } : undefined\n const shaped = shapeDto(this.entity, row, shapeOpts) as Record<string, unknown>\n if (shaped && getBlocksFields(ctx).length > 0) {\n const blocksMap = await loadBlocks(ctx, [id], undefined, { strictTranslations: true })\n attachBlocks(ctx, [shaped], blocksMap)\n }\n return shaped as InferEntityDTO<AllFields>\n }\n\n /**\n * Internal `findById` variant that issues its SELECT through the\n * caller-supplied executor. Used by `updateImpl` so the pre-update\n * snapshot reflects any earlier writes the caller already made\n * inside the same transaction.\n *\n * **Scope is NOT optional** — it must be the same `scopeId` that\n * `resolveAuthContext` produced for the surrounding `update` call.\n * The eventual UPDATE is scope-gated via its WHERE clause; if this\n * pre-load skipped scope, hooks with side effects (audit logs,\n * system-message creation, outbox enqueues) would observe and\n * dispatch on cross-tenant rows even when the gated UPDATE matches\n * nothing. See HIGH finding in pr-review for #247.\n *\n * Permission check is skipped because `resolveAuthContext` ran first\n * in the surrounding `update` call.\n */\n private async findByIdInternal(\n exec: PostgresJsDatabase,\n id: string,\n scopeId: string | undefined,\n ): Promise<InferEntityDTO<AllFields> | null> {\n const ctx = this.ctxFor(exec)\n const conditions = [eq(this.table.id, id)]\n if (scopeId) conditions.push(buildScopeCondition(ctx, scopeId))\n const [row] = await exec\n .select()\n .from(this.table)\n .where(and(...conditions))\n if (!row) return null\n const shaped = shapeDto(this.entity, row) as Record<string, unknown>\n if (!shaped) return null\n if (getBlocksFields(ctx).length > 0) {\n const blocksMap = await loadBlocks(ctx, [shaped.id as string], undefined, {\n strictTranslations: true,\n })\n attachBlocks(ctx, [shaped], blocksMap)\n }\n return shaped as InferEntityDTO<AllFields>\n }\n\n /**\n * Delete entity by ID.\n *\n * Wraps cascade + delete in a transaction so cascaded deletes are rolled\n * back if the final entity delete fails (no partial data loss). When\n * the caller threads in `{ tx }` (PR D of PLAN-OUTBOX), the same tx\n * is reused — the inner `db.transaction(...)` call is replaced with a\n * direct invocation of the body, and any hook-fired `enqueueOnCommit`\n * INSERT routes through the caller's tx so an outer rollback removes\n * everything atomically.\n *\n * Checks entity_refs for incoming references first — throws\n * ReferencedEntityError if this entity is still used somewhere.\n */\n async delete(id: string, options?: { tx?: PostgresJsDatabase }): Promise<void> {\n this.logger?.info({ entity: this.entity.name, id }, 'Deleting entity')\n\n // Permission + scope + behavior context (one resolver call; strict scope for writes)\n const { scopeId, behaviorCtx } = await resolveAuthContext(\n this.entity,\n this.contextResolver,\n 'delete',\n { strictScope: true, ...this.buildAuthOptions(options?.tx) },\n )\n\n // Verify entity is in current scope before deleting. Use the caller's\n // tx if supplied so an in-flight write to the row is visible.\n const exec = options?.tx ?? this.db\n if (scopeId) {\n const [row] = await exec\n .select({ id: this.table.id })\n .from(this.table)\n .where(and(eq(this.table.id, id), buildScopeCondition(this.ctx, scopeId)))\n if (!row) return // Not found in this scope — no-op\n }\n\n const runDelete = async (tx: PostgresJsDatabase): Promise<void> => {\n // Re-bind `enqueueOnCommit` to the active tx. When the caller\n // supplied `{ tx }`, `behaviorCtx` already routes through it.\n // When we opened our own internal tx (the no-caller-tx path),\n // the original `behaviorCtx.enqueueOnCommit` was bound with\n // `undefined` — calling it would commit the queue row outside\n // this tx, breaking PLAN-OUTBOX §4.2 atomicity if the cascade\n // rolls back. Rebuilding here keeps the semantics consistent\n // across both paths.\n const txCtx = this.rebindEnqueueOnCommit(behaviorCtx, tx)\n\n // Handle incoming references — cascade or restrict based on field config\n await this.handleIncomingRefsForDelete(tx, [id])\n\n // Execute beforeDelete hooks\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeDelete) await b.hooks.beforeDelete(id, txCtx)\n }\n\n // Delete from database\n await tx.delete(this.table).where(eq(this.table.id, id))\n\n // Clean up outgoing references from entity_refs\n await tx\n .delete(entityRefs)\n .where(and(eq(entityRefs.sourceEntity, this.entity.name), eq(entityRefs.sourceId, id)))\n }\n\n // Reuse the caller's tx when supplied — an outer rollback removes\n // both the entity-side delete AND any hook-fired enqueueOnCommit.\n // Otherwise open a new tx so cascade delete keeps today's\n // single-call atomicity.\n if (options?.tx) await runDelete(options.tx)\n else await this.db.transaction(runDelete)\n\n // Execute afterDelete hooks (outside transaction — side effects should not block commit)\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.afterDelete) await b.hooks.afterDelete(id, behaviorCtx)\n }\n\n // Invalidate count cache after deletion. Skip when caller-tx is\n // in flight — see `invalidateCountCache` JSDoc.\n this.invalidateCountCache(options?.tx !== undefined)\n }\n\n /**\n * Delete multiple entities matching a WHERE condition.\n *\n * Wraps cascade + delete in a transaction so cascaded deletes are rolled\n * back if the final entity delete fails (no partial data loss).\n *\n * Respects `onDelete` config on referencing fields:\n * - `cascade`: recursively deletes referencing entities\n * - `set-null`: nullifies the reference column\n * - `restrict` (default): blocks deletion if references exist\n *\n * Runs beforeDelete/afterDelete hooks for each entity.\n *\n * @returns Number of rows deleted\n */\n async deleteMany(where: SQL, options?: { tx?: PostgresJsDatabase }): Promise<number> {\n this.logger?.info({ entity: this.entity.name }, 'Bulk deleting entities')\n\n // Permission + scope + behavior context (one resolver call; strict scope for writes)\n const { scopeId, behaviorCtx } = await resolveAuthContext(\n this.entity,\n this.contextResolver,\n 'delete',\n { strictScope: true, ...this.buildAuthOptions(options?.tx) },\n )\n\n const exec = options?.tx ?? this.db\n\n // Get IDs first — needed for ref handling and hooks (scoped to current tenant)\n const deleteConditions = [where]\n if (scopeId) deleteConditions.push(buildScopeCondition(this.ctx, scopeId))\n const rows = await exec\n .select({ id: this.table.id })\n .from(this.table)\n .where(and(...deleteConditions))\n const ids = rows.map((r) => (r as Record<string, unknown>).id as string)\n if (ids.length === 0) return 0\n\n const runDeleteMany = async (tx: PostgresJsDatabase): Promise<void> => {\n // Same rebinding as `delete()` — when we opened our own internal\n // tx, the original `behaviorCtx.enqueueOnCommit` was bound with\n // `undefined`. Rebind to the active tx so hook-fired enqueues\n // commit (or roll back) atomically with the cascade.\n const txCtx = this.rebindEnqueueOnCommit(behaviorCtx, tx)\n\n // Handle incoming references — cascade or restrict\n await this.handleIncomingRefsForDelete(tx, ids)\n\n // Run beforeDelete hooks\n for (const id of ids) {\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeDelete) await b.hooks.beforeDelete(id, txCtx)\n }\n }\n\n // Delete from database\n await tx.delete(this.table).where(inArray(this.table.id, ids))\n\n // Clean up outgoing references from entity_refs\n await tx\n .delete(entityRefs)\n .where(\n and(eq(entityRefs.sourceEntity, this.entity.name), inArray(entityRefs.sourceId, ids)),\n )\n }\n\n if (options?.tx) await runDeleteMany(options.tx)\n else await this.db.transaction(runDeleteMany)\n\n // Run afterDelete hooks (outside transaction — side effects should not block commit)\n for (const id of ids) {\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.afterDelete) await b.hooks.afterDelete(id, behaviorCtx)\n }\n }\n\n this.invalidateCountCache(options?.tx !== undefined)\n return ids.length\n }\n\n /** Maximum cascade depth to prevent infinite recursion from circular references. */\n private static readonly MAX_CASCADE_DEPTH = 10\n\n /**\n * Handle incoming references before deleting entities.\n *\n * For each referencing entity/field:\n * - `onDelete: 'cascade'` → recursively delete referencing entities\n * - `onDelete: 'set-null'` → nullify the FK column on referencing rows\n * - `onDelete: 'restrict'` → throw ReferencedEntityError\n *\n * Looks up referencing entity definitions from the app's entity registry\n * to determine the onDelete strategy. Falls back to 'restrict' if the\n * entity registry is unavailable.\n *\n * @param tx - Transaction handle for atomicity (cascade + delete in same tx)\n * @param ids - Entity IDs being deleted\n * @param depth - Current recursion depth (guards against circular references)\n */\n private async handleIncomingRefsForDelete(\n tx: PostgresJsDatabase,\n ids: string[],\n depth = 0,\n ): Promise<void> {\n if (depth >= AdminClient.MAX_CASCADE_DEPTH) {\n throw new Error(\n `Cascade delete exceeded maximum depth of ${AdminClient.MAX_CASCADE_DEPTH} ` +\n `while deleting '${this.entity.name}'. This likely indicates a circular reference chain.`,\n )\n }\n\n // Find all incoming references for all IDs in a single query\n const usages = await tx\n .select({\n sourceEntity: entityRefs.sourceEntity,\n sourceId: entityRefs.sourceId,\n sourceField: entityRefs.sourceField,\n })\n .from(entityRefs)\n .where(and(eq(entityRefs.targetEntity, this.entity.name), inArray(entityRefs.targetId, ids)))\n\n if (usages.length === 0) return\n\n // Group usages by source entity + field to determine strategy per group\n const groups = new Map<\n string,\n { sourceEntity: string; sourceField: string; sourceIds: string[] }\n >()\n for (const u of usages) {\n const key = `${u.sourceEntity}:${u.sourceField}`\n let group = groups.get(key)\n if (!group) {\n group = { sourceEntity: u.sourceEntity, sourceField: u.sourceField, sourceIds: [] }\n groups.set(key, group)\n }\n group.sourceIds.push(u.sourceId)\n }\n\n // Look up entity registry for onDelete config. When no resolver is\n // configured, all incoming refs are treated as `restrict`.\n const entityMap = this.entityResolver?.()\n\n for (const [, group] of groups) {\n const refEntity = entityMap?.get(group.sourceEntity)\n const refField = refEntity?.fields[group.sourceField] as\n | { type: string; onDelete?: string }\n | undefined\n const strategy = refField?.onDelete ?? 'restrict'\n\n if (strategy === 'restrict') {\n const firstId = ids[0]\n if (!firstId) throw new Error('expected at least one id')\n throw new ReferencedEntityError(\n this.entity.name,\n firstId,\n group.sourceIds.map((sid) => ({\n sourceEntity: group.sourceEntity,\n sourceId: sid,\n sourceField: group.sourceField,\n })),\n )\n }\n\n if (strategy === 'cascade') {\n // Recursively delete referencing entities via their own AdminClient.\n // Propagates contextResolver so permission + scope checks apply to the\n // referenced entity too — deleting an Article shouldn't silently delete\n // Comments the caller has no permission for, nor cross-tenant comments.\n const sourceTable = schemaRegistry.get(group.sourceEntity)\n if (sourceTable && refEntity) {\n const sourceClient = new AdminClient({\n entity: refEntity,\n db: tx,\n logger: this.logger,\n contextResolver: this.contextResolver,\n ...(this.enqueueOnCommit !== undefined && { enqueueOnCommit: this.enqueueOnCommit }),\n ...(this.enqueueOnCommitFactory !== undefined && {\n enqueueOnCommitFactory: this.enqueueOnCommitFactory,\n }),\n })\n // Enforce 'delete' permission on the referenced entity before recursing\n // (one resolver call covers permission + scope + behavior context).\n // Forward `enqueueOnCommit` bound to the cascade tx so behaviors on\n // the referenced entity enqueue jobs that commit (or roll back) with\n // the surrounding cascade.\n const refAuth = await resolveAuthContext(\n refEntity,\n this.contextResolver,\n 'delete',\n this.buildAuthOptions(tx),\n )\n let sourceWhere: SQL = inArray(sourceTable.id, group.sourceIds)\n if (refAuth.scopeId) {\n const scopedCtx: EntityContext = {\n entity: refEntity,\n db: tx,\n table: sourceTable,\n resolveContext: this.contextResolver,\n }\n const scoped = and(sourceWhere, buildScopeCondition(scopedCtx, refAuth.scopeId))\n if (scoped) sourceWhere = scoped\n }\n await sourceClient.deleteManyInTx(tx, sourceWhere, depth + 1, refAuth.behaviorCtx)\n }\n } else if (strategy === 'set-null') {\n // Nullify the FK column on referencing rows.\n // Enforce 'update' permission + scope on the referenced entity — setting\n // a FK to null is a mutation the caller may not be authorized for.\n const sourceTable = schemaRegistry.get(group.sourceEntity)\n if (sourceTable && refEntity) {\n const col = sourceTable[group.sourceField]\n if (col) {\n const refAuth = await resolveAuthContext(\n refEntity,\n this.contextResolver,\n 'update',\n this.buildAuthOptions(tx),\n )\n const sourceScopeId = refAuth.scopeId\n let updateWhere: SQL = inArray(sourceTable.id, group.sourceIds)\n if (sourceScopeId) {\n const scopedCtx: EntityContext = {\n entity: refEntity,\n db: tx,\n table: sourceTable,\n resolveContext: this.contextResolver,\n }\n const scoped = and(updateWhere, buildScopeCondition(scopedCtx, sourceScopeId))\n if (scoped) updateWhere = scoped\n }\n await tx\n .update(sourceTable)\n .set({ [group.sourceField]: null })\n .where(updateWhere)\n // Clean up the ref rows\n await tx\n .delete(entityRefs)\n .where(\n and(\n eq(entityRefs.sourceEntity, group.sourceEntity),\n inArray(entityRefs.sourceId, group.sourceIds),\n eq(entityRefs.sourceField, group.sourceField),\n ),\n )\n }\n }\n }\n }\n }\n\n /**\n * Internal: delete within an existing transaction (used by cascade).\n * Skips wrapping in a new transaction — the caller already has one.\n *\n * Receives `behaviorCtx` from the caller — auth was already enforced on\n * this entity by the cascade orchestrator, so no extra resolver call here.\n */\n private async deleteManyInTx(\n tx: PostgresJsDatabase,\n where: SQL,\n depth: number,\n behaviorCtx: BehaviorContext,\n ): Promise<number> {\n const rows = await tx.select({ id: this.table.id }).from(this.table).where(where)\n const ids = rows.map((r) => (r as Record<string, unknown>).id as string)\n if (ids.length === 0) return 0\n\n await this.handleIncomingRefsForDelete(tx, ids, depth)\n\n for (const id of ids) {\n for (const b of this.entity.behaviors ?? []) {\n if (b.hooks?.beforeDelete) await b.hooks.beforeDelete(id, behaviorCtx)\n }\n }\n\n await tx.delete(this.table).where(inArray(this.table.id, ids))\n await tx\n .delete(entityRefs)\n .where(and(eq(entityRefs.sourceEntity, this.entity.name), inArray(entityRefs.sourceId, ids)))\n\n // afterDelete hooks run outside the caller's transaction scope\n // (the caller — deleteMany — handles them after tx commits)\n\n // `deleteManyInTx` ALWAYS runs inside an outer tx (cascade path).\n // Skip cache invalidation here; the outer `delete` / `deleteMany`\n // handles it after its own tx resolves.\n this.invalidateCountCache(true)\n return ids.length\n }\n\n // -------------------------------------------------------------------------\n // Bulk operations\n // -------------------------------------------------------------------------\n\n /**\n * Update multiple entities matching a WHERE condition.\n *\n * Unlike `update(id, data)`, this does NOT run hooks (beforeUpdate/afterUpdate),\n * does NOT validate with Zod, and does NOT update blocks/translations.\n * It's a direct bulk column update — use for operational changes like\n * status transitions, assignments, and cleanup jobs.\n *\n * @returns Number of rows updated\n *\n * @example Close all resolved tickets older than 30 days\n * ```ts\n * const closed = await ticketClient.updateMany(\n * and(eq(table.status, 'resolved'), lte(table.updatedAt, cutoff)),\n * { status: 'closed', updatedAt: new Date() },\n * )\n * ```\n *\n * @example Cascade materialized path updates with SQL expressions\n * ```ts\n * const t = taxonomyClient.getTable()\n * await taxonomyClient.updateMany(\n * like(t.path, `${oldPath}/%`),\n * {},\n * {\n * expressions: {\n * path: sql`replace(${t.path}, ${oldPath}, ${newPath})`,\n * depth: sql`length(replace(${t.path}, ${oldPath}, ${newPath}))\n * - length(replace(replace(${t.path}, ${oldPath}, ${newPath}), '/', ''))`,\n * },\n * },\n * )\n * ```\n */\n async updateMany(\n where: SQL,\n data: Partial<InferUpdateInput<AllFields>>,\n options?: {\n expressions?: Record<string, SQL>\n allowInternal?: boolean\n tx?: PostgresJsDatabase\n },\n ): Promise<number> {\n this.logger?.info({ entity: this.entity.name }, 'Bulk updating entities')\n\n // Permission + scope (one resolver call; strict scope for writes).\n // Thread tx into `buildAuthOptions` for consistency with the other\n // mutation paths — `updateMany` itself does not run hooks today,\n // so `behaviorCtx.enqueueOnCommit` is unused here, but keeping the\n // wiring uniform avoids a foot-gun if a future hook surface is\n // added at this layer.\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'update', {\n strictScope: true,\n ...this.buildAuthOptions(options?.tx),\n })\n\n const exec = options?.tx ?? this.db\n\n // Strip caller-provided internal fields on the public path. Trusted\n // callers using `allowInternal: true` bypass the strip — same contract\n // as `update()` vs `updateInternal()`, applied here at the bulk surface.\n const safeData = options?.allowInternal\n ? (data as Record<string, unknown>)\n : this.stripCallerInternals(data as Record<string, unknown>)\n\n // Validate data shape. Internal-inclusive schema only on the trusted path.\n const schema = options?.allowInternal ? this.updateInternalSchema : this.updateSchema\n const validated = schema.parse(safeData)\n const columns = this.prepareDataForUpdate(validated as Record<string, unknown>)\n\n // Merge SQL expressions — bypass validation (computed values, not user input).\n // Guard against silent clobbering: reject collisions, unknown columns, AND\n // (on the public path) any internal-flagged columns. Otherwise a caller could\n // poison a state-machine field via `expressions: { _workflowStatus: sql\\`'approved'\\` }`,\n // bypassing the same boundary the public `update()` installs.\n if (options?.expressions) {\n const allFields = getAllFields(this.ctx)\n for (const [key, expr] of Object.entries(options.expressions)) {\n if (Object.hasOwn(columns, key)) {\n throw new Error(\n `updateMany: field '${key}' appears in both \\`data\\` and \\`expressions\\`. ` +\n `Choose one — expressions silently overriding validated values is unsafe.`,\n )\n }\n // Allow real entity fields + the infrastructure _scopeId column.\n // `Object.hasOwn` blocks prototype-chain keys (`__proto__`, …).\n if (!Object.hasOwn(allFields, key) && key !== '_scopeId') {\n throw new Error(\n `updateMany.expressions: unknown column '${key}' on entity '${this.entity.name}'`,\n )\n }\n // Block internal fields unless trusted-caller opt-in.\n if (!options.allowInternal && this.internalFieldNames.has(key)) {\n throw new Error(\n `updateMany.expressions: '${key}' is internal — pass \\`allowInternal: true\\` ` +\n `from a trusted server context that has authorized the transition out-of-band.`,\n )\n }\n columns[key] = expr\n }\n }\n\n const updateConditions = [where]\n if (scopeId) updateConditions.push(buildScopeCondition(this.ctx, scopeId))\n const result = await exec\n .update(this.table)\n .set(columns)\n .where(and(...updateConditions))\n .returning({ id: this.table.id })\n\n this.invalidateCountCache(options?.tx !== undefined)\n return result.length\n }\n\n // -------------------------------------------------------------------------\n // Aggregation\n // -------------------------------------------------------------------------\n\n /**\n * Run an aggregate query on this entity's table.\n *\n * Supports GROUP BY with standard aggregate functions (count, sum, avg,\n * min, max). This is the entity-level equivalent of Drupal's\n * `EntityQueryAggregate` — standardized stats without raw SQL.\n *\n * @example Count tickets by status\n * ```ts\n * const stats = await ticketClient.aggregate({\n * select: { count: sql<number>`count(*)::int` },\n * groupBy: [table.status],\n * })\n * // → [{ status: 'open', count: 12 }, { status: 'closed', count: 45 }]\n * ```\n *\n * @example Dashboard stats with conditional counts\n * ```ts\n * const [stats] = await ticketClient.aggregate({\n * select: {\n * open: sql<number>`count(case when ${table.status} = ${'open'} then 1 end)::int`,\n * pending: sql<number>`count(case when ${table.status} = ${'pending'} then 1 end)::int`,\n * },\n * })\n * ```\n */\n async aggregate<TResult extends Record<string, unknown> = Record<string, unknown>>(options: {\n /** Named select expressions — each key becomes an output column. Use `sql<T>` for aggregates. */\n select: Record<string, SQL | ReturnType<typeof eq>>\n /** Columns to GROUP BY. */\n groupBy?: SQL[]\n /** WHERE filter applied before aggregation. */\n where?: SQL\n /** ORDER BY for the result. */\n orderBy?: SQL | SQL[]\n /** Limit the number of rows returned. */\n limit?: number\n }): Promise<TResult[]> {\n this.logger?.info({ entity: this.entity.name }, 'Aggregate query')\n\n // Permission + scope (one resolver call; reads don't need strict scope)\n const { scopeId } = await resolveAuthContext(this.entity, this.contextResolver, 'view')\n\n let query = this.db.select(options.select).from(this.table).$dynamic()\n\n const conditions: SQL[] = []\n if (scopeId) conditions.push(buildScopeCondition(this.ctx, scopeId))\n if (options.where) conditions.push(options.where)\n if (conditions.length > 0) query = query.where(and(...conditions))\n if (options.groupBy && options.groupBy.length > 0) {\n query = query.groupBy(...options.groupBy)\n }\n if (options.orderBy) {\n const cols = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy]\n query = query.orderBy(...cols)\n }\n if (options.limit) {\n query = query.limit(options.limit)\n }\n\n return (await query) as TResult[]\n }\n\n /**\n * Expose the underlying Drizzle table for typed Drizzle queries.\n *\n * Use this when you need column references for `.where()`, `.orderBy()`,\n * aggregate expressions, or JOINs with other entity tables. This is the\n * standardized way to access entity columns — never use string table names\n * or `sql.identifier()`.\n *\n * @example\n * ```ts\n * const t = ticketClient.getTable()\n * const stats = await ticketClient.aggregate({\n * select: { count: sql<number>`count(*)::int` },\n * groupBy: [t.status],\n * })\n * ```\n */\n // biome-ignore lint/suspicious/noExplicitAny: entity tables have dynamic columns not known at compile time\n getTable(): PgTableWithColumns<any> {\n return this.table\n }\n\n /**\n * Expose the database handle for sibling infrastructure.\n *\n * Wrapper clients (MediaClient, ContentClient, TaxonomyClient) use this\n * to create `TableClient` instances for infrastructure tables (versions,\n * drafts, locks) that sit alongside entity tables. Not intended for\n * end-user code — wrapper packages are trusted consumers.\n */\n getDb(): PostgresJsDatabase {\n return this.db\n }\n\n /**\n * Prepare data for insert — every field is a real column.\n */\n private prepareDataForInsert(data: Record<string, unknown>): Record<string, unknown> {\n const columns: Record<string, unknown> = {}\n\n for (const [fieldName, value] of Object.entries(data)) {\n const fieldConfig = getAllFields(this.ctx)[fieldName]\n if (!fieldConfig) continue\n if (fieldName === 'id') continue\n if (fieldConfig.type === 'blocks') continue\n columns[fieldName] = value\n }\n\n // Pass through infrastructure column (not in allFields, added by schema generator)\n if (data._scopeId !== undefined) columns._scopeId = data._scopeId\n\n return columns\n }\n\n /**\n * Prepare data for update — every field is a real column.\n * Standard column SET, no JSONB merge needed.\n */\n private prepareDataForUpdate(data: Record<string, unknown>): Record<string, unknown> {\n const columns: Record<string, unknown> = {}\n\n for (const [fieldName, value] of Object.entries(data)) {\n const fieldConfig = getAllFields(this.ctx)[fieldName]\n if (!fieldConfig) continue\n if (fieldName === 'id') continue\n if (fieldConfig.type === 'blocks') continue\n columns[fieldName] = value\n }\n\n return columns\n }\n\n /**\n * Save translation for an entity.\n * Each translatable field is a real column on the translation table.\n *\n * **Caller transaction (`options.tx`)** — Phase 4b of PLAN-OUTBOX. When\n * supplied, the upsert participates in the caller's transaction so a\n * caller wrapping `entity.create + saveTranslation` in a `db.transaction`\n * gets atomic visibility — outer rollback removes BOTH the entity row\n * and the translation row, never leaves an orphan translation pointing\n * at a now-deleted entity. Without it, the translation would auto-commit\n * regardless of the outer rollback. No hooks fire on this method, so no\n * factory plumbing is needed — just routing the SQL through `exec`.\n */\n async saveTranslation(\n entityId: string,\n locale: string,\n translations: Record<string, unknown>,\n options?: { tx?: PostgresJsDatabase },\n ): Promise<void> {\n await assertEntityAccess(this.entity, this.contextResolver, 'update')\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Saving translation')\n\n const exec = options?.tx ?? this.db\n const translationTableName = `${this.entity.name}_translations`\n const translationTable = schemaRegistry.get(translationTableName)\n\n if (!translationTable) {\n throw new Error(`Translation table ${translationTableName} not found in schema registry`)\n }\n\n // Get translatable field names\n const translatableFields = Object.entries(getAllFields(this.ctx))\n .filter(([_, config]) => config.translatable)\n .map(([name]) => name)\n\n if (translatableFields.length === 0) {\n throw new Error(`Entity ${this.entity.name} has no translatable fields`)\n }\n\n // Extract only translatable fields — these are real columns on the translation table\n const translationData: Record<string, unknown> = { entityId, locale }\n const updateData: Record<string, unknown> = {}\n for (const fieldName of translatableFields) {\n if (translations[fieldName] !== undefined) {\n translationData[fieldName] = translations[fieldName]\n updateData[fieldName] = translations[fieldName]\n }\n }\n\n // Upsert: insert or update on conflict\n await exec\n .insert(translationTable)\n .values(translationData)\n .onConflictDoUpdate({\n target: [translationTable.entityId, translationTable.locale],\n set: updateData,\n })\n\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Translation saved')\n }\n\n /**\n * Delete translation(s) for an entity. If `locale` is provided, deletes\n * the specific locale row; otherwise deletes every translation row for\n * the entity.\n *\n * **Caller transaction (`options.tx`)** — Phase 4b of PLAN-OUTBOX. Same\n * shape as `saveTranslation` — when supplied, the DELETE participates\n * in the caller's tx.\n */\n async deleteTranslation(\n entityId: string,\n locale?: string,\n options?: { tx?: PostgresJsDatabase },\n ): Promise<void> {\n await assertEntityAccess(this.entity, this.contextResolver, 'update')\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Deleting translation')\n\n const exec = options?.tx ?? this.db\n const translationTableName = `${this.entity.name}_translations`\n const translationTable = schemaRegistry.get(translationTableName)\n\n if (!translationTable) {\n throw new Error(`Translation table ${translationTableName} not found in schema registry`)\n }\n\n if (locale) {\n // Delete specific locale\n await exec\n .delete(translationTable)\n .where(and(eq(translationTable.entityId, entityId), eq(translationTable.locale, locale)))\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Translation deleted')\n } else {\n // Delete all translations for entity\n await exec.delete(translationTable).where(eq(translationTable.entityId, entityId))\n this.logger?.info({ entity: this.entity.name, entityId }, 'All translations deleted')\n }\n }\n\n /**\n * Save block-level translations for an entity.\n * For each block in each blocks field, extracts translatable field values\n * and upserts them into {entity}_layout_translations.\n *\n * Block _id must match an existing layout row ID.\n *\n * **Caller transaction (`options.tx`)** — Phase 4b of PLAN-OUTBOX.\n * Routes the layout-row lookup AND every layout-translation upsert\n * through the supplied tx, so a caller doing\n * `entity.update + saveBlockTranslations` in one tx gets atomic\n * visibility on rollback.\n */\n async saveBlockTranslations(\n entityId: string,\n locale: string,\n data: Record<string, unknown>,\n options?: { tx?: PostgresJsDatabase },\n ): Promise<void> {\n await assertEntityAccess(this.entity, this.contextResolver, 'update')\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Saving block translations')\n\n const exec = options?.tx ?? this.db\n const blocksFields = getBlocksFields(this.ctx)\n if (blocksFields.length === 0) return\n\n const layoutTransTable = schemaRegistry.get(`${this.entity.name}_layout_translations`)\n if (!layoutTransTable) return\n\n const layoutTable = schemaRegistry.get(`${this.entity.name}_layout`)\n if (!layoutTable) return\n\n for (const { name: fieldName, config } of blocksFields) {\n const blocks = data[fieldName]\n if (!Array.isArray(blocks)) continue\n\n // Get block definitions to identify translatable fields\n if (config.type !== 'blocks') continue\n const blockDefs = config.blocks\n if (!blockDefs) continue\n\n // Load actual layout row IDs from DB (ordered by sort_order to match request array order).\n // The _id from the request may be a Puck-generated ID (e.g. \"hero-abc123\") rather than\n // the real DB UUID, so we match by position instead. Routes through `exec` so the\n // SELECT sees in-flight writes from a caller-tx.\n const layoutRows = await exec\n .select({ id: layoutTable.id, blockType: layoutTable.blockType })\n .from(layoutTable)\n .where(\n and(\n eq(layoutTable.entityId, entityId),\n eq(layoutTable.fieldName, fieldName),\n isNull(layoutTable.locale),\n ),\n )\n .orderBy(layoutTable.sortOrder)\n\n for (let i = 0; i < (blocks as Record<string, unknown>[]).length; i++) {\n const block = (blocks as Record<string, unknown>[])[i]\n if (!block) continue\n const blockType = block._block as string\n if (!blockType) continue\n\n // Match by position — layout rows and request blocks are in the same sort order\n const layoutRow = layoutRows[i]\n if (!layoutRow || layoutRow.blockType !== blockType) continue\n\n const blockDef = blockDefs.find((b) => b.slug === blockType)\n if (!blockDef) continue\n\n // Extract only translatable fields\n const translatedFields: Record<string, unknown> = {}\n for (const [fname, fconfig] of Object.entries(blockDef.fields)) {\n if (fconfig.translatable && block[fname] !== undefined) {\n translatedFields[fname] = block[fname]\n }\n }\n\n if (Object.keys(translatedFields).length === 0) continue\n\n // Upsert using the real DB layout row ID\n await exec\n .insert(layoutTransTable)\n .values({ layoutId: layoutRow.id, locale, fields: translatedFields })\n .onConflictDoUpdate({\n target: [layoutTransTable.layoutId, layoutTransTable.locale],\n set: { fields: translatedFields },\n })\n }\n }\n\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Block translations saved')\n }\n\n /**\n * Save per-locale blocks for an entity.\n * Used for entities with `localized: true` on their blocks field — each locale gets\n * its own independent block layout rows in the layout table.\n *\n * **Caller transaction (`options.tx`)** — Phase 4b of PLAN-OUTBOX. Threaded\n * through to `saveBlocks`, which already accepts an executor.\n */\n async saveLocalizedBlocks(\n entityId: string,\n locale: string,\n data: Record<string, unknown>,\n options?: { tx?: PostgresJsDatabase },\n ): Promise<void> {\n await assertEntityAccess(this.entity, this.contextResolver, 'update')\n this.logger?.info({ entity: this.entity.name, entityId, locale }, 'Saving localized blocks')\n const exec = options?.tx ?? this.db\n await this.saveBlocks(exec, entityId, data, { locale })\n }\n\n // ---------------------------------------------------------------\n // Block CRUD (layout table operations)\n // ---------------------------------------------------------------\n\n /**\n * Save blocks for an entity after create/update.\n * For each blocks field, writes rows to {entity}_layout table.\n *\n * `exec` is the active Drizzle executor — either a caller-supplied tx\n * (PR D of PLAN-OUTBOX) or `this.db`. Routing through it keeps the\n * blocks INSERTs/DELETEs in the same transaction as the parent\n * entity write.\n */\n private async saveBlocks(\n exec: PostgresJsDatabase,\n entityId: string,\n data: Record<string, unknown>,\n options?: { locale?: string },\n ): Promise<void> {\n const blocksFields = getBlocksFields(this.ctx)\n if (blocksFields.length === 0) return\n\n const layoutTable = schemaRegistry.get(`${this.entity.name}_layout`)\n if (!layoutTable) return\n\n for (const { name: fieldName, config } of blocksFields) {\n const blocks = data[fieldName]\n if (!Array.isArray(blocks)) continue\n\n const isLocalized = 'localized' in config && config.localized === true\n const locale = isLocalized ? (options?.locale ?? null) : null\n\n // Delete existing layout rows for this field + locale\n const deleteConditions = [\n eq(layoutTable.entityId, entityId),\n eq(layoutTable.fieldName, fieldName),\n ]\n if (locale) {\n deleteConditions.push(eq(layoutTable.locale, locale))\n } else if (!isLocalized) {\n // For shared layout, delete rows where locale IS NULL\n deleteConditions.push(isNull(layoutTable.locale))\n }\n await exec.delete(layoutTable).where(and(...deleteConditions))\n\n // Insert new layout rows\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i] as Record<string, unknown>\n const blockType = block._block as string\n if (!blockType) continue\n\n // Separate _block and _id from block data\n const { _block, _id, ...blockData } = block\n\n await exec.insert(layoutTable).values({\n entityId,\n fieldName,\n blockType,\n sortOrder: i,\n data: blockData,\n locale,\n })\n }\n }\n }\n\n // ---------------------------------------------------------------\n // Reference tracking (entity_refs)\n // ---------------------------------------------------------------\n\n /**\n * Sync outgoing references in entity_refs after create/update.\n *\n * - 'create': inserts all refs (entity is new, no existing refs)\n * - 'update': deletes refs for changed ref-bearing fields, then inserts new ones\n *\n * Gracefully skips if entity_refs table is not registered (e.g. before migration).\n *\n * `exec` is the active Drizzle executor — caller-supplied tx (PR D of\n * PLAN-OUTBOX) or `this.db`. Routes the entity_refs UPDATE/INSERT\n * through the same transaction as the parent entity write.\n */\n private async syncRefs(\n exec: PostgresJsDatabase,\n entityId: string,\n data: Record<string, unknown>,\n mode: 'create' | 'update',\n ): Promise<void> {\n // Graceful degradation: skip if entity_refs table doesn't exist yet\n if (!schemaRegistry.has('entity_refs')) return\n\n const allFields = getAllFields(this.ctx)\n\n if (mode === 'update') {\n // Only delete refs for fields present in the update data\n const changedRefFields = Object.keys(data).filter((k) => {\n const config = allFields[k]\n return (\n config &&\n (config.type === 'media' || config.type === 'reference' || config.type === 'blocks')\n )\n })\n if (changedRefFields.length > 0) {\n await exec\n .delete(entityRefs)\n .where(\n and(\n eq(entityRefs.sourceEntity, this.entity.name),\n eq(entityRefs.sourceId, entityId),\n inArray(entityRefs.sourceField, changedRefFields),\n ),\n )\n }\n }\n\n // Extract refs from the data and insert\n const refs = extractRefs(allFields, data)\n if (refs.length > 0) {\n await exec\n .insert(entityRefs)\n .values(\n refs.map((ref) => ({\n sourceEntity: this.entity.name,\n sourceId: entityId,\n sourceField: ref.sourceField,\n targetEntity: ref.targetEntity,\n targetId: ref.targetId,\n })),\n )\n .onConflictDoNothing()\n }\n }\n\n /**\n * Invalidate all count cache entries for this entity.\n *\n * **Tx-aware** (PR D of PLAN-OUTBOX). When `inCallerTx === true`, we\n * skip the invalidation: the caller's tx hasn't committed yet, and\n * eagerly invalidating would have other connections re-compute the\n * count from the still-pre-tx state and cache that stale value.\n * The caller is responsible for invalidating after their own commit\n * (or accepting the cache TTL self-heal). For internally-managed\n * txs (no-caller-tx path) and no-tx CRUD calls, the invalidation\n * runs after the inner `db.transaction(...)` resolves — which only\n * happens post-commit — so the cache reflects the committed state.\n */\n private invalidateCountCache(inCallerTx = false): void {\n if (inCallerTx) return\n this.countCache?.invalidate(this.entity.name)\n }\n}\n"],"mappings":"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"}
|