@open-mercato/shared 0.6.5-develop.5337.1.534b781eac → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/lib/ai/llm-provider-registry.js.map +1 -1
  4. package/dist/lib/crud/custom-fields.js +23 -15
  5. package/dist/lib/crud/custom-fields.js.map +2 -2
  6. package/dist/lib/crud/factory.js.map +1 -1
  7. package/dist/lib/crud/optimistic-lock-command.js.map +1 -1
  8. package/dist/lib/crud/optimistic-lock-headers.js.map +1 -1
  9. package/dist/lib/crud/optimistic-lock-store.js.map +1 -1
  10. package/dist/lib/crud/optimistic-lock.js.map +1 -1
  11. package/dist/lib/data/engine.js +25 -1
  12. package/dist/lib/data/engine.js.map +2 -2
  13. package/dist/lib/db/buildIlikeTerm.js +17 -0
  14. package/dist/lib/db/buildIlikeTerm.js.map +7 -0
  15. package/dist/lib/db/mikro.js +38 -9
  16. package/dist/lib/db/mikro.js.map +2 -2
  17. package/dist/lib/di/container.js +1 -1
  18. package/dist/lib/di/container.js.map +1 -1
  19. package/dist/lib/encryption/kms.js +41 -6
  20. package/dist/lib/encryption/kms.js.map +2 -2
  21. package/dist/lib/query/advanced-filter-tree.js +5 -5
  22. package/dist/lib/query/advanced-filter-tree.js.map +2 -2
  23. package/dist/lib/query/advanced-filter.js +5 -5
  24. package/dist/lib/query/advanced-filter.js.map +2 -2
  25. package/dist/lib/query/engine.js +3 -1
  26. package/dist/lib/query/engine.js.map +2 -2
  27. package/dist/lib/query/types.js.map +1 -1
  28. package/dist/lib/version.js +1 -1
  29. package/dist/lib/version.js.map +1 -1
  30. package/dist/modules/overrides.js +1 -1
  31. package/dist/modules/overrides.js.map +1 -1
  32. package/dist/modules/search.js.map +1 -1
  33. package/package.json +4 -5
  34. package/src/lib/ai/llm-provider-registry.ts +1 -1
  35. package/src/lib/ai/llm-provider.ts +1 -1
  36. package/src/lib/crud/__tests__/custom-fields.test.ts +91 -0
  37. package/src/lib/crud/custom-fields.ts +30 -17
  38. package/src/lib/crud/factory.ts +1 -1
  39. package/src/lib/crud/optimistic-lock-command.ts +1 -1
  40. package/src/lib/crud/optimistic-lock-headers.ts +1 -1
  41. package/src/lib/crud/optimistic-lock-store.ts +1 -1
  42. package/src/lib/crud/optimistic-lock.ts +1 -1
  43. package/src/lib/data/__tests__/engine.custom-entity-storage-guard.test.ts +78 -0
  44. package/src/lib/data/engine.ts +40 -0
  45. package/src/lib/db/__tests__/buildIlikeTerm.test.ts +40 -0
  46. package/src/lib/db/__tests__/escapeLikePattern.test.ts +123 -0
  47. package/src/lib/db/__tests__/mikro.test.ts +82 -0
  48. package/src/lib/db/buildIlikeTerm.ts +16 -0
  49. package/src/lib/db/mikro.ts +55 -16
  50. package/src/lib/di/container.ts +1 -1
  51. package/src/lib/encryption/__tests__/kms.test.ts +80 -0
  52. package/src/lib/encryption/kms.ts +55 -7
  53. package/src/lib/query/__tests__/engine.count-distinct.test.ts +229 -0
  54. package/src/lib/query/advanced-filter-tree.ts +5 -5
  55. package/src/lib/query/advanced-filter.ts +5 -5
  56. package/src/lib/query/engine.ts +13 -2
  57. package/src/lib/query/types.ts +10 -0
  58. package/src/modules/__tests__/overrides.test.ts +1 -1
  59. package/src/modules/__tests__/route-overrides.test.ts +1 -1
  60. package/src/modules/navigation/backendChrome.ts +9 -0
  61. package/src/modules/overrides.ts +3 -3
  62. package/src/modules/search.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/optimistic-lock-command.ts"],
4
- "sourcesContent": ["/**\n * Generalist command-level OSS optimistic-locking helper.\n *\n * The CRUD guard (`optimistic-lock.ts` + `makeCrudRoute`) only protects\n * mutations that flow through the CRUD factory. Domain writes implemented via\n * the Command pattern \u2014 sales document sub-resources (lines, adjustments,\n * shipments, payments, returns), status transitions, quote\u2192order conversion,\n * etc. \u2014 run their own logic inside a command handler and never reach the CRUD\n * guard for the **aggregate** they mutate. This helper lets any command enforce\n * the same `updated_at` version check against an arbitrary target record\n * (typically the aggregate root, e.g. the parent order/quote) and fail with the\n * identical structured 409 the CRUD path returns.\n *\n * Contract (mirrors the CRUD guard so clients see one behavior):\n * - The client sends the expected version via the\n * `x-om-ext-optimistic-lock-expected-updated-at` header (or a command\n * accepts it as a typed input field and passes it as `expected`).\n * - The command loads the current record (it usually already does) and passes\n * its `updated_at` as `current`.\n * - On mismatch the helper throws `CrudHttpError(409, OptimisticLockConflictBody)`.\n *\n * Strictly additive: when no expected token is present (no header, no input\n * field) the helper is a no-op, so existing API consumers that don't send the\n * header keep working. Respects the same `OM_OPTIMISTIC_LOCK` env contract\n * (default ON; `off` disables; allow-list scopes by `resourceKind`).\n *\n * Spec: .ai/specs/2026-05-25-oss-optimistic-locking.md (\u00A7 command-level checks)\n * .ai/specs/2026-05-28-optimistic-locking-coverage-completion.md (Phase 4)\n */\nimport { CrudHttpError } from './errors'\nimport {\n OPTIMISTIC_LOCK_CONFLICT_CODE,\n OPTIMISTIC_LOCK_CONFLICT_ERROR,\n OPTIMISTIC_LOCK_ENV_VAR,\n OPTIMISTIC_LOCK_HEADER_NAME,\n type OptimisticLockConflictBody,\n} from './optimistic-lock-headers'\nimport {\n normalizeIsoToken,\n parseOptimisticLockEnv,\n type OptimisticLockConfig,\n type OptimisticLockResolverInput,\n type ResolveExpectedUpdatedAt,\n} from './optimistic-lock'\n\nfunction toIsoOrNull(value: string | Date | null | undefined): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isFinite(ms) ? new Date(ms).toISOString() : null\n }\n const trimmed = String(value).trim()\n if (!trimmed) return null\n return normalizeIsoToken(trimmed)\n}\n\nfunction resolveConfig(envValue: string | null | undefined): OptimisticLockConfig {\n return parseOptimisticLockEnv(envValue !== undefined ? envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR])\n}\n\nfunction isResourceLockEnabled(config: OptimisticLockConfig, resourceKind: string): boolean {\n if (config.mode === 'off') return false\n if (config.mode === 'all') return true\n return config.entities.has(resourceKind.toLowerCase())\n}\n\n/**\n * Extract the expected `updated_at` token from a request's headers (or a bare\n * `Headers` object). Returns the trimmed header value, or `null` when absent /\n * empty. Does NOT normalize \u2014 `assertOptimisticLock` normalizes both sides.\n */\nexport function readOptimisticLockExpected(\n source: Request | Headers | null | undefined,\n): string | null {\n if (!source) return null\n const headers = source instanceof Headers\n ? source\n : (source as Request).headers instanceof Headers\n ? (source as Request).headers\n : null\n if (!headers) return null\n const direct = headers.get(OPTIMISTIC_LOCK_HEADER_NAME)\n if (typeof direct === 'string' && direct.trim().length > 0) return direct.trim()\n return null\n}\n\nexport function buildOptimisticLockConflictBody(\n currentIso: string,\n expectedIso: string,\n): OptimisticLockConflictBody {\n return {\n error: OPTIMISTIC_LOCK_CONFLICT_ERROR,\n code: OPTIMISTIC_LOCK_CONFLICT_CODE,\n currentUpdatedAt: currentIso,\n expectedUpdatedAt: expectedIso,\n }\n}\n\nexport type AssertOptimisticLockInput = {\n resourceKind: string\n resourceId: string\n /** Client-provided expected version (header value or typed input field). */\n expected: string | Date | null | undefined\n /** Current version loaded from the DB (typically the aggregate root's `updatedAt`). */\n current: string | Date | null | undefined\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Pure version assertion. Throws `CrudHttpError(409, OptimisticLockConflictBody)`\n * when the expected and current versions disagree.\n *\n * No-op (returns silently) when:\n * - the env disables the guard for this `resourceKind`,\n * - `expected` is missing / unparseable (strictly additive \u2014 clients that\n * don't send the token are never blocked),\n * - `current` is missing / unparseable (let the command's own 404 fire).\n */\nexport function assertOptimisticLock(input: AssertOptimisticLockInput): void {\n const config = resolveConfig(input.envValue)\n if (!isResourceLockEnabled(config, input.resourceKind)) return\n\n const expectedIso = toIsoOrNull(input.expected)\n if (expectedIso == null) return\n\n const currentIso = toIsoOrNull(input.current)\n if (currentIso == null) return\n\n if (currentIso === expectedIso) return\n\n throw new CrudHttpError(409, buildOptimisticLockConflictBody(currentIso, expectedIso))\n}\n\nexport type EnforceCommandOptimisticLockInput = {\n resourceKind: string\n resourceId: string\n /** Current version loaded from the DB (the aggregate root's `updatedAt`). */\n current: string | Date | null | undefined\n /**\n * Explicit expected version \u2014 wins over the request header. Use when the\n * command accepts the token as a typed input field instead of (or in addition\n * to) the extension header.\n */\n expected?: string | Date | null\n /** Request whose headers carry the expected token (e.g. `ctx.request`). */\n request?: Request | Headers | null\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Command-handler convenience: resolves the expected version from an explicit\n * override or the request header, then delegates to `assertOptimisticLock`.\n *\n * ```ts\n * enforceCommandOptimisticLock({\n * resourceKind: 'sales.order',\n * resourceId: order.id,\n * current: order.updatedAt,\n * request: ctx.request,\n * })\n * ```\n */\nexport function enforceCommandOptimisticLock(input: EnforceCommandOptimisticLockInput): void {\n const expected = input.expected !== undefined && input.expected !== null\n ? input.expected\n : readOptimisticLockExpected(input.request ?? null)\n assertOptimisticLock({\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n expected,\n current: input.current,\n envValue: input.envValue,\n })\n}\n\nexport type EnforceRecordGoneIsConflictInput = {\n resourceKind: string\n resourceId: string\n /** Explicit expected version \u2014 wins over the request header. */\n expected?: string | Date | null\n /** Request whose headers carry the expected token (e.g. `ctx.request`). */\n request?: Request | Headers | null\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Command-handler convenience for the *concurrent-delete* race: when a command\n * cannot find its target record (it was deleted in another tab) AND the client\n * opted into optimistic locking (sent the expected-version header / token),\n * throw the SAME structured `CrudHttpError(409, OptimisticLockConflictBody)`\n * the version-mismatch path returns \u2014 so a stale modal save surfaces the unified\n * \"Record changed\" conflict bar instead of a bare, generic 404.\n *\n * Strictly additive and fail-open: a no-op (returns silently) when the env\n * disables the guard for `resourceKind`, or when no expected token is present\n * (plain API consumers that never sent the header keep their existing 404).\n * The caller MUST still throw its own 404 afterwards for that no-token path:\n *\n * ```ts\n * if (!interaction) {\n * enforceRecordGoneIsConflict({ resourceKind: 'customers.interaction', resourceId: id, request: ctx.request })\n * throw new CrudHttpError(404, { error: 'Interaction not found' })\n * }\n * ```\n *\n * The gone record has no current version, so `currentUpdatedAt` echoes the\n * expected token (the body is used only for the conflict-bar copy + diagnostics;\n * the client keys off `code`, not the timestamps).\n */\nexport function enforceRecordGoneIsConflict(input: EnforceRecordGoneIsConflictInput): void {\n const config = resolveConfig(input.envValue)\n if (!isResourceLockEnabled(config, input.resourceKind)) return\n const clientSupplied = input.expected !== undefined && input.expected !== null\n ? input.expected\n : readOptimisticLockExpected(input.request ?? null)\n const expectedIso = toIsoOrNull(clientSupplied)\n if (expectedIso == null) return\n throw new CrudHttpError(409, buildOptimisticLockConflictBody(expectedIso, expectedIso))\n}\n\n/**\n * DI-resolvable command-level optimistic-lock guard. This is the framework\n * seam that lets BOTH layers protect Command-pattern writes through one\n * contract:\n *\n * - **OSS** registers the default service (header/explicit token compare \u2014\n * identical to calling `enforceCommandOptimisticLock` directly).\n * - **Enterprise** (`record_locks`) re-registers the same DI key with a\n * `resolveExpected` that reads the held pessimistic lock's version, so a\n * stale command write fails with the same structured 409 WITHOUT any\n * command handler changing. Mirrors how enterprise already replaces the\n * CRUD-path `crudMutationGuardService` (see `optimistic-lock.ts`).\n *\n * Command handlers depend only on this interface (resolved from the container),\n * never on a concrete implementation \u2014 that is what makes the next-PR\n * enterprise extension a pure DI swap.\n */\nexport type CommandOptimisticLockGuardService = {\n /**\n * Enforce the version check for a command-level mutation against an\n * aggregate/record. Async because an enterprise resolver may load the\n * expected token from a lock record. No-op (resolves silently) when the env\n * disables the guard for `resourceKind` or when no expected token is\n * resolved \u2014 strictly additive, exactly like {@link enforceCommandOptimisticLock}.\n * Throws `CrudHttpError(409, OptimisticLockConflictBody)` on mismatch.\n */\n enforce: (input: EnforceCommandOptimisticLockInput) => Promise<void>\n}\n\nexport type CreateCommandOptimisticLockGuardServiceOptions = {\n /**\n * Override how the expected version is derived. Receives\n * `{ expectedFromHeader, resourceKind, resourceId }` (where\n * `expectedFromHeader` is the normalized client-supplied token \u2014 explicit\n * input or request header) and returns the expected token (or `null` to\n * skip). Defaults to \"use the client-supplied token\", which is the OSS\n * behavior. The enterprise `record_locks` module plugs a lock-backed\n * resolver here. Mirrors the CRUD guard's `resolveExpected`.\n */\n resolveExpected?: ResolveExpectedUpdatedAt\n}\n\n/**\n * Build a {@link CommandOptimisticLockGuardService}. With no options it is\n * behaviourally identical to {@link enforceCommandOptimisticLock} (header/\n * explicit compare), so the OSS default is a thin wrapper. Pass\n * `resolveExpected` to override token resolution (enterprise extension point).\n */\nexport function createCommandOptimisticLockGuardService(\n options: CreateCommandOptimisticLockGuardServiceOptions = {},\n): CommandOptimisticLockGuardService {\n const resolveExpected: ResolveExpectedUpdatedAt | null = options.resolveExpected ?? null\n return {\n async enforce(input: EnforceCommandOptimisticLockInput): Promise<void> {\n const config = resolveConfig(input.envValue)\n if (!isResourceLockEnabled(config, input.resourceKind)) return\n\n const clientSupplied = input.expected !== undefined && input.expected !== null\n ? input.expected\n : readOptimisticLockExpected(input.request ?? null)\n const expectedFromHeader = toIsoOrNull(clientSupplied)\n\n let expected: string | null = expectedFromHeader\n if (resolveExpected) {\n const resolverInput: OptimisticLockResolverInput = {\n expectedFromHeader,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n }\n expected = await resolveExpected(resolverInput)\n }\n\n assertOptimisticLock({\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n expected,\n current: input.current,\n envValue: input.envValue,\n })\n },\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * Generalist command-level OSS optimistic-locking helper.\n *\n * The CRUD guard (`optimistic-lock.ts` + `makeCrudRoute`) only protects\n * mutations that flow through the CRUD factory. Domain writes implemented via\n * the Command pattern \u2014 sales document sub-resources (lines, adjustments,\n * shipments, payments, returns), status transitions, quote\u2192order conversion,\n * etc. \u2014 run their own logic inside a command handler and never reach the CRUD\n * guard for the **aggregate** they mutate. This helper lets any command enforce\n * the same `updated_at` version check against an arbitrary target record\n * (typically the aggregate root, e.g. the parent order/quote) and fail with the\n * identical structured 409 the CRUD path returns.\n *\n * Contract (mirrors the CRUD guard so clients see one behavior):\n * - The client sends the expected version via the\n * `x-om-ext-optimistic-lock-expected-updated-at` header (or a command\n * accepts it as a typed input field and passes it as `expected`).\n * - The command loads the current record (it usually already does) and passes\n * its `updated_at` as `current`.\n * - On mismatch the helper throws `CrudHttpError(409, OptimisticLockConflictBody)`.\n *\n * Strictly additive: when no expected token is present (no header, no input\n * field) the helper is a no-op, so existing API consumers that don't send the\n * header keep working. Respects the same `OM_OPTIMISTIC_LOCK` env contract\n * (default ON; `off` disables; allow-list scopes by `resourceKind`).\n *\n * Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md (\u00A7 command-level checks)\n * .ai/specs/2026-05-28-optimistic-locking-coverage-completion.md (Phase 4)\n */\nimport { CrudHttpError } from './errors'\nimport {\n OPTIMISTIC_LOCK_CONFLICT_CODE,\n OPTIMISTIC_LOCK_CONFLICT_ERROR,\n OPTIMISTIC_LOCK_ENV_VAR,\n OPTIMISTIC_LOCK_HEADER_NAME,\n type OptimisticLockConflictBody,\n} from './optimistic-lock-headers'\nimport {\n normalizeIsoToken,\n parseOptimisticLockEnv,\n type OptimisticLockConfig,\n type OptimisticLockResolverInput,\n type ResolveExpectedUpdatedAt,\n} from './optimistic-lock'\n\nfunction toIsoOrNull(value: string | Date | null | undefined): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n const ms = value.getTime()\n return Number.isFinite(ms) ? new Date(ms).toISOString() : null\n }\n const trimmed = String(value).trim()\n if (!trimmed) return null\n return normalizeIsoToken(trimmed)\n}\n\nfunction resolveConfig(envValue: string | null | undefined): OptimisticLockConfig {\n return parseOptimisticLockEnv(envValue !== undefined ? envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR])\n}\n\nfunction isResourceLockEnabled(config: OptimisticLockConfig, resourceKind: string): boolean {\n if (config.mode === 'off') return false\n if (config.mode === 'all') return true\n return config.entities.has(resourceKind.toLowerCase())\n}\n\n/**\n * Extract the expected `updated_at` token from a request's headers (or a bare\n * `Headers` object). Returns the trimmed header value, or `null` when absent /\n * empty. Does NOT normalize \u2014 `assertOptimisticLock` normalizes both sides.\n */\nexport function readOptimisticLockExpected(\n source: Request | Headers | null | undefined,\n): string | null {\n if (!source) return null\n const headers = source instanceof Headers\n ? source\n : (source as Request).headers instanceof Headers\n ? (source as Request).headers\n : null\n if (!headers) return null\n const direct = headers.get(OPTIMISTIC_LOCK_HEADER_NAME)\n if (typeof direct === 'string' && direct.trim().length > 0) return direct.trim()\n return null\n}\n\nexport function buildOptimisticLockConflictBody(\n currentIso: string,\n expectedIso: string,\n): OptimisticLockConflictBody {\n return {\n error: OPTIMISTIC_LOCK_CONFLICT_ERROR,\n code: OPTIMISTIC_LOCK_CONFLICT_CODE,\n currentUpdatedAt: currentIso,\n expectedUpdatedAt: expectedIso,\n }\n}\n\nexport type AssertOptimisticLockInput = {\n resourceKind: string\n resourceId: string\n /** Client-provided expected version (header value or typed input field). */\n expected: string | Date | null | undefined\n /** Current version loaded from the DB (typically the aggregate root's `updatedAt`). */\n current: string | Date | null | undefined\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Pure version assertion. Throws `CrudHttpError(409, OptimisticLockConflictBody)`\n * when the expected and current versions disagree.\n *\n * No-op (returns silently) when:\n * - the env disables the guard for this `resourceKind`,\n * - `expected` is missing / unparseable (strictly additive \u2014 clients that\n * don't send the token are never blocked),\n * - `current` is missing / unparseable (let the command's own 404 fire).\n */\nexport function assertOptimisticLock(input: AssertOptimisticLockInput): void {\n const config = resolveConfig(input.envValue)\n if (!isResourceLockEnabled(config, input.resourceKind)) return\n\n const expectedIso = toIsoOrNull(input.expected)\n if (expectedIso == null) return\n\n const currentIso = toIsoOrNull(input.current)\n if (currentIso == null) return\n\n if (currentIso === expectedIso) return\n\n throw new CrudHttpError(409, buildOptimisticLockConflictBody(currentIso, expectedIso))\n}\n\nexport type EnforceCommandOptimisticLockInput = {\n resourceKind: string\n resourceId: string\n /** Current version loaded from the DB (the aggregate root's `updatedAt`). */\n current: string | Date | null | undefined\n /**\n * Explicit expected version \u2014 wins over the request header. Use when the\n * command accepts the token as a typed input field instead of (or in addition\n * to) the extension header.\n */\n expected?: string | Date | null\n /** Request whose headers carry the expected token (e.g. `ctx.request`). */\n request?: Request | Headers | null\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Command-handler convenience: resolves the expected version from an explicit\n * override or the request header, then delegates to `assertOptimisticLock`.\n *\n * ```ts\n * enforceCommandOptimisticLock({\n * resourceKind: 'sales.order',\n * resourceId: order.id,\n * current: order.updatedAt,\n * request: ctx.request,\n * })\n * ```\n */\nexport function enforceCommandOptimisticLock(input: EnforceCommandOptimisticLockInput): void {\n const expected = input.expected !== undefined && input.expected !== null\n ? input.expected\n : readOptimisticLockExpected(input.request ?? null)\n assertOptimisticLock({\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n expected,\n current: input.current,\n envValue: input.envValue,\n })\n}\n\nexport type EnforceRecordGoneIsConflictInput = {\n resourceKind: string\n resourceId: string\n /** Explicit expected version \u2014 wins over the request header. */\n expected?: string | Date | null\n /** Request whose headers carry the expected token (e.g. `ctx.request`). */\n request?: Request | Headers | null\n /** Override `OM_OPTIMISTIC_LOCK` (mostly for tests). */\n envValue?: string | null\n}\n\n/**\n * Command-handler convenience for the *concurrent-delete* race: when a command\n * cannot find its target record (it was deleted in another tab) AND the client\n * opted into optimistic locking (sent the expected-version header / token),\n * throw the SAME structured `CrudHttpError(409, OptimisticLockConflictBody)`\n * the version-mismatch path returns \u2014 so a stale modal save surfaces the unified\n * \"Record changed\" conflict bar instead of a bare, generic 404.\n *\n * Strictly additive and fail-open: a no-op (returns silently) when the env\n * disables the guard for `resourceKind`, or when no expected token is present\n * (plain API consumers that never sent the header keep their existing 404).\n * The caller MUST still throw its own 404 afterwards for that no-token path:\n *\n * ```ts\n * if (!interaction) {\n * enforceRecordGoneIsConflict({ resourceKind: 'customers.interaction', resourceId: id, request: ctx.request })\n * throw new CrudHttpError(404, { error: 'Interaction not found' })\n * }\n * ```\n *\n * The gone record has no current version, so `currentUpdatedAt` echoes the\n * expected token (the body is used only for the conflict-bar copy + diagnostics;\n * the client keys off `code`, not the timestamps).\n */\nexport function enforceRecordGoneIsConflict(input: EnforceRecordGoneIsConflictInput): void {\n const config = resolveConfig(input.envValue)\n if (!isResourceLockEnabled(config, input.resourceKind)) return\n const clientSupplied = input.expected !== undefined && input.expected !== null\n ? input.expected\n : readOptimisticLockExpected(input.request ?? null)\n const expectedIso = toIsoOrNull(clientSupplied)\n if (expectedIso == null) return\n throw new CrudHttpError(409, buildOptimisticLockConflictBody(expectedIso, expectedIso))\n}\n\n/**\n * DI-resolvable command-level optimistic-lock guard. This is the framework\n * seam that lets BOTH layers protect Command-pattern writes through one\n * contract:\n *\n * - **OSS** registers the default service (header/explicit token compare \u2014\n * identical to calling `enforceCommandOptimisticLock` directly).\n * - **Enterprise** (`record_locks`) re-registers the same DI key with a\n * `resolveExpected` that reads the held pessimistic lock's version, so a\n * stale command write fails with the same structured 409 WITHOUT any\n * command handler changing. Mirrors how enterprise already replaces the\n * CRUD-path `crudMutationGuardService` (see `optimistic-lock.ts`).\n *\n * Command handlers depend only on this interface (resolved from the container),\n * never on a concrete implementation \u2014 that is what makes the next-PR\n * enterprise extension a pure DI swap.\n */\nexport type CommandOptimisticLockGuardService = {\n /**\n * Enforce the version check for a command-level mutation against an\n * aggregate/record. Async because an enterprise resolver may load the\n * expected token from a lock record. No-op (resolves silently) when the env\n * disables the guard for `resourceKind` or when no expected token is\n * resolved \u2014 strictly additive, exactly like {@link enforceCommandOptimisticLock}.\n * Throws `CrudHttpError(409, OptimisticLockConflictBody)` on mismatch.\n */\n enforce: (input: EnforceCommandOptimisticLockInput) => Promise<void>\n}\n\nexport type CreateCommandOptimisticLockGuardServiceOptions = {\n /**\n * Override how the expected version is derived. Receives\n * `{ expectedFromHeader, resourceKind, resourceId }` (where\n * `expectedFromHeader` is the normalized client-supplied token \u2014 explicit\n * input or request header) and returns the expected token (or `null` to\n * skip). Defaults to \"use the client-supplied token\", which is the OSS\n * behavior. The enterprise `record_locks` module plugs a lock-backed\n * resolver here. Mirrors the CRUD guard's `resolveExpected`.\n */\n resolveExpected?: ResolveExpectedUpdatedAt\n}\n\n/**\n * Build a {@link CommandOptimisticLockGuardService}. With no options it is\n * behaviourally identical to {@link enforceCommandOptimisticLock} (header/\n * explicit compare), so the OSS default is a thin wrapper. Pass\n * `resolveExpected` to override token resolution (enterprise extension point).\n */\nexport function createCommandOptimisticLockGuardService(\n options: CreateCommandOptimisticLockGuardServiceOptions = {},\n): CommandOptimisticLockGuardService {\n const resolveExpected: ResolveExpectedUpdatedAt | null = options.resolveExpected ?? null\n return {\n async enforce(input: EnforceCommandOptimisticLockInput): Promise<void> {\n const config = resolveConfig(input.envValue)\n if (!isResourceLockEnabled(config, input.resourceKind)) return\n\n const clientSupplied = input.expected !== undefined && input.expected !== null\n ? input.expected\n : readOptimisticLockExpected(input.request ?? null)\n const expectedFromHeader = toIsoOrNull(clientSupplied)\n\n let expected: string | null = expectedFromHeader\n if (resolveExpected) {\n const resolverInput: OptimisticLockResolverInput = {\n expectedFromHeader,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n }\n expected = await resolveExpected(resolverInput)\n }\n\n assertOptimisticLock({\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n expected,\n current: input.current,\n envValue: input.envValue,\n })\n },\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/optimistic-lock-headers.ts"],
4
- "sourcesContent": ["/**\n * Wire constants for the OSS opt-in optimistic-locking guard.\n *\n * The header name follows the project's extension-header convention\n * (`x-om-ext-<moduleId>-<key>`, see `umes/extension-headers.ts`). The\n * module id used by env opt-in is `optimistic_lock` (snake_case), but\n * the HTTP header itself uses dash-separated `optimistic-lock` because\n * many HTTP intermediaries (nginx, some fetch implementations) strip\n * underscored header names \u2014 see RFC 7230 \u00A73.2.6.\n *\n * Spec: .ai/specs/2026-05-25-oss-optimistic-locking.md \u00A73.2\n */\nexport const OPTIMISTIC_LOCK_MODULE_ID = 'optimistic_lock'\n\nexport const OPTIMISTIC_LOCK_HEADER_NAME = 'x-om-ext-optimistic-lock-expected-updated-at'\n\nexport const OPTIMISTIC_LOCK_CONFLICT_CODE = 'optimistic_lock_conflict'\n\nexport const OPTIMISTIC_LOCK_CONFLICT_ERROR = 'record_modified'\n\nexport const OPTIMISTIC_LOCK_ENV_VAR = 'OM_OPTIMISTIC_LOCK'\n\nexport const OPTIMISTIC_LOCK_DEFAULT_PRIORITY = 50\n\nexport type OptimisticLockConflictBody = {\n error: typeof OPTIMISTIC_LOCK_CONFLICT_ERROR\n code: typeof OPTIMISTIC_LOCK_CONFLICT_CODE\n currentUpdatedAt: string\n expectedUpdatedAt: string\n}\n"],
4
+ "sourcesContent": ["/**\n * Wire constants for the OSS opt-in optimistic-locking guard.\n *\n * The header name follows the project's extension-header convention\n * (`x-om-ext-<moduleId>-<key>`, see `umes/extension-headers.ts`). The\n * module id used by env opt-in is `optimistic_lock` (snake_case), but\n * the HTTP header itself uses dash-separated `optimistic-lock` because\n * many HTTP intermediaries (nginx, some fetch implementations) strip\n * underscored header names \u2014 see RFC 7230 \u00A73.2.6.\n *\n * Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md \u00A73.2\n */\nexport const OPTIMISTIC_LOCK_MODULE_ID = 'optimistic_lock'\n\nexport const OPTIMISTIC_LOCK_HEADER_NAME = 'x-om-ext-optimistic-lock-expected-updated-at'\n\nexport const OPTIMISTIC_LOCK_CONFLICT_CODE = 'optimistic_lock_conflict'\n\nexport const OPTIMISTIC_LOCK_CONFLICT_ERROR = 'record_modified'\n\nexport const OPTIMISTIC_LOCK_ENV_VAR = 'OM_OPTIMISTIC_LOCK'\n\nexport const OPTIMISTIC_LOCK_DEFAULT_PRIORITY = 50\n\nexport type OptimisticLockConflictBody = {\n error: typeof OPTIMISTIC_LOCK_CONFLICT_ERROR\n code: typeof OPTIMISTIC_LOCK_CONFLICT_CODE\n currentUpdatedAt: string\n expectedUpdatedAt: string\n}\n"],
5
5
  "mappings": "AAYO,MAAM,4BAA4B;AAElC,MAAM,8BAA8B;AAEpC,MAAM,gCAAgC;AAEtC,MAAM,iCAAiC;AAEvC,MAAM,0BAA0B;AAEhC,MAAM,mCAAmC;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/optimistic-lock-store.ts"],
4
- "sourcesContent": ["/**\n * Global registry of OSS optimistic-lock readers keyed by `resourceKind`.\n *\n * Multiple modules can contribute readers without conflicting on the\n * single Awilix `crudMutationGuardService` slot: each module calls\n * `registerOptimisticLockReaders({...})` from its `di.ts` at module-load\n * time, and any one of them can register the\n * `crudMutationGuardService` Awilix binding (Awilix replaces same-key\n * registrations, so the *last loaded* wins \u2014 but every binding points\n * to the same store-backed factory, so the resulting guard set is\n * identical regardless of order).\n *\n * Mirrors the `mutation-guard-store.ts` HMR-safe globalThis pattern.\n *\n * Spec: .ai/specs/2026-05-25-oss-optimistic-locking.md\n */\nimport type { OptimisticLockCurrentReader } from './optimistic-lock'\n\nconst GLOBAL_KEY = '__openMercatoOptimisticLockReaders__'\n\nfunction readGlobal(): Record<string, OptimisticLockCurrentReader> {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_KEY]\n if (value && typeof value === 'object') {\n return value as Record<string, OptimisticLockCurrentReader>\n }\n return {}\n } catch {\n return {}\n }\n}\n\nfunction writeGlobal(value: Record<string, OptimisticLockCurrentReader>): void {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_KEY] = value\n } catch {\n // ignore global assignment failures in restricted runtimes\n }\n}\n\n/**\n * Register optimistic-lock readers for one or more `resourceKind` values.\n * Idempotent for same-key calls (later registration overrides earlier).\n */\nexport function registerOptimisticLockReaders(\n readers: Record<string, OptimisticLockCurrentReader>,\n): void {\n const existing = readGlobal()\n writeGlobal({ ...existing, ...readers })\n}\n\n/**\n * Register optimistic-lock readers only for keys that have no reader yet.\n * Use this for fallback / generic registrations (e.g. the auto-registration\n * driven by `makeCrudRoute`) so module-level hand-wired readers \u2014 which\n * register first via `di.ts` \u2014 always win.\n *\n * Returns the set of keys that were actually written, which makes the helper\n * easy to assert on in tests and useful for diagnostics in callers.\n */\nexport function registerOptimisticLockReaderIfAbsent(\n readers: Record<string, OptimisticLockCurrentReader>,\n): string[] {\n const existing = readGlobal()\n const next: Record<string, OptimisticLockCurrentReader> = { ...existing }\n const written: string[] = []\n for (const [key, reader] of Object.entries(readers)) {\n if (!(key in existing)) {\n next[key] = reader\n written.push(key)\n }\n }\n if (written.length > 0) writeGlobal(next)\n return written\n}\n\nexport function getAllOptimisticLockReaders(): Record<string, OptimisticLockCurrentReader> {\n return readGlobal()\n}\n\nexport function clearOptimisticLockReadersForTests(): void {\n try {\n delete (globalThis as Record<string, unknown>)[GLOBAL_KEY]\n } catch {\n // ignore\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * Global registry of OSS optimistic-lock readers keyed by `resourceKind`.\n *\n * Multiple modules can contribute readers without conflicting on the\n * single Awilix `crudMutationGuardService` slot: each module calls\n * `registerOptimisticLockReaders({...})` from its `di.ts` at module-load\n * time, and any one of them can register the\n * `crudMutationGuardService` Awilix binding (Awilix replaces same-key\n * registrations, so the *last loaded* wins \u2014 but every binding points\n * to the same store-backed factory, so the resulting guard set is\n * identical regardless of order).\n *\n * Mirrors the `mutation-guard-store.ts` HMR-safe globalThis pattern.\n *\n * Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md\n */\nimport type { OptimisticLockCurrentReader } from './optimistic-lock'\n\nconst GLOBAL_KEY = '__openMercatoOptimisticLockReaders__'\n\nfunction readGlobal(): Record<string, OptimisticLockCurrentReader> {\n try {\n const value = (globalThis as Record<string, unknown>)[GLOBAL_KEY]\n if (value && typeof value === 'object') {\n return value as Record<string, OptimisticLockCurrentReader>\n }\n return {}\n } catch {\n return {}\n }\n}\n\nfunction writeGlobal(value: Record<string, OptimisticLockCurrentReader>): void {\n try {\n ;(globalThis as Record<string, unknown>)[GLOBAL_KEY] = value\n } catch {\n // ignore global assignment failures in restricted runtimes\n }\n}\n\n/**\n * Register optimistic-lock readers for one or more `resourceKind` values.\n * Idempotent for same-key calls (later registration overrides earlier).\n */\nexport function registerOptimisticLockReaders(\n readers: Record<string, OptimisticLockCurrentReader>,\n): void {\n const existing = readGlobal()\n writeGlobal({ ...existing, ...readers })\n}\n\n/**\n * Register optimistic-lock readers only for keys that have no reader yet.\n * Use this for fallback / generic registrations (e.g. the auto-registration\n * driven by `makeCrudRoute`) so module-level hand-wired readers \u2014 which\n * register first via `di.ts` \u2014 always win.\n *\n * Returns the set of keys that were actually written, which makes the helper\n * easy to assert on in tests and useful for diagnostics in callers.\n */\nexport function registerOptimisticLockReaderIfAbsent(\n readers: Record<string, OptimisticLockCurrentReader>,\n): string[] {\n const existing = readGlobal()\n const next: Record<string, OptimisticLockCurrentReader> = { ...existing }\n const written: string[] = []\n for (const [key, reader] of Object.entries(readers)) {\n if (!(key in existing)) {\n next[key] = reader\n written.push(key)\n }\n }\n if (written.length > 0) writeGlobal(next)\n return written\n}\n\nexport function getAllOptimisticLockReaders(): Record<string, OptimisticLockCurrentReader> {\n return readGlobal()\n}\n\nexport function clearOptimisticLockReadersForTests(): void {\n try {\n delete (globalThis as Record<string, unknown>)[GLOBAL_KEY]\n } catch {\n // ignore\n }\n}\n"],
5
5
  "mappings": "AAkBA,MAAM,aAAa;AAEnB,SAAS,aAA0D;AACjE,MAAI;AACF,UAAM,QAAS,WAAuC,UAAU;AAChE,QAAI,SAAS,OAAO,UAAU,UAAU;AACtC,aAAO;AAAA,IACT;AACA,WAAO,CAAC;AAAA,EACV,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,YAAY,OAA0D;AAC7E,MAAI;AACF;AAAC,IAAC,WAAuC,UAAU,IAAI;AAAA,EACzD,QAAQ;AAAA,EAER;AACF;AAMO,SAAS,8BACd,SACM;AACN,QAAM,WAAW,WAAW;AAC5B,cAAY,EAAE,GAAG,UAAU,GAAG,QAAQ,CAAC;AACzC;AAWO,SAAS,qCACd,SACU;AACV,QAAM,WAAW,WAAW;AAC5B,QAAM,OAAoD,EAAE,GAAG,SAAS;AACxE,QAAM,UAAoB,CAAC;AAC3B,aAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnD,QAAI,EAAE,OAAO,WAAW;AACtB,WAAK,GAAG,IAAI;AACZ,cAAQ,KAAK,GAAG;AAAA,IAClB;AAAA,EACF;AACA,MAAI,QAAQ,SAAS,EAAG,aAAY,IAAI;AACxC,SAAO;AACT;AAEO,SAAS,8BAA2E;AACzF,SAAO,WAAW;AACpB;AAEO,SAAS,qCAA2C;AACzD,MAAI;AACF,WAAQ,WAAuC,UAAU;AAAA,EAC3D,QAAQ;AAAA,EAER;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/optimistic-lock.ts"],
4
- "sourcesContent": ["/**\n * OSS optimistic-locking guard service.\n *\n * Registered as `crudMutationGuardService` (by the platform DI bootstrap and\n * by hand-wiring modules that override the default reader). Compares the\n * client-sent expected `updated_at` (carried via the extension header\n * defined in `optimistic-lock-headers.ts`) against the current DB\n * `updated_at` for the target entity; on mismatch returns HTTP 409 with the\n * structured `OptimisticLockConflictBody`.\n *\n * **Default ON** (Phase 14, 2026-05-27). Activate / scope / disable via\n * `OM_OPTIMISTIC_LOCK`:\n * - unset / empty / whitespace \u2192 ON for every CRUD entity (`{ mode: 'all' }`)\n * - `all` \u2192 all entities (explicit form of the default)\n * - `customers.company,sales.order` \u2192 allow-list (lowercased, trimmed, deduped)\n * - `off` / `false` / `0` / `no` / `disabled` / `none` \u2192 fully disabled\n *\n * The guard is still strictly additive at runtime: clients that do not send\n * the `x-om-ext-optimistic-lock-expected-updated-at` header pass through\n * unchanged, so flipping the default to ON cannot introduce new 409s on\n * existing API consumers. Pages that opt into the round-trip (via\n * `CrudForm`'s `optimisticLockUpdatedAt` prop or by calling\n * `buildOptimisticLockHeader`) gain protection without any per-deployment\n * env change.\n *\n * Cannot be registered as a static `data/guards.ts` `MutationGuard` because\n * the static `validate(input)` receives only `MutationGuardInput` \u2014 no\n * container / em access. Stateful checks that need to read current DB state\n * MUST go through the DI service path (this file).\n *\n * Spec: .ai/specs/2026-05-25-oss-optimistic-locking.md\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type {\n CrudMutationGuardValidateInput,\n CrudMutationGuardValidationResult,\n CrudMutationGuardAfterSuccessInput,\n} from './mutation-guard'\nimport {\n OPTIMISTIC_LOCK_CONFLICT_CODE,\n OPTIMISTIC_LOCK_CONFLICT_ERROR,\n OPTIMISTIC_LOCK_ENV_VAR,\n OPTIMISTIC_LOCK_HEADER_NAME,\n type OptimisticLockConflictBody,\n} from './optimistic-lock-headers'\nimport { getAllOptimisticLockReaders } from './optimistic-lock-store'\n\nexport type OptimisticLockConfig =\n | { mode: 'off' }\n | { mode: 'all' }\n | { mode: 'allowlist'; entities: ReadonlySet<string> }\n\n/**\n * Tokens (case-insensitive, single-token-only) that explicitly disable the\n * guard. Spelled out as a fixed set so tests can pin them; deliberately the\n * same shape `parseBooleanToken` recognises so operators can mirror existing\n * habit. Mixing an off-token with other entities is invalid input \u2014 we treat\n * any presence of an off-token in the comma list as a request to disable.\n */\nconst OPTIMISTIC_LOCK_OFF_TOKENS: ReadonlySet<string> = new Set([\n 'off',\n 'false',\n '0',\n 'no',\n 'disabled',\n 'none',\n])\n\n/**\n * Pure parser for `OM_OPTIMISTIC_LOCK`. Exported separately so tests can\n * exercise the grammar without spinning up the full service.\n *\n * Default is **ON** (`{ mode: 'all' }`) \u2014 unset / empty / whitespace input\n * activates the guard for every CRUD entity. Operators opt out via\n * `OM_OPTIMISTIC_LOCK=off` (or `false` / `0` / `no` / `disabled` / `none`).\n */\nexport function parseOptimisticLockEnv(raw: string | undefined | null): OptimisticLockConfig {\n if (raw == null) return { mode: 'all' }\n const trimmed = String(raw).trim()\n if (trimmed === '') return { mode: 'all' }\n\n const tokens = trimmed\n .split(',')\n .map((token) => token.trim().toLowerCase())\n .filter((token) => token.length > 0)\n\n if (tokens.length === 0) return { mode: 'all' }\n if (tokens.some((token) => OPTIMISTIC_LOCK_OFF_TOKENS.has(token))) return { mode: 'off' }\n if (tokens.includes('all')) return { mode: 'all' }\n\n return { mode: 'allowlist', entities: new Set(tokens) }\n}\n\nexport type OptimisticLockResolverInput = {\n expectedFromHeader: string | null\n resourceKind: string\n resourceId: string\n}\n\n/**\n * Hook reserved for the enterprise `record_locks` module to override token\n * resolution (e.g. read the expected token from a lock record instead of\n * the request header). OSS keeps the default = \"what the client sent\".\n *\n * Documented as part of the enterprise extension contract; not used in OSS\n * itself.\n */\nexport type ResolveExpectedUpdatedAt = (\n input: OptimisticLockResolverInput,\n) => Promise<string | null> | string | null\n\nconst defaultResolveExpectedUpdatedAt: ResolveExpectedUpdatedAt = ({ expectedFromHeader }) =>\n expectedFromHeader\n\nexport type OptimisticLockCurrentReader = (\n em: EntityManager,\n input: { resourceKind: string; resourceId: string; tenantId: string; organizationId: string | null },\n) => Promise<string | null>\n\nexport type GenericOptimisticLockReaderOptions = {\n /** MikroORM entity class. */\n entity: unknown\n /** Primary key field. Defaults to `id`. */\n idField?: string\n /** Tenant scope field. Defaults to `tenantId`. Pass `null` to skip tenant scoping (rare \u2014 only when the entity itself has no `tenantId` column). */\n tenantField?: string | null\n /** Organization scope field. Defaults to `organizationId`. Pass `null` to skip organization scoping. */\n orgField?: string | null\n /** Soft-delete column. Defaults to `deletedAt`. Pass `null` to skip the implicit not-deleted filter. */\n softDeleteField?: string | null\n /** Optional fixed filter merged into every query (e.g. `{ kind: 'company' }` for a discriminated table). */\n extraFilter?: Record<string, unknown>\n /** Optional ORM field name carrying the timestamp. Defaults to `updatedAt`. */\n updatedAtField?: string\n}\n\n/**\n * Build a generic optimistic-lock reader for any ORM entity that follows the\n * platform conventions (`id` + `tenantId` + `organizationId` + `deletedAt` +\n * `updatedAt`). The reader projects only the timestamp column so PII never\n * materializes.\n *\n * Used by `makeCrudRoute` to auto-register one reader per CRUD route at\n * module-load time (see Phase 13 of the OSS optimistic-locking spec).\n * Module authors who need bespoke filtering (e.g. discriminator on a shared\n * table) keep registering their own reader via `registerOptimisticLockReaders`\n * \u2014 those hand-wired registrations win because they land first.\n *\n * Fail-open contract: if the underlying `findOne` throws (missing column,\n * schema drift, mid-migration) the reader returns `null`, which the guard\n * treats as \"entity already gone\" and lets the CRUD path's own 404 fire.\n * We MUST NOT throw out of the reader \u2014 that would 500 every mutation on\n * the affected entity instead of opting it out of the optimistic check.\n */\nexport function createGenericOptimisticLockReader(\n opts: GenericOptimisticLockReaderOptions,\n): OptimisticLockCurrentReader {\n const idField = opts.idField ?? 'id'\n const tenantField = opts.tenantField === null ? null : opts.tenantField ?? 'tenantId'\n const orgField = opts.orgField === null ? null : opts.orgField ?? 'organizationId'\n const softDeleteField = opts.softDeleteField === null ? null : opts.softDeleteField ?? 'deletedAt'\n const updatedAtField = opts.updatedAtField ?? 'updatedAt'\n const extraFilter = opts.extraFilter ?? {}\n\n return async (em, { resourceId, tenantId, organizationId }) => {\n const filter: Record<string, unknown> = { [idField]: resourceId }\n if (tenantField) filter[tenantField] = tenantId\n if (orgField && organizationId) filter[orgField] = organizationId\n if (softDeleteField) filter[softDeleteField] = null\n for (const [key, value] of Object.entries(extraFilter)) filter[key] = value\n\n try {\n const row = await em.findOne(opts.entity as never, filter as never, {\n fields: [updatedAtField] as never,\n })\n if (!row || typeof row !== 'object') return null\n const value = (row as Record<string, unknown>)[updatedAtField]\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string' && value.length > 0) return value\n return null\n } catch {\n return null\n }\n }\n}\n\nexport type OptimisticLockGuardOptions = {\n /** EntityManager resolver. Container-bound via DI in real usage. */\n getEm: () => EntityManager\n /**\n * Maps `resourceKind` \u2192 reader that returns the current\n * `updated_at` as an ISO string (or null when not found).\n *\n * The reader receives the EM so module authors can choose the\n * right `findOne` shape for their entity (`findOneWithDecryption`\n * when sensitive, plain `findOne` otherwise \u2014 but only requesting\n * `updated_at` so no PII materializes).\n *\n * When omitted, the service pulls readers from the shared\n * `optimistic-lock-store` (the recommended pattern for multi-module\n * deployments \u2014 each module registers its own readers via\n * `registerOptimisticLockReaders(...)` at module-load time).\n */\n readers?: Record<string, OptimisticLockCurrentReader>\n /** Override env source (mostly for tests). Defaults to `process.env`. */\n envValue?: string | null\n /** Override the token resolver. Defaults to \"use the header value\". */\n resolveExpected?: ResolveExpectedUpdatedAt\n}\n\nexport type OptimisticLockGuardService = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n /** Exposed for tests / introspection. */\n getConfig: () => OptimisticLockConfig\n}\n\nfunction readHeader(headers: Headers, name: string): string | null {\n const direct = headers.get(name)\n if (typeof direct === 'string' && direct.trim().length > 0) return direct.trim()\n return null\n}\n\n/**\n * Normalize an `updated_at` token to a canonical ISO-8601 string, or `null`\n * when the input cannot be parsed. Exported so the command-level helper\n * (`optimistic-lock-command.ts`) compares timestamps with the EXACT same\n * normalization as the CRUD guard \u2014 otherwise the same instant could compare\n * unequal across the two paths.\n */\nexport function normalizeIsoToken(raw: string): string | null {\n const ms = Date.parse(raw)\n if (!Number.isFinite(ms)) return null\n return new Date(ms).toISOString()\n}\n\nfunction buildConflictBody(currentIso: string, expectedIso: string): OptimisticLockConflictBody {\n return {\n error: OPTIMISTIC_LOCK_CONFLICT_ERROR,\n code: OPTIMISTIC_LOCK_CONFLICT_CODE,\n currentUpdatedAt: currentIso,\n expectedUpdatedAt: expectedIso,\n }\n}\n\n/**\n * Factory for the optimistic-lock guard service.\n *\n * Usage from a module's `di.ts`:\n *\n * ```ts\n * import { asFunction } from 'awilix'\n * import { createOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock'\n *\n * container.register({\n * crudMutationGuardService: asFunction((cradle) => createOptimisticLockGuardService({\n * getEm: () => cradle.em,\n * readers: {\n * 'customers.company': async (em, { resourceId, tenantId }) => {\n * const row = await em.findOne(Company, { id: resourceId, tenantId }, { fields: ['updatedAt'] })\n * return row?.updatedAt ? row.updatedAt.toISOString() : null\n * },\n * },\n * })).singleton(),\n * })\n * ```\n */\nexport function createOptimisticLockGuardService(\n opts: OptimisticLockGuardOptions,\n): OptimisticLockGuardService {\n const envValue = opts.envValue !== undefined ? opts.envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR]\n const config = parseOptimisticLockEnv(envValue)\n const resolveExpected = opts.resolveExpected ?? defaultResolveExpectedUpdatedAt\n const debugEnabled = process.env.OM_OPTIMISTIC_LOCK_DEBUG === '1'\n\n function isEntityEnabled(resourceKind: string): boolean {\n if (config.mode === 'off') return false\n if (config.mode === 'all') return true\n return config.entities.has(resourceKind.toLowerCase())\n }\n\n async function validateMutation(\n input: CrudMutationGuardValidateInput,\n ): Promise<CrudMutationGuardValidationResult> {\n if (config.mode === 'off') {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n if (input.operation !== 'update' && input.operation !== 'delete') {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n if (!isEntityEnabled(input.resourceKind)) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n const readers = opts.readers ?? getAllOptimisticLockReaders()\n const reader = readers[input.resourceKind]\n if (!reader) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n const expectedRaw = readHeader(input.requestHeaders, OPTIMISTIC_LOCK_HEADER_NAME)\n const resolvedExpected = await resolveExpected({\n expectedFromHeader: expectedRaw,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n if (resolvedExpected == null) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n const expectedIso = normalizeIsoToken(resolvedExpected)\n if (expectedIso == null) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n const em = opts.getEm()\n const currentRaw = await reader(em, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n })\n if (currentRaw == null) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n const currentIso = normalizeIsoToken(currentRaw)\n if (currentIso == null) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n if (currentIso === expectedIso) {\n if (debugEnabled) {\n // eslint-disable-next-line no-console\n console.log('[optimistic-lock] match', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n currentIso,\n expectedIso,\n })\n }\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n if (debugEnabled) {\n // eslint-disable-next-line no-console\n console.log('[optimistic-lock] CONFLICT', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n expectedRaw: resolvedExpected,\n expectedIso,\n currentRaw,\n currentIso,\n })\n }\n\n return {\n ok: false,\n status: 409,\n body: buildConflictBody(currentIso, expectedIso),\n }\n }\n\n async function afterMutationSuccess(_input: CrudMutationGuardAfterSuccessInput): Promise<void> {\n // no-op: optimistic check has no post-success cleanup\n }\n\n function getConfig(): OptimisticLockConfig {\n return config\n }\n\n return {\n validateMutation,\n afterMutationSuccess,\n getConfig,\n }\n}\n"],
4
+ "sourcesContent": ["/**\n * OSS optimistic-locking guard service.\n *\n * Registered as `crudMutationGuardService` (by the platform DI bootstrap and\n * by hand-wiring modules that override the default reader). Compares the\n * client-sent expected `updated_at` (carried via the extension header\n * defined in `optimistic-lock-headers.ts`) against the current DB\n * `updated_at` for the target entity; on mismatch returns HTTP 409 with the\n * structured `OptimisticLockConflictBody`.\n *\n * **Default ON** (Phase 14, 2026-05-27). Activate / scope / disable via\n * `OM_OPTIMISTIC_LOCK`:\n * - unset / empty / whitespace \u2192 ON for every CRUD entity (`{ mode: 'all' }`)\n * - `all` \u2192 all entities (explicit form of the default)\n * - `customers.company,sales.order` \u2192 allow-list (lowercased, trimmed, deduped)\n * - `off` / `false` / `0` / `no` / `disabled` / `none` \u2192 fully disabled\n *\n * The guard is still strictly additive at runtime: clients that do not send\n * the `x-om-ext-optimistic-lock-expected-updated-at` header pass through\n * unchanged, so flipping the default to ON cannot introduce new 409s on\n * existing API consumers. Pages that opt into the round-trip (via\n * `CrudForm`'s `optimisticLockUpdatedAt` prop or by calling\n * `buildOptimisticLockHeader`) gain protection without any per-deployment\n * env change.\n *\n * Cannot be registered as a static `data/guards.ts` `MutationGuard` because\n * the static `validate(input)` receives only `MutationGuardInput` \u2014 no\n * container / em access. Stateful checks that need to read current DB state\n * MUST go through the DI service path (this file).\n *\n * Spec: .ai/specs/implemented/2026-05-25-oss-optimistic-locking.md\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type {\n CrudMutationGuardValidateInput,\n CrudMutationGuardValidationResult,\n CrudMutationGuardAfterSuccessInput,\n} from './mutation-guard'\nimport {\n OPTIMISTIC_LOCK_CONFLICT_CODE,\n OPTIMISTIC_LOCK_CONFLICT_ERROR,\n OPTIMISTIC_LOCK_ENV_VAR,\n OPTIMISTIC_LOCK_HEADER_NAME,\n type OptimisticLockConflictBody,\n} from './optimistic-lock-headers'\nimport { getAllOptimisticLockReaders } from './optimistic-lock-store'\n\nexport type OptimisticLockConfig =\n | { mode: 'off' }\n | { mode: 'all' }\n | { mode: 'allowlist'; entities: ReadonlySet<string> }\n\n/**\n * Tokens (case-insensitive, single-token-only) that explicitly disable the\n * guard. Spelled out as a fixed set so tests can pin them; deliberately the\n * same shape `parseBooleanToken` recognises so operators can mirror existing\n * habit. Mixing an off-token with other entities is invalid input \u2014 we treat\n * any presence of an off-token in the comma list as a request to disable.\n */\nconst OPTIMISTIC_LOCK_OFF_TOKENS: ReadonlySet<string> = new Set([\n 'off',\n 'false',\n '0',\n 'no',\n 'disabled',\n 'none',\n])\n\n/**\n * Pure parser for `OM_OPTIMISTIC_LOCK`. Exported separately so tests can\n * exercise the grammar without spinning up the full service.\n *\n * Default is **ON** (`{ mode: 'all' }`) \u2014 unset / empty / whitespace input\n * activates the guard for every CRUD entity. Operators opt out via\n * `OM_OPTIMISTIC_LOCK=off` (or `false` / `0` / `no` / `disabled` / `none`).\n */\nexport function parseOptimisticLockEnv(raw: string | undefined | null): OptimisticLockConfig {\n if (raw == null) return { mode: 'all' }\n const trimmed = String(raw).trim()\n if (trimmed === '') return { mode: 'all' }\n\n const tokens = trimmed\n .split(',')\n .map((token) => token.trim().toLowerCase())\n .filter((token) => token.length > 0)\n\n if (tokens.length === 0) return { mode: 'all' }\n if (tokens.some((token) => OPTIMISTIC_LOCK_OFF_TOKENS.has(token))) return { mode: 'off' }\n if (tokens.includes('all')) return { mode: 'all' }\n\n return { mode: 'allowlist', entities: new Set(tokens) }\n}\n\nexport type OptimisticLockResolverInput = {\n expectedFromHeader: string | null\n resourceKind: string\n resourceId: string\n}\n\n/**\n * Hook reserved for the enterprise `record_locks` module to override token\n * resolution (e.g. read the expected token from a lock record instead of\n * the request header). OSS keeps the default = \"what the client sent\".\n *\n * Documented as part of the enterprise extension contract; not used in OSS\n * itself.\n */\nexport type ResolveExpectedUpdatedAt = (\n input: OptimisticLockResolverInput,\n) => Promise<string | null> | string | null\n\nconst defaultResolveExpectedUpdatedAt: ResolveExpectedUpdatedAt = ({ expectedFromHeader }) =>\n expectedFromHeader\n\nexport type OptimisticLockCurrentReader = (\n em: EntityManager,\n input: { resourceKind: string; resourceId: string; tenantId: string; organizationId: string | null },\n) => Promise<string | null>\n\nexport type GenericOptimisticLockReaderOptions = {\n /** MikroORM entity class. */\n entity: unknown\n /** Primary key field. Defaults to `id`. */\n idField?: string\n /** Tenant scope field. Defaults to `tenantId`. Pass `null` to skip tenant scoping (rare \u2014 only when the entity itself has no `tenantId` column). */\n tenantField?: string | null\n /** Organization scope field. Defaults to `organizationId`. Pass `null` to skip organization scoping. */\n orgField?: string | null\n /** Soft-delete column. Defaults to `deletedAt`. Pass `null` to skip the implicit not-deleted filter. */\n softDeleteField?: string | null\n /** Optional fixed filter merged into every query (e.g. `{ kind: 'company' }` for a discriminated table). */\n extraFilter?: Record<string, unknown>\n /** Optional ORM field name carrying the timestamp. Defaults to `updatedAt`. */\n updatedAtField?: string\n}\n\n/**\n * Build a generic optimistic-lock reader for any ORM entity that follows the\n * platform conventions (`id` + `tenantId` + `organizationId` + `deletedAt` +\n * `updatedAt`). The reader projects only the timestamp column so PII never\n * materializes.\n *\n * Used by `makeCrudRoute` to auto-register one reader per CRUD route at\n * module-load time (see Phase 13 of the OSS optimistic-locking spec).\n * Module authors who need bespoke filtering (e.g. discriminator on a shared\n * table) keep registering their own reader via `registerOptimisticLockReaders`\n * \u2014 those hand-wired registrations win because they land first.\n *\n * Fail-open contract: if the underlying `findOne` throws (missing column,\n * schema drift, mid-migration) the reader returns `null`, which the guard\n * treats as \"entity already gone\" and lets the CRUD path's own 404 fire.\n * We MUST NOT throw out of the reader \u2014 that would 500 every mutation on\n * the affected entity instead of opting it out of the optimistic check.\n */\nexport function createGenericOptimisticLockReader(\n opts: GenericOptimisticLockReaderOptions,\n): OptimisticLockCurrentReader {\n const idField = opts.idField ?? 'id'\n const tenantField = opts.tenantField === null ? null : opts.tenantField ?? 'tenantId'\n const orgField = opts.orgField === null ? null : opts.orgField ?? 'organizationId'\n const softDeleteField = opts.softDeleteField === null ? null : opts.softDeleteField ?? 'deletedAt'\n const updatedAtField = opts.updatedAtField ?? 'updatedAt'\n const extraFilter = opts.extraFilter ?? {}\n\n return async (em, { resourceId, tenantId, organizationId }) => {\n const filter: Record<string, unknown> = { [idField]: resourceId }\n if (tenantField) filter[tenantField] = tenantId\n if (orgField && organizationId) filter[orgField] = organizationId\n if (softDeleteField) filter[softDeleteField] = null\n for (const [key, value] of Object.entries(extraFilter)) filter[key] = value\n\n try {\n const row = await em.findOne(opts.entity as never, filter as never, {\n fields: [updatedAtField] as never,\n })\n if (!row || typeof row !== 'object') return null\n const value = (row as Record<string, unknown>)[updatedAtField]\n if (value instanceof Date) return value.toISOString()\n if (typeof value === 'string' && value.length > 0) return value\n return null\n } catch {\n return null\n }\n }\n}\n\nexport type OptimisticLockGuardOptions = {\n /** EntityManager resolver. Container-bound via DI in real usage. */\n getEm: () => EntityManager\n /**\n * Maps `resourceKind` \u2192 reader that returns the current\n * `updated_at` as an ISO string (or null when not found).\n *\n * The reader receives the EM so module authors can choose the\n * right `findOne` shape for their entity (`findOneWithDecryption`\n * when sensitive, plain `findOne` otherwise \u2014 but only requesting\n * `updated_at` so no PII materializes).\n *\n * When omitted, the service pulls readers from the shared\n * `optimistic-lock-store` (the recommended pattern for multi-module\n * deployments \u2014 each module registers its own readers via\n * `registerOptimisticLockReaders(...)` at module-load time).\n */\n readers?: Record<string, OptimisticLockCurrentReader>\n /** Override env source (mostly for tests). Defaults to `process.env`. */\n envValue?: string | null\n /** Override the token resolver. Defaults to \"use the header value\". */\n resolveExpected?: ResolveExpectedUpdatedAt\n}\n\nexport type OptimisticLockGuardService = {\n validateMutation: (input: CrudMutationGuardValidateInput) => Promise<CrudMutationGuardValidationResult>\n afterMutationSuccess: (input: CrudMutationGuardAfterSuccessInput) => Promise<void>\n /** Exposed for tests / introspection. */\n getConfig: () => OptimisticLockConfig\n}\n\nfunction readHeader(headers: Headers, name: string): string | null {\n const direct = headers.get(name)\n if (typeof direct === 'string' && direct.trim().length > 0) return direct.trim()\n return null\n}\n\n/**\n * Normalize an `updated_at` token to a canonical ISO-8601 string, or `null`\n * when the input cannot be parsed. Exported so the command-level helper\n * (`optimistic-lock-command.ts`) compares timestamps with the EXACT same\n * normalization as the CRUD guard \u2014 otherwise the same instant could compare\n * unequal across the two paths.\n */\nexport function normalizeIsoToken(raw: string): string | null {\n const ms = Date.parse(raw)\n if (!Number.isFinite(ms)) return null\n return new Date(ms).toISOString()\n}\n\nfunction buildConflictBody(currentIso: string, expectedIso: string): OptimisticLockConflictBody {\n return {\n error: OPTIMISTIC_LOCK_CONFLICT_ERROR,\n code: OPTIMISTIC_LOCK_CONFLICT_CODE,\n currentUpdatedAt: currentIso,\n expectedUpdatedAt: expectedIso,\n }\n}\n\n/**\n * Factory for the optimistic-lock guard service.\n *\n * Usage from a module's `di.ts`:\n *\n * ```ts\n * import { asFunction } from 'awilix'\n * import { createOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock'\n *\n * container.register({\n * crudMutationGuardService: asFunction((cradle) => createOptimisticLockGuardService({\n * getEm: () => cradle.em,\n * readers: {\n * 'customers.company': async (em, { resourceId, tenantId }) => {\n * const row = await em.findOne(Company, { id: resourceId, tenantId }, { fields: ['updatedAt'] })\n * return row?.updatedAt ? row.updatedAt.toISOString() : null\n * },\n * },\n * })).singleton(),\n * })\n * ```\n */\nexport function createOptimisticLockGuardService(\n opts: OptimisticLockGuardOptions,\n): OptimisticLockGuardService {\n const envValue = opts.envValue !== undefined ? opts.envValue : process.env[OPTIMISTIC_LOCK_ENV_VAR]\n const config = parseOptimisticLockEnv(envValue)\n const resolveExpected = opts.resolveExpected ?? defaultResolveExpectedUpdatedAt\n const debugEnabled = process.env.OM_OPTIMISTIC_LOCK_DEBUG === '1'\n\n function isEntityEnabled(resourceKind: string): boolean {\n if (config.mode === 'off') return false\n if (config.mode === 'all') return true\n return config.entities.has(resourceKind.toLowerCase())\n }\n\n async function validateMutation(\n input: CrudMutationGuardValidateInput,\n ): Promise<CrudMutationGuardValidationResult> {\n if (config.mode === 'off') {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n if (input.operation !== 'update' && input.operation !== 'delete') {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n if (!isEntityEnabled(input.resourceKind)) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n const readers = opts.readers ?? getAllOptimisticLockReaders()\n const reader = readers[input.resourceKind]\n if (!reader) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n const expectedRaw = readHeader(input.requestHeaders, OPTIMISTIC_LOCK_HEADER_NAME)\n const resolvedExpected = await resolveExpected({\n expectedFromHeader: expectedRaw,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n if (resolvedExpected == null) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n const expectedIso = normalizeIsoToken(resolvedExpected)\n if (expectedIso == null) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n const em = opts.getEm()\n const currentRaw = await reader(em, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n })\n if (currentRaw == null) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n const currentIso = normalizeIsoToken(currentRaw)\n if (currentIso == null) {\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n if (currentIso === expectedIso) {\n if (debugEnabled) {\n // eslint-disable-next-line no-console\n console.log('[optimistic-lock] match', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n currentIso,\n expectedIso,\n })\n }\n return { ok: true, shouldRunAfterSuccess: false }\n }\n\n if (debugEnabled) {\n // eslint-disable-next-line no-console\n console.log('[optimistic-lock] CONFLICT', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n expectedRaw: resolvedExpected,\n expectedIso,\n currentRaw,\n currentIso,\n })\n }\n\n return {\n ok: false,\n status: 409,\n body: buildConflictBody(currentIso, expectedIso),\n }\n }\n\n async function afterMutationSuccess(_input: CrudMutationGuardAfterSuccessInput): Promise<void> {\n // no-op: optimistic check has no post-success cleanup\n }\n\n function getConfig(): OptimisticLockConfig {\n return config\n }\n\n return {\n validateMutation,\n afterMutationSuccess,\n getConfig,\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,8 @@ import { setRecordCustomFields } from "@open-mercato/core/modules/entities/lib/h
3
3
  import { validateCustomFieldValuesServer } from "@open-mercato/core/modules/entities/lib/validation";
4
4
  import { sanitizeCustomFieldHtmlRichTextValuesServer } from "@open-mercato/core/modules/entities/lib/htmlRichTextSanitizer";
5
5
  import { CrudHttpError } from "../crud/errors.js";
6
+ import { resolveRegisteredEntityTableName } from "../query/engine.js";
7
+ import { getEntityIds } from "../encryption/entityIds.js";
6
8
  import { normalizeCustomFieldValues } from "../custom-fields/normalize.js";
7
9
  import { parseBooleanToken } from "../boolean.js";
8
10
  import { isEventDeclared } from "../../modules/events/index.js";
@@ -29,6 +31,22 @@ function shouldTriggerCoverageRefresh(entityType, tenantId) {
29
31
  coverageRefreshTracker.set(key, now);
30
32
  return true;
31
33
  }
34
+ const SYSTEM_ENTITY_RECORDS_BLOCKED_CODE = "system_entity_records_blocked";
35
+ function isOrmBackedSystemEntityId(em, entityId) {
36
+ const registry = getEntityIds(false);
37
+ const moduleIds = Object.values(registry).flatMap((moduleEntities) => Object.values(moduleEntities ?? {}));
38
+ if (moduleIds.length > 0 && !moduleIds.includes(entityId)) return false;
39
+ return resolveRegisteredEntityTableName(em, entityId) !== null;
40
+ }
41
+ function assertCustomEntityStorageEntityId(em, entityId) {
42
+ if (isOrmBackedSystemEntityId(em, entityId)) {
43
+ throw new CrudHttpError(400, {
44
+ error: "Records are available for custom entities only",
45
+ code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE,
46
+ entityId
47
+ });
48
+ }
49
+ }
32
50
  class DefaultDataEngine {
33
51
  constructor(em, container) {
34
52
  this.em = em;
@@ -132,6 +150,7 @@ class DefaultDataEngine {
132
150
  }
133
151
  }
134
152
  async createCustomEntityRecord(opts) {
153
+ assertCustomEntityStorageEntityId(this.em, opts.entityId);
135
154
  const db = this.getKysely();
136
155
  await this.ensureStorageTableExists();
137
156
  const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {
@@ -202,6 +221,7 @@ class DefaultDataEngine {
202
221
  return { id };
203
222
  }
204
223
  async updateCustomEntityRecord(opts) {
224
+ assertCustomEntityStorageEntityId(this.em, opts.entityId);
205
225
  const db = this.getKysely();
206
226
  const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {
207
227
  entityId: opts.entityId,
@@ -261,6 +281,7 @@ class DefaultDataEngine {
261
281
  }
262
282
  }
263
283
  async deleteCustomEntityRecord(opts) {
284
+ assertCustomEntityStorageEntityId(this.em, opts.entityId);
264
285
  const db = this.getKysely();
265
286
  const id = String(opts.recordId);
266
287
  const orgId = opts.organizationId ?? null;
@@ -477,6 +498,9 @@ class DefaultDataEngine {
477
498
  }
478
499
  export {
479
500
  DefaultDataEngine,
480
- __resetUndeclaredEventWarningsForTests
501
+ SYSTEM_ENTITY_RECORDS_BLOCKED_CODE,
502
+ __resetUndeclaredEventWarningsForTests,
503
+ assertCustomEntityStorageEntityId,
504
+ isOrmBackedSystemEntityId
481
505
  };
482
506
  //# sourceMappingURL=engine.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/data/engine.ts"],
4
- "sourcesContent": ["import type { EntityData, EntityName, FilterQuery, RequiredEntityData } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { type Kysely, sql } from 'kysely'\nimport { setRecordCustomFields } from '@open-mercato/core/modules/entities/lib/helpers'\nimport { validateCustomFieldValuesServer } from '@open-mercato/core/modules/entities/lib/validation'\nimport { sanitizeCustomFieldHtmlRichTextValuesServer } from '@open-mercato/core/modules/entities/lib/htmlRichTextSanitizer'\nimport type { EventBus } from '@open-mercato/events/types'\nimport type {\n CrudEventAction,\n CrudEventsConfig,\n CrudIndexerConfig,\n CrudEntityIdentifiers,\n} from '../crud/types'\nimport { CrudHttpError } from '../crud/errors'\nimport { normalizeCustomFieldValues } from '../custom-fields/normalize'\nimport { parseBooleanToken } from '../boolean'\nimport { isEventDeclared } from '../../modules/events'\n\nconst undeclaredEventWarned = new Set<string>()\n\nfunction warnIfUndeclaredEvent(eventName: string, context: string): void {\n if (isEventDeclared(eventName)) return\n if (undeclaredEventWarned.has(eventName)) return\n undeclaredEventWarned.add(eventName)\n console.warn(\n `[data-engine] ${context} is emitting undeclared event \"${eventName}\". ` +\n `Declare it in the owning module's events.ts (createModuleEvents) so the event registry stays authoritative.`,\n )\n}\n\n/** Internal: clear the undeclared-event warning cache. Exposed for tests. */\nexport function __resetUndeclaredEventWarningsForTests(): void {\n undeclaredEventWarned.clear()\n}\n\nconst COVERAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000\nconst coverageRefreshTracker = new Map<string, number>()\n\nfunction shouldTriggerCoverageRefresh(entityType: string | undefined, tenantId: string | null): boolean {\n if (!entityType) return false\n const key = `${entityType}|${tenantId ?? '__null__'}`\n const now = Date.now()\n const last = coverageRefreshTracker.get(key) ?? 0\n if (now - last < COVERAGE_REFRESH_INTERVAL_MS) return false\n coverageRefreshTracker.set(key, now)\n return true\n}\n\ntype CustomEntityValues = Record<string, unknown>\n\ntype QueuedCrudSideEffect = {\n action: CrudEventAction\n entity: unknown\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n events?: CrudEventsConfig<unknown>\n indexer?: CrudIndexerConfig<unknown>\n}\n\nexport interface DataEngine {\n setCustomFields(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, string | number | boolean | null | undefined | Array<string | number | boolean | null | undefined>>\n notify?: boolean // default true -> emit '<module>.<entity>.updated'\n }): Promise<void>\n\n // Storage for user-defined entities (doc-based)\n createCustomEntityRecord(opts: {\n entityId: string // '<module>:<entity>'\n recordId?: string // optional; auto-generate if not provided\n organizationId?: string | null\n tenantId?: string | null\n values: CustomEntityValues\n notify?: boolean // keep event emitting as it is via setCustomFields (updated)\n }): Promise<{ id: string }>\n\n updateCustomEntityRecord(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: CustomEntityValues\n notify?: boolean // keep event emitting as it is via setCustomFields (updated)\n }): Promise<void>\n\n deleteCustomEntityRecord(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n soft?: boolean // default true: sets deleted_at\n notify?: boolean // keep event emitting as it is (no extra events here)\n }): Promise<void>\n\n // Generic ORM-backed entity operations used by CrudFactory\n createOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n data: EntityData<T>\n }): Promise<T>\n\n updateOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n apply: (current: T) => Promise<void> | void\n }): Promise<T | null>\n\n deleteOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n soft?: boolean\n softDeleteField?: keyof T & string\n }): Promise<T | null>\n\n emitOrmEntityEvent<T>(opts: {\n action: CrudEventAction\n entity: T\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): Promise<void>\n\n markOrmEntityChange<T>(opts: {\n action: CrudEventAction\n entity: T | null | undefined\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): void\n\n flushOrmEntityChanges(): Promise<void>\n}\n\nexport class DefaultDataEngine implements DataEngine {\n private pendingSideEffects = new Map<string, QueuedCrudSideEffect>()\n constructor(private em: EntityManager, private container: AwilixContainer) {}\n\n async setCustomFields(opts: Parameters<DataEngine['setCustomFields']>[0]): Promise<void> {\n const { entityId, recordId, organizationId = null, tenantId = null, values } = opts\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId,\n organizationId,\n tenantId,\n values,\n })\n await this.validateCustomFieldValues(entityId, organizationId, tenantId, sanitizedValues as Record<string, unknown>)\n let encryptionService: any = null\n try {\n encryptionService = this.container.resolve('tenantEncryptionService') as any\n } catch {\n encryptionService = null\n }\n await setRecordCustomFields(this.em, {\n entityId,\n recordId,\n organizationId,\n tenantId,\n values: sanitizedValues,\n encryptionService,\n })\n if (opts.notify !== false) {\n let bus: EventBus | null = null\n try {\n bus = (this.container.resolve('eventBus') as EventBus)\n } catch {\n bus = null\n }\n if (bus) {\n const [mod, ent] = (entityId || '').split(':')\n if (mod && ent) {\n const eventName = `${mod}.${ent}.updated`\n warnIfUndeclaredEvent(eventName, 'setCustomFields')\n try {\n await bus.emitEvent(eventName, { id: recordId, organizationId, tenantId }, { persistent: true })\n } catch {\n // non-blocking\n }\n }\n }\n }\n }\n\n private normalizeDocValues(values: CustomEntityValues): CustomEntityValues {\n const out: CustomEntityValues = {}\n for (const [k, v] of Object.entries(values || {})) {\n // Never allow callers to override reserved identifiers in the doc\n if (k === 'id' || k === 'entity_id' || k === 'entityId') continue\n // Accept both 'cf_<key>' and 'cf:<key>' inputs and normalize to 'cf:<key>'\n if (k.startsWith('cf_')) out[`cf:${k.slice(3)}`] = v\n else out[k] = v\n }\n return out\n }\n\n private backcompatEavEnabled(): boolean {\n try {\n return parseBooleanToken(process.env.ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM ?? '') === true\n } catch { return false }\n }\n\n private getKysely(): Kysely<any> {\n return this.em.getKysely<any>()\n }\n\n private async ensureStorageTableExists(): Promise<void> {\n const db = this.getKysely()\n const exists = await db\n .selectFrom('information_schema.tables' as any)\n .select(sql`1`.as('present'))\n .where('table_name' as any, '=', 'custom_entities_storage')\n .executeTakeFirst()\n if (!exists) {\n throw new Error('custom_entities_storage table is missing. Run migrations (yarn db:migrate).')\n }\n }\n\n private normalizeValuesForValidation(values: Record<string, unknown> | undefined | null): Record<string, unknown> {\n if (!values) return {}\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (value === undefined) continue\n if (key.startsWith('cf_') || key.startsWith('cf:')) {\n const normalized = key.slice(3)\n if (normalized) out[normalized] = value\n continue\n }\n out[key] = value\n }\n return out\n }\n\n private async validateCustomFieldValues(\n entityId: string,\n organizationId: string | null,\n tenantId: string | null,\n values: Record<string, unknown> | undefined | null,\n ): Promise<void> {\n const prepared = this.normalizeValuesForValidation(values)\n if (!entityId || Object.keys(prepared).length === 0) return\n const result = await validateCustomFieldValuesServer(this.em, {\n entityId,\n organizationId,\n tenantId,\n values: prepared,\n })\n if (!result.ok) {\n throw new CrudHttpError(400, { error: 'Validation failed', fields: result.fieldErrors })\n }\n }\n\n async createCustomEntityRecord(opts: Parameters<DataEngine['createCustomEntityRecord']>[0]): Promise<{ id: string }> {\n const db = this.getKysely()\n await this.ensureStorageTableExists()\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId: opts.entityId,\n organizationId: opts.organizationId ?? null,\n tenantId: opts.tenantId ?? null,\n values: opts.values || {},\n })\n await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues)\n const rawId = String(opts.recordId ?? '').trim()\n const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(rawId)\n const sentinel = rawId.toLowerCase()\n const shouldGenerate = !rawId || !isUuid || sentinel === 'create' || sentinel === 'new' || sentinel === 'null' || sentinel === 'undefined'\n const id = shouldGenerate ? ((): string => {\n const g = globalThis as { crypto?: { randomUUID?: () => string } }\n if (g.crypto?.randomUUID) return g.crypto.randomUUID()\n // Fallback UUIDv4 generator\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === 'x' ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n })() : rawId\n const orgId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const doc: Record<string, unknown> = { id, ...this.normalizeDocValues(sanitizedValues || {}) }\n\n const now = sql`now()`\n const payload = {\n entity_type: opts.entityId,\n entity_id: id,\n organization_id: orgId,\n tenant_id: tenantId,\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: now,\n created_at: now,\n deleted_at: null,\n }\n\n // Upsert by scoped uniqueness\n try {\n await db\n .insertInto('custom_entities_storage' as any)\n .values(payload as any)\n .onConflict((oc) => oc\n .columns(['entity_type', 'entity_id', 'organization_id'])\n .doUpdateSet({\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any))\n .execute()\n } catch {\n // Fallback for global scope uniqueness\n try {\n const updated = await db\n .updateTable('custom_entities_storage' as any)\n .set({\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any)\n .where('entity_type' as any, '=', opts.entityId)\n .where('entity_id' as any, '=', id)\n .where('organization_id' as any, orgId === null ? 'is' : '=', orgId as any)\n .executeTakeFirst()\n if (!updated || Number(updated.numUpdatedRows ?? 0) === 0) {\n await db.insertInto('custom_entities_storage' as any).values(payload as any).execute()\n }\n } catch (err) {\n // Surface a clear error so it doesn't silently fall back only to EAV\n throw err\n }\n }\n\n // Optional EAV backward compatibility (disabled by default)\n if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {\n await this.setCustomFields({\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: tenantId,\n values: normalizeCustomFieldValues(sanitizedValues),\n notify: opts.notify, // defaults to true\n })\n }\n\n return { id }\n }\n\n async updateCustomEntityRecord(opts: Parameters<DataEngine['updateCustomEntityRecord']>[0]): Promise<void> {\n const db = this.getKysely()\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId: opts.entityId,\n organizationId: opts.organizationId ?? null,\n tenantId: opts.tenantId ?? null,\n values: opts.values || {},\n })\n await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues)\n const id = String(opts.recordId)\n const orgId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n\n // Merge doc shallowly: load existing doc and overlay\n await this.ensureStorageTableExists()\n const applyScope = <T extends { where: (col: any, op: any, val?: any) => T }>(q: T) => {\n let chain = q.where('entity_type' as any, '=', opts.entityId)\n chain = chain.where('entity_id' as any, '=', id)\n chain = orgId === null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', orgId)\n return chain\n }\n const row = await applyScope(\n db.selectFrom('custom_entities_storage' as any).select(['doc' as any])\n ).executeTakeFirst()\n const prevDoc: Record<string, unknown> = (row as any)?.doc || { id }\n const nextDoc: Record<string, unknown> = { ...prevDoc, ...this.normalizeDocValues(sanitizedValues || {}), id }\n try {\n const updated = await applyScope(\n db.updateTable('custom_entities_storage' as any).set({\n doc: sql`${JSON.stringify(nextDoc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any) as any\n ).executeTakeFirst()\n if (!updated || Number((updated as any).numUpdatedRows ?? 0) === 0) {\n await db.insertInto('custom_entities_storage' as any).values({\n entity_type: opts.entityId,\n entity_id: id,\n organization_id: orgId,\n tenant_id: tenantId,\n doc: sql`${JSON.stringify(nextDoc)}::jsonb`,\n created_at: sql`now()`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any).execute()\n }\n } catch (err) {\n throw err\n }\n\n // Optional EAV backward compatibility (disabled by default)\n if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {\n await this.setCustomFields({\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: tenantId,\n values: normalizeCustomFieldValues(sanitizedValues),\n notify: opts.notify, // defaults to true\n })\n }\n }\n\n async deleteCustomEntityRecord(opts: Parameters<DataEngine['deleteCustomEntityRecord']>[0]): Promise<void> {\n const db = this.getKysely()\n const id = String(opts.recordId)\n const orgId = opts.organizationId ?? null\n const soft = opts.soft !== false\n\n const applyScope = <T extends { where: (col: any, op: any, val?: any) => T }>(q: T) => {\n let chain = q.where('entity_type' as any, '=', opts.entityId)\n chain = chain.where('entity_id' as any, '=', id)\n chain = orgId === null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', orgId)\n return chain\n }\n\n if (soft) {\n await applyScope(\n db.updateTable('custom_entities_storage' as any).set({\n deleted_at: sql`now()`,\n updated_at: sql`now()`,\n } as any) as any\n ).execute()\n } else {\n await applyScope(db.deleteFrom('custom_entities_storage' as any) as any).execute()\n }\n\n // Soft-delete EAV values to preserve current behavior\n try {\n const { CustomFieldValue } = await import('@open-mercato/core/modules/entities/data/entities')\n const values = await this.em.find(CustomFieldValue, {\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: opts.tenantId ?? null,\n })\n const now = new Date()\n const mutated = values.filter((record) => {\n if (record.deletedAt) return false\n record.deletedAt = now\n return true\n })\n if (mutated.length) {\n for (const record of values) this.em.persist(record)\n await this.em.flush()\n }\n } catch { /* non-blocking */ }\n }\n\n async createOrmEntity<T extends object>(opts: { entity: EntityName<T>; data: EntityData<T> }): Promise<T> {\n const entity = this.em.create(\n opts.entity as EntityName<T>,\n opts.data as unknown as RequiredEntityData<T>\n )\n await this.em.persist(entity).flush()\n return entity\n }\n\n async updateOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n apply: (current: T) => Promise<void> | void\n }): Promise<T | null> {\n const current = await this.em.findOne(opts.entity as EntityName<T>, opts.where as FilterQuery<NoInfer<T>>)\n if (!current) return null\n await opts.apply(current)\n await this.em.persist(current).flush()\n return current\n }\n\n async deleteOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n soft?: boolean\n softDeleteField?: keyof T & string\n }): Promise<T | null> {\n const current = await this.em.findOne(opts.entity as EntityName<T>, opts.where as FilterQuery<NoInfer<T>>)\n if (!current) return null\n if (opts.soft !== false) {\n const field = opts.softDeleteField || ('deletedAt' as keyof T & string)\n if (typeof current === 'object' && current !== null) {\n ;(current as Record<string, unknown>)[field] = new Date()\n await this.em.persist(current).flush()\n }\n } else {\n await this.em.remove(current).flush()\n }\n return current\n }\n\n async emitOrmEntityEvent<T>(opts: {\n action: CrudEventAction\n entity: T\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): Promise<void> {\n const { action, entity, events, indexer, identifiers, syncOrigin } = opts\n if (!events && !indexer) return\n if (!identifiers?.id) return\n\n let bus: EventBus | null = null\n try {\n bus = (this.container.resolve('eventBus') as EventBus)\n } catch {\n bus = null\n }\n if (!bus) return\n\n const ctx = {\n action,\n entity,\n identifiers: {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n },\n syncOrigin: syncOrigin ?? null,\n }\n\n if (events) {\n const eventName = `${events.module}.${events.entity}.${action}`\n warnIfUndeclaredEvent(eventName, 'emitOrmEntityEvent')\n const payload = events.buildPayload\n ? events.buildPayload(ctx)\n : {\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),\n }\n try {\n await bus.emitEvent(eventName, payload, {\n persistent: !!events.persistent,\n tenantId: ctx.identifiers.tenantId ?? null,\n organizationId: ctx.identifiers.organizationId ?? null,\n })\n } catch {\n // non-blocking\n }\n }\n\n if (indexer) {\n const resolveCoverageBaseDelta = (): number | undefined => {\n if (action === 'created') return 1\n if (action === 'deleted') return -1\n return undefined\n }\n const coverageBaseDelta = resolveCoverageBaseDelta()\n\n if (action === 'deleted') {\n const payload = indexer.buildDeletePayload\n ? indexer.buildDeletePayload(ctx)\n : {\n entityType: indexer.entityType,\n recordId: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }\n const enrichedPayload = payload as Record<string, unknown>\n enrichedPayload.crudAction = action\n if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta\n if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin\n // Await the index update so query-index reads (the `customValues`/scalar\n // projection that list endpoints serve) are consistent the moment the write\n // returns. The subscriber removes the projection row + tokens synchronously and\n // defers the coverage recompute + fulltext delete, so this stays bounded.\n // Errors are logged, not thrown \u2014 index drift never fails the originating write.\n await bus.emitEvent('query_index.delete_one', enrichedPayload).catch((err: unknown) => {\n console.error('[data-engine] query_index.delete_one emit failed', err)\n })\n } else {\n const payload = indexer.buildUpsertPayload\n ? indexer.buildUpsertPayload(ctx)\n : {\n entityType: indexer.entityType,\n recordId: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }\n const enrichedPayload = payload as Record<string, unknown>\n enrichedPayload.crudAction = action\n if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta\n if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin\n // Await the projection upsert so list reads observe the new doc immediately\n // (see delete_one above). The subscriber updates `entity_indexes` synchronously\n // and defers the heavy token-reindex pipeline (build doc + encrypt + decrypt +\n // tokenize + DELETE + chunked INSERT) so write latency stays bounded.\n await bus.emitEvent('query_index.upsert_one', enrichedPayload).catch((err: unknown) => {\n console.error('[data-engine] query_index.upsert_one emit failed', err)\n })\n }\n\n if (shouldTriggerCoverageRefresh(indexer.entityType, ctx.identifiers.tenantId ?? null)) {\n void bus.emitEvent('query_index.coverage.refresh', {\n entityType: indexer.entityType,\n tenantId: ctx.identifiers.tenantId ?? null,\n organizationId: null,\n delayMs: 0,\n }).catch(() => undefined)\n }\n }\n }\n\n markOrmEntityChange<T>(opts: {\n action: CrudEventAction\n entity: T | null | undefined\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): void {\n const { entity, identifiers } = opts\n if (!entity) return\n if (!identifiers?.id) return\n const key = this.buildSideEffectKey(opts.action, identifiers)\n const existing = this.pendingSideEffects.get(key)\n if (existing) {\n existing.entity = entity\n existing.identifiers = {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n }\n existing.syncOrigin = opts.syncOrigin ?? null\n if (opts.events) existing.events = opts.events as CrudEventsConfig<unknown>\n if (opts.indexer) existing.indexer = opts.indexer as CrudIndexerConfig<unknown>\n this.pendingSideEffects.set(key, existing)\n return\n }\n const entry: QueuedCrudSideEffect = {\n action: opts.action,\n entity,\n identifiers: {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n },\n syncOrigin: opts.syncOrigin ?? null,\n }\n if (opts.events) entry.events = opts.events as CrudEventsConfig<unknown>\n if (opts.indexer) entry.indexer = opts.indexer as CrudIndexerConfig<unknown>\n this.pendingSideEffects.set(key, entry)\n }\n\n async flushOrmEntityChanges(): Promise<void> {\n if (!this.pendingSideEffects.size) return\n const entries = Array.from(this.pendingSideEffects.values())\n this.pendingSideEffects.clear()\n for (const entry of entries) {\n try {\n await this.emitOrmEntityEvent({\n action: entry.action,\n entity: entry.entity,\n identifiers: entry.identifiers,\n syncOrigin: entry.syncOrigin ?? null,\n events: entry.events as CrudEventsConfig<unknown>,\n indexer: entry.indexer as CrudIndexerConfig<unknown>,\n })\n } catch {\n // best-effort; continue with remaining side effects\n }\n }\n }\n\n private buildSideEffectKey(action: CrudEventAction, identifiers: CrudEntityIdentifiers): string {\n const id = identifiers.id ?? ''\n const org = identifiers.organizationId ?? ''\n const tenant = identifiers.tenantId ?? ''\n return [action, id, org, tenant].join('|')\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityData, EntityName, FilterQuery, RequiredEntityData } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { type Kysely, sql } from 'kysely'\nimport { setRecordCustomFields } from '@open-mercato/core/modules/entities/lib/helpers'\nimport { validateCustomFieldValuesServer } from '@open-mercato/core/modules/entities/lib/validation'\nimport { sanitizeCustomFieldHtmlRichTextValuesServer } from '@open-mercato/core/modules/entities/lib/htmlRichTextSanitizer'\nimport type { EventBus } from '@open-mercato/events/types'\nimport type {\n CrudEventAction,\n CrudEventsConfig,\n CrudIndexerConfig,\n CrudEntityIdentifiers,\n} from '../crud/types'\nimport { CrudHttpError } from '../crud/errors'\nimport { resolveRegisteredEntityTableName } from '../query/engine'\nimport { getEntityIds } from '../encryption/entityIds'\nimport { normalizeCustomFieldValues } from '../custom-fields/normalize'\nimport { parseBooleanToken } from '../boolean'\nimport { isEventDeclared } from '../../modules/events'\n\nconst undeclaredEventWarned = new Set<string>()\n\nfunction warnIfUndeclaredEvent(eventName: string, context: string): void {\n if (isEventDeclared(eventName)) return\n if (undeclaredEventWarned.has(eventName)) return\n undeclaredEventWarned.add(eventName)\n console.warn(\n `[data-engine] ${context} is emitting undeclared event \"${eventName}\". ` +\n `Declare it in the owning module's events.ts (createModuleEvents) so the event registry stays authoritative.`,\n )\n}\n\n/** Internal: clear the undeclared-event warning cache. Exposed for tests. */\nexport function __resetUndeclaredEventWarningsForTests(): void {\n undeclaredEventWarned.clear()\n}\n\nconst COVERAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000\nconst coverageRefreshTracker = new Map<string, number>()\n\nfunction shouldTriggerCoverageRefresh(entityType: string | undefined, tenantId: string | null): boolean {\n if (!entityType) return false\n const key = `${entityType}|${tenantId ?? '__null__'}`\n const now = Date.now()\n const last = coverageRefreshTracker.get(key) ?? 0\n if (now - last < COVERAGE_REFRESH_INTERVAL_MS) return false\n coverageRefreshTracker.set(key, now)\n return true\n}\n\ntype CustomEntityValues = Record<string, unknown>\n\ntype QueuedCrudSideEffect = {\n action: CrudEventAction\n entity: unknown\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n events?: CrudEventsConfig<unknown>\n indexer?: CrudIndexerConfig<unknown>\n}\n\nexport interface DataEngine {\n setCustomFields(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, string | number | boolean | null | undefined | Array<string | number | boolean | null | undefined>>\n notify?: boolean // default true -> emit '<module>.<entity>.updated'\n }): Promise<void>\n\n // Storage for user-defined entities (doc-based)\n createCustomEntityRecord(opts: {\n entityId: string // '<module>:<entity>'\n recordId?: string // optional; auto-generate if not provided\n organizationId?: string | null\n tenantId?: string | null\n values: CustomEntityValues\n notify?: boolean // keep event emitting as it is via setCustomFields (updated)\n }): Promise<{ id: string }>\n\n updateCustomEntityRecord(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: CustomEntityValues\n notify?: boolean // keep event emitting as it is via setCustomFields (updated)\n }): Promise<void>\n\n deleteCustomEntityRecord(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n soft?: boolean // default true: sets deleted_at\n notify?: boolean // keep event emitting as it is (no extra events here)\n }): Promise<void>\n\n // Generic ORM-backed entity operations used by CrudFactory\n createOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n data: EntityData<T>\n }): Promise<T>\n\n updateOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n apply: (current: T) => Promise<void> | void\n }): Promise<T | null>\n\n deleteOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n soft?: boolean\n softDeleteField?: keyof T & string\n }): Promise<T | null>\n\n emitOrmEntityEvent<T>(opts: {\n action: CrudEventAction\n entity: T\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): Promise<void>\n\n markOrmEntityChange<T>(opts: {\n action: CrudEventAction\n entity: T | null | undefined\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): void\n\n flushOrmEntityChanges(): Promise<void>\n}\n\nexport const SYSTEM_ENTITY_RECORDS_BLOCKED_CODE = 'system_entity_records_blocked'\n\n/**\n * A system entity for doc-storage purposes is an id that modules declare in the\n * generated entity-id registry AND that resolves to a registered ORM table. Both\n * conditions matter: `resolveRegisteredEntityTableName` matches class-name candidates\n * from the entity segment alone, so a runtime-registered custom entity whose name\n * happens to collide with some ORM class (e.g. `user:todo` vs the example module's\n * `Todo`) must never be classified as system. When the registry is not populated\n * (exotic bootstraps, unit harnesses) the check conservatively falls back to the\n * ORM-table match alone so the #2939 protection never switches off.\n */\nexport function isOrmBackedSystemEntityId(em: EntityManager, entityId: string): boolean {\n const registry = getEntityIds(false)\n const moduleIds = Object.values(registry).flatMap((moduleEntities) => Object.values(moduleEntities ?? {}))\n if (moduleIds.length > 0 && !moduleIds.includes(entityId)) return false\n return resolveRegisteredEntityTableName(em, entityId) !== null\n}\n\n/**\n * Doc storage (`custom_entities_storage`) is for custom entities only. A system\n * entity's records live in its own module tables/APIs \u2014 writing doc rows for it\n * poisons read-path classification (#2939) and must be rejected at the deepest\n * seam so no caller (API, AI tool, workflow) can do it.\n */\nexport function assertCustomEntityStorageEntityId(em: EntityManager, entityId: string): void {\n if (isOrmBackedSystemEntityId(em, entityId)) {\n throw new CrudHttpError(400, {\n error: 'Records are available for custom entities only',\n code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE,\n entityId,\n })\n }\n}\n\nexport class DefaultDataEngine implements DataEngine {\n private pendingSideEffects = new Map<string, QueuedCrudSideEffect>()\n constructor(private em: EntityManager, private container: AwilixContainer) {}\n\n async setCustomFields(opts: Parameters<DataEngine['setCustomFields']>[0]): Promise<void> {\n const { entityId, recordId, organizationId = null, tenantId = null, values } = opts\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId,\n organizationId,\n tenantId,\n values,\n })\n await this.validateCustomFieldValues(entityId, organizationId, tenantId, sanitizedValues as Record<string, unknown>)\n let encryptionService: any = null\n try {\n encryptionService = this.container.resolve('tenantEncryptionService') as any\n } catch {\n encryptionService = null\n }\n await setRecordCustomFields(this.em, {\n entityId,\n recordId,\n organizationId,\n tenantId,\n values: sanitizedValues,\n encryptionService,\n })\n if (opts.notify !== false) {\n let bus: EventBus | null = null\n try {\n bus = (this.container.resolve('eventBus') as EventBus)\n } catch {\n bus = null\n }\n if (bus) {\n const [mod, ent] = (entityId || '').split(':')\n if (mod && ent) {\n const eventName = `${mod}.${ent}.updated`\n warnIfUndeclaredEvent(eventName, 'setCustomFields')\n try {\n await bus.emitEvent(eventName, { id: recordId, organizationId, tenantId }, { persistent: true })\n } catch {\n // non-blocking\n }\n }\n }\n }\n }\n\n private normalizeDocValues(values: CustomEntityValues): CustomEntityValues {\n const out: CustomEntityValues = {}\n for (const [k, v] of Object.entries(values || {})) {\n // Never allow callers to override reserved identifiers in the doc\n if (k === 'id' || k === 'entity_id' || k === 'entityId') continue\n // Accept both 'cf_<key>' and 'cf:<key>' inputs and normalize to 'cf:<key>'\n if (k.startsWith('cf_')) out[`cf:${k.slice(3)}`] = v\n else out[k] = v\n }\n return out\n }\n\n private backcompatEavEnabled(): boolean {\n try {\n return parseBooleanToken(process.env.ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM ?? '') === true\n } catch { return false }\n }\n\n private getKysely(): Kysely<any> {\n return this.em.getKysely<any>()\n }\n\n private async ensureStorageTableExists(): Promise<void> {\n const db = this.getKysely()\n const exists = await db\n .selectFrom('information_schema.tables' as any)\n .select(sql`1`.as('present'))\n .where('table_name' as any, '=', 'custom_entities_storage')\n .executeTakeFirst()\n if (!exists) {\n throw new Error('custom_entities_storage table is missing. Run migrations (yarn db:migrate).')\n }\n }\n\n private normalizeValuesForValidation(values: Record<string, unknown> | undefined | null): Record<string, unknown> {\n if (!values) return {}\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (value === undefined) continue\n if (key.startsWith('cf_') || key.startsWith('cf:')) {\n const normalized = key.slice(3)\n if (normalized) out[normalized] = value\n continue\n }\n out[key] = value\n }\n return out\n }\n\n private async validateCustomFieldValues(\n entityId: string,\n organizationId: string | null,\n tenantId: string | null,\n values: Record<string, unknown> | undefined | null,\n ): Promise<void> {\n const prepared = this.normalizeValuesForValidation(values)\n if (!entityId || Object.keys(prepared).length === 0) return\n const result = await validateCustomFieldValuesServer(this.em, {\n entityId,\n organizationId,\n tenantId,\n values: prepared,\n })\n if (!result.ok) {\n throw new CrudHttpError(400, { error: 'Validation failed', fields: result.fieldErrors })\n }\n }\n\n async createCustomEntityRecord(opts: Parameters<DataEngine['createCustomEntityRecord']>[0]): Promise<{ id: string }> {\n assertCustomEntityStorageEntityId(this.em, opts.entityId)\n const db = this.getKysely()\n await this.ensureStorageTableExists()\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId: opts.entityId,\n organizationId: opts.organizationId ?? null,\n tenantId: opts.tenantId ?? null,\n values: opts.values || {},\n })\n await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues)\n const rawId = String(opts.recordId ?? '').trim()\n const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(rawId)\n const sentinel = rawId.toLowerCase()\n const shouldGenerate = !rawId || !isUuid || sentinel === 'create' || sentinel === 'new' || sentinel === 'null' || sentinel === 'undefined'\n const id = shouldGenerate ? ((): string => {\n const g = globalThis as { crypto?: { randomUUID?: () => string } }\n if (g.crypto?.randomUUID) return g.crypto.randomUUID()\n // Fallback UUIDv4 generator\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === 'x' ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n })() : rawId\n const orgId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const doc: Record<string, unknown> = { id, ...this.normalizeDocValues(sanitizedValues || {}) }\n\n const now = sql`now()`\n const payload = {\n entity_type: opts.entityId,\n entity_id: id,\n organization_id: orgId,\n tenant_id: tenantId,\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: now,\n created_at: now,\n deleted_at: null,\n }\n\n // Upsert by scoped uniqueness\n try {\n await db\n .insertInto('custom_entities_storage' as any)\n .values(payload as any)\n .onConflict((oc) => oc\n .columns(['entity_type', 'entity_id', 'organization_id'])\n .doUpdateSet({\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any))\n .execute()\n } catch {\n // Fallback for global scope uniqueness\n try {\n const updated = await db\n .updateTable('custom_entities_storage' as any)\n .set({\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any)\n .where('entity_type' as any, '=', opts.entityId)\n .where('entity_id' as any, '=', id)\n .where('organization_id' as any, orgId === null ? 'is' : '=', orgId as any)\n .executeTakeFirst()\n if (!updated || Number(updated.numUpdatedRows ?? 0) === 0) {\n await db.insertInto('custom_entities_storage' as any).values(payload as any).execute()\n }\n } catch (err) {\n // Surface a clear error so it doesn't silently fall back only to EAV\n throw err\n }\n }\n\n // Optional EAV backward compatibility (disabled by default)\n if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {\n await this.setCustomFields({\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: tenantId,\n values: normalizeCustomFieldValues(sanitizedValues),\n notify: opts.notify, // defaults to true\n })\n }\n\n return { id }\n }\n\n async updateCustomEntityRecord(opts: Parameters<DataEngine['updateCustomEntityRecord']>[0]): Promise<void> {\n assertCustomEntityStorageEntityId(this.em, opts.entityId)\n const db = this.getKysely()\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId: opts.entityId,\n organizationId: opts.organizationId ?? null,\n tenantId: opts.tenantId ?? null,\n values: opts.values || {},\n })\n await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues)\n const id = String(opts.recordId)\n const orgId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n\n // Merge doc shallowly: load existing doc and overlay\n await this.ensureStorageTableExists()\n const applyScope = <T extends { where: (col: any, op: any, val?: any) => T }>(q: T) => {\n let chain = q.where('entity_type' as any, '=', opts.entityId)\n chain = chain.where('entity_id' as any, '=', id)\n chain = orgId === null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', orgId)\n return chain\n }\n const row = await applyScope(\n db.selectFrom('custom_entities_storage' as any).select(['doc' as any])\n ).executeTakeFirst()\n const prevDoc: Record<string, unknown> = (row as any)?.doc || { id }\n const nextDoc: Record<string, unknown> = { ...prevDoc, ...this.normalizeDocValues(sanitizedValues || {}), id }\n try {\n const updated = await applyScope(\n db.updateTable('custom_entities_storage' as any).set({\n doc: sql`${JSON.stringify(nextDoc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any) as any\n ).executeTakeFirst()\n if (!updated || Number((updated as any).numUpdatedRows ?? 0) === 0) {\n await db.insertInto('custom_entities_storage' as any).values({\n entity_type: opts.entityId,\n entity_id: id,\n organization_id: orgId,\n tenant_id: tenantId,\n doc: sql`${JSON.stringify(nextDoc)}::jsonb`,\n created_at: sql`now()`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any).execute()\n }\n } catch (err) {\n throw err\n }\n\n // Optional EAV backward compatibility (disabled by default)\n if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {\n await this.setCustomFields({\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: tenantId,\n values: normalizeCustomFieldValues(sanitizedValues),\n notify: opts.notify, // defaults to true\n })\n }\n }\n\n async deleteCustomEntityRecord(opts: Parameters<DataEngine['deleteCustomEntityRecord']>[0]): Promise<void> {\n assertCustomEntityStorageEntityId(this.em, opts.entityId)\n const db = this.getKysely()\n const id = String(opts.recordId)\n const orgId = opts.organizationId ?? null\n const soft = opts.soft !== false\n\n const applyScope = <T extends { where: (col: any, op: any, val?: any) => T }>(q: T) => {\n let chain = q.where('entity_type' as any, '=', opts.entityId)\n chain = chain.where('entity_id' as any, '=', id)\n chain = orgId === null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', orgId)\n return chain\n }\n\n if (soft) {\n await applyScope(\n db.updateTable('custom_entities_storage' as any).set({\n deleted_at: sql`now()`,\n updated_at: sql`now()`,\n } as any) as any\n ).execute()\n } else {\n await applyScope(db.deleteFrom('custom_entities_storage' as any) as any).execute()\n }\n\n // Soft-delete EAV values to preserve current behavior\n try {\n const { CustomFieldValue } = await import('@open-mercato/core/modules/entities/data/entities')\n const values = await this.em.find(CustomFieldValue, {\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: opts.tenantId ?? null,\n })\n const now = new Date()\n const mutated = values.filter((record) => {\n if (record.deletedAt) return false\n record.deletedAt = now\n return true\n })\n if (mutated.length) {\n for (const record of values) this.em.persist(record)\n await this.em.flush()\n }\n } catch { /* non-blocking */ }\n }\n\n async createOrmEntity<T extends object>(opts: { entity: EntityName<T>; data: EntityData<T> }): Promise<T> {\n const entity = this.em.create(\n opts.entity as EntityName<T>,\n opts.data as unknown as RequiredEntityData<T>\n )\n await this.em.persist(entity).flush()\n return entity\n }\n\n async updateOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n apply: (current: T) => Promise<void> | void\n }): Promise<T | null> {\n const current = await this.em.findOne(opts.entity as EntityName<T>, opts.where as FilterQuery<NoInfer<T>>)\n if (!current) return null\n await opts.apply(current)\n await this.em.persist(current).flush()\n return current\n }\n\n async deleteOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n soft?: boolean\n softDeleteField?: keyof T & string\n }): Promise<T | null> {\n const current = await this.em.findOne(opts.entity as EntityName<T>, opts.where as FilterQuery<NoInfer<T>>)\n if (!current) return null\n if (opts.soft !== false) {\n const field = opts.softDeleteField || ('deletedAt' as keyof T & string)\n if (typeof current === 'object' && current !== null) {\n ;(current as Record<string, unknown>)[field] = new Date()\n await this.em.persist(current).flush()\n }\n } else {\n await this.em.remove(current).flush()\n }\n return current\n }\n\n async emitOrmEntityEvent<T>(opts: {\n action: CrudEventAction\n entity: T\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): Promise<void> {\n const { action, entity, events, indexer, identifiers, syncOrigin } = opts\n if (!events && !indexer) return\n if (!identifiers?.id) return\n\n let bus: EventBus | null = null\n try {\n bus = (this.container.resolve('eventBus') as EventBus)\n } catch {\n bus = null\n }\n if (!bus) return\n\n const ctx = {\n action,\n entity,\n identifiers: {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n },\n syncOrigin: syncOrigin ?? null,\n }\n\n if (events) {\n const eventName = `${events.module}.${events.entity}.${action}`\n warnIfUndeclaredEvent(eventName, 'emitOrmEntityEvent')\n const payload = events.buildPayload\n ? events.buildPayload(ctx)\n : {\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),\n }\n try {\n await bus.emitEvent(eventName, payload, {\n persistent: !!events.persistent,\n tenantId: ctx.identifiers.tenantId ?? null,\n organizationId: ctx.identifiers.organizationId ?? null,\n })\n } catch {\n // non-blocking\n }\n }\n\n if (indexer) {\n const resolveCoverageBaseDelta = (): number | undefined => {\n if (action === 'created') return 1\n if (action === 'deleted') return -1\n return undefined\n }\n const coverageBaseDelta = resolveCoverageBaseDelta()\n\n if (action === 'deleted') {\n const payload = indexer.buildDeletePayload\n ? indexer.buildDeletePayload(ctx)\n : {\n entityType: indexer.entityType,\n recordId: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }\n const enrichedPayload = payload as Record<string, unknown>\n enrichedPayload.crudAction = action\n if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta\n if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin\n // Await the index update so query-index reads (the `customValues`/scalar\n // projection that list endpoints serve) are consistent the moment the write\n // returns. The subscriber removes the projection row + tokens synchronously and\n // defers the coverage recompute + fulltext delete, so this stays bounded.\n // Errors are logged, not thrown \u2014 index drift never fails the originating write.\n await bus.emitEvent('query_index.delete_one', enrichedPayload).catch((err: unknown) => {\n console.error('[data-engine] query_index.delete_one emit failed', err)\n })\n } else {\n const payload = indexer.buildUpsertPayload\n ? indexer.buildUpsertPayload(ctx)\n : {\n entityType: indexer.entityType,\n recordId: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }\n const enrichedPayload = payload as Record<string, unknown>\n enrichedPayload.crudAction = action\n if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta\n if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin\n // Await the projection upsert so list reads observe the new doc immediately\n // (see delete_one above). The subscriber updates `entity_indexes` synchronously\n // and defers the heavy token-reindex pipeline (build doc + encrypt + decrypt +\n // tokenize + DELETE + chunked INSERT) so write latency stays bounded.\n await bus.emitEvent('query_index.upsert_one', enrichedPayload).catch((err: unknown) => {\n console.error('[data-engine] query_index.upsert_one emit failed', err)\n })\n }\n\n if (shouldTriggerCoverageRefresh(indexer.entityType, ctx.identifiers.tenantId ?? null)) {\n void bus.emitEvent('query_index.coverage.refresh', {\n entityType: indexer.entityType,\n tenantId: ctx.identifiers.tenantId ?? null,\n organizationId: null,\n delayMs: 0,\n }).catch(() => undefined)\n }\n }\n }\n\n markOrmEntityChange<T>(opts: {\n action: CrudEventAction\n entity: T | null | undefined\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): void {\n const { entity, identifiers } = opts\n if (!entity) return\n if (!identifiers?.id) return\n const key = this.buildSideEffectKey(opts.action, identifiers)\n const existing = this.pendingSideEffects.get(key)\n if (existing) {\n existing.entity = entity\n existing.identifiers = {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n }\n existing.syncOrigin = opts.syncOrigin ?? null\n if (opts.events) existing.events = opts.events as CrudEventsConfig<unknown>\n if (opts.indexer) existing.indexer = opts.indexer as CrudIndexerConfig<unknown>\n this.pendingSideEffects.set(key, existing)\n return\n }\n const entry: QueuedCrudSideEffect = {\n action: opts.action,\n entity,\n identifiers: {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n },\n syncOrigin: opts.syncOrigin ?? null,\n }\n if (opts.events) entry.events = opts.events as CrudEventsConfig<unknown>\n if (opts.indexer) entry.indexer = opts.indexer as CrudIndexerConfig<unknown>\n this.pendingSideEffects.set(key, entry)\n }\n\n async flushOrmEntityChanges(): Promise<void> {\n if (!this.pendingSideEffects.size) return\n const entries = Array.from(this.pendingSideEffects.values())\n this.pendingSideEffects.clear()\n for (const entry of entries) {\n try {\n await this.emitOrmEntityEvent({\n action: entry.action,\n entity: entry.entity,\n identifiers: entry.identifiers,\n syncOrigin: entry.syncOrigin ?? null,\n events: entry.events as CrudEventsConfig<unknown>,\n indexer: entry.indexer as CrudIndexerConfig<unknown>,\n })\n } catch {\n // best-effort; continue with remaining side effects\n }\n }\n }\n\n private buildSideEffectKey(action: CrudEventAction, identifiers: CrudEntityIdentifiers): string {\n const id = identifiers.id ?? ''\n const org = identifiers.organizationId ?? ''\n const tenant = identifiers.tenantId ?? ''\n return [action, id, org, tenant].join('|')\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,17 @@
1
+ import { escapeLikePattern } from "./escapeLikePattern.js";
2
+ const buildIlikeTerm = (value, mode = "contains") => {
3
+ const escaped = escapeLikePattern(value);
4
+ switch (mode) {
5
+ case "startsWith":
6
+ return `${escaped}%`;
7
+ case "endsWith":
8
+ return `%${escaped}`;
9
+ case "contains":
10
+ default:
11
+ return `%${escaped}%`;
12
+ }
13
+ };
14
+ export {
15
+ buildIlikeTerm
16
+ };
17
+ //# sourceMappingURL=buildIlikeTerm.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/db/buildIlikeTerm.ts"],
4
+ "sourcesContent": ["import { escapeLikePattern } from './escapeLikePattern'\n\nexport type IlikeMatchMode = 'contains' | 'startsWith' | 'endsWith'\n\nexport const buildIlikeTerm = (value: string, mode: IlikeMatchMode = 'contains'): string => {\n const escaped = escapeLikePattern(value)\n switch (mode) {\n case 'startsWith':\n return `${escaped}%`\n case 'endsWith':\n return `%${escaped}`\n case 'contains':\n default:\n return `%${escaped}%`\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,yBAAyB;AAI3B,MAAM,iBAAiB,CAAC,OAAe,OAAuB,eAAuB;AAC1F,QAAM,UAAU,kBAAkB,KAAK;AACvC,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,GAAG,OAAO;AAAA,IACnB,KAAK;AACH,aAAO,IAAI,OAAO;AAAA,IACpB,KAAK;AAAA,IACL;AACE,aAAO,IAAI,OAAO;AAAA,EACtB;AACF;",
6
+ "names": []
7
+ }
@@ -25,6 +25,28 @@ function getOrmEntities() {
25
25
  }
26
26
  return entities;
27
27
  }
28
+ function parsePositiveIntEnv(raw) {
29
+ const parsed = parseInt(raw || "");
30
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
31
+ }
32
+ function resolvePoolConfig(env = process.env) {
33
+ const idleSessionTimeoutEnv = parseInt(env.DB_IDLE_SESSION_TIMEOUT_MS || "");
34
+ const idleInTxTimeoutEnv = parseInt(env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || "");
35
+ return {
36
+ poolMin: parseInt(env.DB_POOL_MIN || "2"),
37
+ poolMax: parseInt(env.DB_POOL_MAX || "20"),
38
+ poolIdleTimeout: parseInt(env.DB_POOL_IDLE_TIMEOUT || "3000"),
39
+ poolAcquireTimeout: parseInt(env.DB_POOL_ACQUIRE_TIMEOUT || "6000"),
40
+ idleSessionTimeoutMs: Number.isFinite(idleSessionTimeoutEnv) ? idleSessionTimeoutEnv : env.NODE_ENV === "production" ? void 0 : 6e5,
41
+ // Finite default in every environment (including production) so a leaked or idle
42
+ // open transaction cannot pin a pool connection indefinitely and exhaust the pool.
43
+ // Mirrors the long-standing dev value; override (incl. 0 to disable) via env.
44
+ idleInTransactionTimeoutMs: Number.isFinite(idleInTxTimeoutEnv) ? idleInTxTimeoutEnv : 12e4,
45
+ // Opt-in guards against runaway statements and lock waits. No timeout when unset.
46
+ statementTimeoutMs: parsePositiveIntEnv(env.DB_STATEMENT_TIMEOUT_MS),
47
+ lockTimeoutMs: parsePositiveIntEnv(env.DB_LOCK_TIMEOUT_MS)
48
+ };
49
+ }
28
50
  async function getOrm() {
29
51
  if (ormInstance) {
30
52
  return ormInstance;
@@ -34,14 +56,16 @@ async function getOrm() {
34
56
  if (!clientUrl) {
35
57
  throw new Error("DATABASE_URL is not set");
36
58
  }
37
- const poolMin = parseInt(process.env.DB_POOL_MIN || "2");
38
- const poolMax = parseInt(process.env.DB_POOL_MAX || "20");
39
- const poolIdleTimeout = parseInt(process.env.DB_POOL_IDLE_TIMEOUT || "3000");
40
- const poolAcquireTimeout = parseInt(process.env.DB_POOL_ACQUIRE_TIMEOUT || "6000");
41
- const idleSessionTimeoutEnv = parseInt(process.env.DB_IDLE_SESSION_TIMEOUT_MS || "");
42
- const idleInTxTimeoutEnv = parseInt(process.env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || "");
43
- const idleSessionTimeoutMs = Number.isFinite(idleSessionTimeoutEnv) ? idleSessionTimeoutEnv : process.env.NODE_ENV === "production" ? void 0 : 6e5;
44
- const idleInTransactionTimeoutMs = Number.isFinite(idleInTxTimeoutEnv) ? idleInTxTimeoutEnv : process.env.NODE_ENV === "production" ? void 0 : 12e4;
59
+ const {
60
+ poolMin,
61
+ poolMax,
62
+ poolIdleTimeout,
63
+ poolAcquireTimeout,
64
+ idleSessionTimeoutMs,
65
+ idleInTransactionTimeoutMs,
66
+ statementTimeoutMs,
67
+ lockTimeoutMs
68
+ } = resolvePoolConfig();
45
69
  const connectionOptions = idleSessionTimeoutMs && idleSessionTimeoutMs > 0 ? `-c idle_session_timeout=${idleSessionTimeoutMs}` : void 0;
46
70
  const sslConfig = getSslConfig();
47
71
  if (process.env.OM_DB_POOL_DEBUG === "1" || process.env.OM_INTEGRATION_TEST === "true") {
@@ -52,6 +76,8 @@ async function getOrm() {
52
76
  poolAcquireTimeout,
53
77
  idleSessionTimeoutMs,
54
78
  idleInTransactionTimeoutMs,
79
+ statementTimeoutMs,
80
+ lockTimeoutMs,
55
81
  nodeEnv: process.env.NODE_ENV
56
82
  });
57
83
  }
@@ -80,6 +106,8 @@ async function getOrm() {
80
106
  driverOptions: {
81
107
  connectionTimeoutMillis: poolAcquireTimeout,
82
108
  idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,
109
+ statement_timeout: statementTimeoutMs,
110
+ lock_timeout: lockTimeoutMs,
83
111
  options: connectionOptions,
84
112
  ssl: sslConfig,
85
113
  onPoolCreated: (pool) => {
@@ -108,6 +136,7 @@ if (process.env.NODE_ENV !== "production") {
108
136
  export {
109
137
  getOrm,
110
138
  getOrmEntities,
111
- registerOrmEntities
139
+ registerOrmEntities,
140
+ resolvePoolConfig
112
141
  };
113
142
  //# sourceMappingURL=mikro.js.map