@open-mercato/shared 0.6.7-develop.6775.1.c2313bb8a3 → 0.6.7-develop.6785.1.1dd7cfac55

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 (52) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +18 -3
  3. package/dist/lib/ai/safety-identifier.js +32 -0
  4. package/dist/lib/ai/safety-identifier.js.map +7 -0
  5. package/dist/lib/commands/command-bus.js +5 -1
  6. package/dist/lib/commands/command-bus.js.map +2 -2
  7. package/dist/lib/commands/command-interceptor-runner.js +2 -2
  8. package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
  9. package/dist/lib/crud/enricher-runner.js +2 -11
  10. package/dist/lib/crud/enricher-runner.js.map +2 -2
  11. package/dist/lib/crud/factory.js +4 -2
  12. package/dist/lib/crud/factory.js.map +2 -2
  13. package/dist/lib/crud/interceptor-runner.js +2 -2
  14. package/dist/lib/crud/interceptor-runner.js.map +2 -2
  15. package/dist/lib/crud/mutation-guard-registry.js +2 -2
  16. package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
  17. package/dist/lib/crud/types.js +4 -0
  18. package/dist/lib/crud/types.js.map +3 -3
  19. package/dist/lib/data/consistency.js +19 -0
  20. package/dist/lib/data/consistency.js.map +7 -0
  21. package/dist/lib/data/engine.js +37 -11
  22. package/dist/lib/data/engine.js.map +2 -2
  23. package/dist/lib/version.js +1 -1
  24. package/dist/lib/version.js.map +1 -1
  25. package/dist/security/enabledModulesRegistry.js +56 -11
  26. package/dist/security/enabledModulesRegistry.js.map +2 -2
  27. package/dist/security/featurePolicy.js +62 -0
  28. package/dist/security/featurePolicy.js.map +7 -0
  29. package/package.json +6 -2
  30. package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
  31. package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
  32. package/src/lib/ai/llm-provider.ts +33 -0
  33. package/src/lib/ai/safety-identifier.ts +73 -0
  34. package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +28 -0
  35. package/src/lib/commands/command-bus.ts +5 -1
  36. package/src/lib/commands/command-interceptor-runner.ts +2 -2
  37. package/src/lib/crud/__tests__/crud-factory.test.ts +18 -0
  38. package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
  39. package/src/lib/crud/enricher-runner.ts +2 -11
  40. package/src/lib/crud/factory.ts +2 -0
  41. package/src/lib/crud/interceptor-runner.ts +2 -2
  42. package/src/lib/crud/mutation-guard-registry.ts +2 -2
  43. package/src/lib/crud/types.ts +3 -0
  44. package/src/lib/data/__tests__/consistency.test.ts +38 -0
  45. package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
  46. package/src/lib/data/consistency.ts +17 -0
  47. package/src/lib/data/engine.ts +50 -16
  48. package/src/modules/customer-auth.ts +1 -0
  49. package/src/modules/navigation/backendChrome.ts +1 -0
  50. package/src/security/__tests__/featurePolicy.test.ts +166 -0
  51. package/src/security/enabledModulesRegistry.ts +64 -12
  52. package/src/security/featurePolicy.ts +89 -0
@@ -1,5 +1,5 @@
1
1
  import { getApiInterceptorsForRoute } from "./interceptor-registry.js";
2
- import { hasAllFeatures } from "../../security/features.js";
2
+ import { authorizeFeatures } from "../../security/featurePolicy.js";
3
3
  import { logInterceptorActivity } from "../umes/interceptor-activity.js";
4
4
  const DEFAULT_TIMEOUT_MS = 5e3;
5
5
  function sanitizeObject(input) {
@@ -8,7 +8,7 @@ function sanitizeObject(input) {
8
8
  return Object.keys(clean).length > 0 ? clean : {};
9
9
  }
10
10
  function hasRequiredFeatures(features, userFeatures) {
11
- return hasAllFeatures(userFeatures, features);
11
+ return authorizeFeatures(features ?? [], { grantedFeatures: userFeatures ?? [] });
12
12
  }
13
13
  function timeoutPromise(ms, interceptorId) {
14
14
  return new Promise((_, reject) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/interceptor-runner.ts"],
4
- "sourcesContent": ["import type {\n ApiInterceptorMethod,\n InterceptorContext,\n InterceptorRequest,\n InterceptorResponse,\n InterceptorBeforeResult,\n} from './api-interceptor'\nimport { getApiInterceptorsForRoute } from './interceptor-registry'\nimport { hasAllFeatures } from '../../security/features'\nimport { logInterceptorActivity } from '../umes/interceptor-activity'\n\nconst DEFAULT_TIMEOUT_MS = 5000\n\ntype BeforeRunOk = {\n ok: true\n request: InterceptorRequest\n metadataByInterceptor: Record<string, Record<string, unknown> | undefined>\n}\n\ntype BeforeRunFailed = {\n ok: false\n statusCode: number\n body: Record<string, unknown>\n}\n\nexport type RunInterceptorsBeforeResult = BeforeRunOk | BeforeRunFailed\n\nexport type RunInterceptorsAfterResult = {\n ok: boolean\n statusCode: number\n body: Record<string, unknown>\n headers: Record<string, string>\n}\n\nfunction sanitizeObject(input?: Record<string, unknown>): Record<string, unknown> | undefined {\n if (!input || typeof input !== 'object') return undefined\n const clean = Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined))\n return Object.keys(clean).length > 0 ? clean : {}\n}\n\nfunction hasRequiredFeatures(features: string[] | undefined, userFeatures: string[] | undefined): boolean {\n return hasAllFeatures(userFeatures, features)\n}\n\nfunction timeoutPromise(ms: number, interceptorId: string): Promise<never> {\n return new Promise((_, reject) => {\n setTimeout(() => {\n reject(new Error(`INTERCEPTOR_TIMEOUT:${interceptorId}`))\n }, ms)\n })\n}\n\nfunction isTimeoutError(error: unknown): boolean {\n return error instanceof Error && error.message.startsWith('INTERCEPTOR_TIMEOUT:')\n}\n\nasync function runWithTimeout<T>(\n task: Promise<T>,\n timeoutMs: number,\n interceptorId: string,\n): Promise<T> {\n return Promise.race([task, timeoutPromise(timeoutMs, interceptorId)])\n}\n\nfunction toErrorBody(interceptorId: string, error: unknown): Record<string, unknown> {\n const body: Record<string, unknown> = {\n error: 'Internal interceptor error',\n }\n if (process.env.NODE_ENV !== 'production') {\n body.interceptorId = interceptorId\n body.message = error instanceof Error ? error.message : String(error)\n }\n return body\n}\n\nexport async function runApiInterceptorsBefore(args: {\n routePath: string\n method: ApiInterceptorMethod\n request: InterceptorRequest\n context: Omit<InterceptorContext, 'metadata'>\n}): Promise<RunInterceptorsBeforeResult> {\n const { routePath, method, context } = args\n let currentRequest: InterceptorRequest = {\n ...args.request,\n body: sanitizeObject(args.request.body),\n query: sanitizeObject(args.request.query),\n headers: { ...(args.request.headers ?? {}) },\n }\n\n const metadataByInterceptor: Record<string, Record<string, unknown> | undefined> = {}\n const interceptors = getApiInterceptorsForRoute(routePath, method)\n\n for (const entry of interceptors) {\n const interceptor = entry.interceptor\n if (!interceptor.before) continue\n if (!hasRequiredFeatures(interceptor.features, context.userFeatures)) continue\n\n const startTime = Date.now()\n try {\n const timeoutMs = interceptor.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const result = await runWithTimeout(\n interceptor.before(currentRequest, context),\n timeoutMs,\n interceptor.id,\n )\n\n const normalized: InterceptorBeforeResult = result ?? { ok: true }\n if (!normalized.ok) {\n const rejectBody: Record<string, unknown> = {\n error: normalized.message ?? 'Request blocked by API interceptor',\n }\n if (process.env.NODE_ENV !== 'production') {\n rejectBody.interceptorId = interceptor.id\n }\n logInterceptorActivity({\n interceptorId: interceptor.id,\n moduleId: entry.moduleId,\n route: routePath,\n method,\n result: 'blocked',\n durationMs: Date.now() - startTime,\n timestamp: Date.now(),\n statusCode: normalized.statusCode ?? 400,\n message: normalized.message,\n })\n return {\n ok: false,\n statusCode: normalized.statusCode ?? 400,\n body: rejectBody,\n }\n }\n\n const hasModifications = !!(normalized.headers || normalized.body || normalized.query)\n logInterceptorActivity({\n interceptorId: interceptor.id,\n moduleId: entry.moduleId,\n route: routePath,\n method,\n result: hasModifications ? 'modified' : 'allowed',\n durationMs: Date.now() - startTime,\n timestamp: Date.now(),\n })\n\n if (normalized.headers) {\n currentRequest = {\n ...currentRequest,\n headers: {\n ...currentRequest.headers,\n ...normalized.headers,\n },\n }\n }\n if (normalized.body) {\n currentRequest = {\n ...currentRequest,\n body: sanitizeObject(normalized.body),\n }\n }\n if (normalized.query) {\n currentRequest = {\n ...currentRequest,\n query: sanitizeObject(normalized.query),\n }\n }\n metadataByInterceptor[interceptor.id] = normalized.metadata\n } catch (error) {\n if (isTimeoutError(error)) {\n const timeoutBody: Record<string, unknown> = { error: 'Interceptor timeout' }\n if (process.env.NODE_ENV !== 'production') {\n timeoutBody.interceptorId = interceptor.id\n }\n logInterceptorActivity({\n interceptorId: interceptor.id,\n moduleId: entry.moduleId,\n route: routePath,\n method,\n result: 'blocked',\n durationMs: Date.now() - startTime,\n timestamp: Date.now(),\n statusCode: 504,\n message: 'Interceptor timeout',\n })\n return {\n ok: false,\n statusCode: 504,\n body: timeoutBody,\n }\n }\n logInterceptorActivity({\n interceptorId: interceptor.id,\n moduleId: entry.moduleId,\n route: routePath,\n method,\n result: 'blocked',\n durationMs: Date.now() - startTime,\n timestamp: Date.now(),\n statusCode: 500,\n message: error instanceof Error ? error.message : String(error),\n })\n return {\n ok: false,\n statusCode: 500,\n body: toErrorBody(interceptor.id, error),\n }\n }\n }\n\n return {\n ok: true,\n request: currentRequest,\n metadataByInterceptor,\n }\n}\n\nexport async function runApiInterceptorsAfter(args: {\n routePath: string\n method: ApiInterceptorMethod\n request: InterceptorRequest\n response: InterceptorResponse\n context: Omit<InterceptorContext, 'metadata'>\n metadataByInterceptor?: Record<string, Record<string, unknown> | undefined>\n}): Promise<RunInterceptorsAfterResult> {\n const { routePath, method, context } = args\n let body: Record<string, unknown> = { ...(args.response.body ?? {}) }\n let headers: Record<string, string> = { ...(args.response.headers ?? {}) }\n\n const interceptors = getApiInterceptorsForRoute(routePath, method)\n for (const entry of interceptors) {\n const interceptor = entry.interceptor\n if (!interceptor.after) continue\n if (!hasRequiredFeatures(interceptor.features, context.userFeatures)) continue\n\n try {\n const timeoutMs = interceptor.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const result = await runWithTimeout(\n interceptor.after(\n args.request,\n { statusCode: args.response.statusCode, body, headers },\n { ...context, metadata: args.metadataByInterceptor?.[interceptor.id] },\n ),\n timeoutMs,\n interceptor.id,\n )\n if (!result) continue\n if (result.replace && typeof result.replace === 'object') {\n body = { ...result.replace }\n } else if (result.merge && typeof result.merge === 'object') {\n body = { ...body, ...result.merge }\n }\n } catch (error) {\n if (isTimeoutError(error)) {\n const timeoutBody: Record<string, unknown> = { error: 'Interceptor timeout' }\n if (process.env.NODE_ENV !== 'production') {\n timeoutBody.interceptorId = interceptor.id\n }\n return {\n ok: false,\n statusCode: 504,\n body: timeoutBody,\n headers,\n }\n }\n return {\n ok: false,\n statusCode: 500,\n body: toErrorBody(interceptor.id, error),\n headers,\n }\n }\n }\n\n return {\n ok: true,\n statusCode: args.response.statusCode,\n body,\n headers,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type {\n ApiInterceptorMethod,\n InterceptorContext,\n InterceptorRequest,\n InterceptorResponse,\n InterceptorBeforeResult,\n} from './api-interceptor'\nimport { getApiInterceptorsForRoute } from './interceptor-registry'\nimport { authorizeFeatures } from '../../security/featurePolicy'\nimport { logInterceptorActivity } from '../umes/interceptor-activity'\n\nconst DEFAULT_TIMEOUT_MS = 5000\n\ntype BeforeRunOk = {\n ok: true\n request: InterceptorRequest\n metadataByInterceptor: Record<string, Record<string, unknown> | undefined>\n}\n\ntype BeforeRunFailed = {\n ok: false\n statusCode: number\n body: Record<string, unknown>\n}\n\nexport type RunInterceptorsBeforeResult = BeforeRunOk | BeforeRunFailed\n\nexport type RunInterceptorsAfterResult = {\n ok: boolean\n statusCode: number\n body: Record<string, unknown>\n headers: Record<string, string>\n}\n\nfunction sanitizeObject(input?: Record<string, unknown>): Record<string, unknown> | undefined {\n if (!input || typeof input !== 'object') return undefined\n const clean = Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined))\n return Object.keys(clean).length > 0 ? clean : {}\n}\n\nfunction hasRequiredFeatures(features: string[] | undefined, userFeatures: string[] | undefined): boolean {\n return authorizeFeatures(features ?? [], { grantedFeatures: userFeatures ?? [] })\n}\n\nfunction timeoutPromise(ms: number, interceptorId: string): Promise<never> {\n return new Promise((_, reject) => {\n setTimeout(() => {\n reject(new Error(`INTERCEPTOR_TIMEOUT:${interceptorId}`))\n }, ms)\n })\n}\n\nfunction isTimeoutError(error: unknown): boolean {\n return error instanceof Error && error.message.startsWith('INTERCEPTOR_TIMEOUT:')\n}\n\nasync function runWithTimeout<T>(\n task: Promise<T>,\n timeoutMs: number,\n interceptorId: string,\n): Promise<T> {\n return Promise.race([task, timeoutPromise(timeoutMs, interceptorId)])\n}\n\nfunction toErrorBody(interceptorId: string, error: unknown): Record<string, unknown> {\n const body: Record<string, unknown> = {\n error: 'Internal interceptor error',\n }\n if (process.env.NODE_ENV !== 'production') {\n body.interceptorId = interceptorId\n body.message = error instanceof Error ? error.message : String(error)\n }\n return body\n}\n\nexport async function runApiInterceptorsBefore(args: {\n routePath: string\n method: ApiInterceptorMethod\n request: InterceptorRequest\n context: Omit<InterceptorContext, 'metadata'>\n}): Promise<RunInterceptorsBeforeResult> {\n const { routePath, method, context } = args\n let currentRequest: InterceptorRequest = {\n ...args.request,\n body: sanitizeObject(args.request.body),\n query: sanitizeObject(args.request.query),\n headers: { ...(args.request.headers ?? {}) },\n }\n\n const metadataByInterceptor: Record<string, Record<string, unknown> | undefined> = {}\n const interceptors = getApiInterceptorsForRoute(routePath, method)\n\n for (const entry of interceptors) {\n const interceptor = entry.interceptor\n if (!interceptor.before) continue\n if (!hasRequiredFeatures(interceptor.features, context.userFeatures)) continue\n\n const startTime = Date.now()\n try {\n const timeoutMs = interceptor.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const result = await runWithTimeout(\n interceptor.before(currentRequest, context),\n timeoutMs,\n interceptor.id,\n )\n\n const normalized: InterceptorBeforeResult = result ?? { ok: true }\n if (!normalized.ok) {\n const rejectBody: Record<string, unknown> = {\n error: normalized.message ?? 'Request blocked by API interceptor',\n }\n if (process.env.NODE_ENV !== 'production') {\n rejectBody.interceptorId = interceptor.id\n }\n logInterceptorActivity({\n interceptorId: interceptor.id,\n moduleId: entry.moduleId,\n route: routePath,\n method,\n result: 'blocked',\n durationMs: Date.now() - startTime,\n timestamp: Date.now(),\n statusCode: normalized.statusCode ?? 400,\n message: normalized.message,\n })\n return {\n ok: false,\n statusCode: normalized.statusCode ?? 400,\n body: rejectBody,\n }\n }\n\n const hasModifications = !!(normalized.headers || normalized.body || normalized.query)\n logInterceptorActivity({\n interceptorId: interceptor.id,\n moduleId: entry.moduleId,\n route: routePath,\n method,\n result: hasModifications ? 'modified' : 'allowed',\n durationMs: Date.now() - startTime,\n timestamp: Date.now(),\n })\n\n if (normalized.headers) {\n currentRequest = {\n ...currentRequest,\n headers: {\n ...currentRequest.headers,\n ...normalized.headers,\n },\n }\n }\n if (normalized.body) {\n currentRequest = {\n ...currentRequest,\n body: sanitizeObject(normalized.body),\n }\n }\n if (normalized.query) {\n currentRequest = {\n ...currentRequest,\n query: sanitizeObject(normalized.query),\n }\n }\n metadataByInterceptor[interceptor.id] = normalized.metadata\n } catch (error) {\n if (isTimeoutError(error)) {\n const timeoutBody: Record<string, unknown> = { error: 'Interceptor timeout' }\n if (process.env.NODE_ENV !== 'production') {\n timeoutBody.interceptorId = interceptor.id\n }\n logInterceptorActivity({\n interceptorId: interceptor.id,\n moduleId: entry.moduleId,\n route: routePath,\n method,\n result: 'blocked',\n durationMs: Date.now() - startTime,\n timestamp: Date.now(),\n statusCode: 504,\n message: 'Interceptor timeout',\n })\n return {\n ok: false,\n statusCode: 504,\n body: timeoutBody,\n }\n }\n logInterceptorActivity({\n interceptorId: interceptor.id,\n moduleId: entry.moduleId,\n route: routePath,\n method,\n result: 'blocked',\n durationMs: Date.now() - startTime,\n timestamp: Date.now(),\n statusCode: 500,\n message: error instanceof Error ? error.message : String(error),\n })\n return {\n ok: false,\n statusCode: 500,\n body: toErrorBody(interceptor.id, error),\n }\n }\n }\n\n return {\n ok: true,\n request: currentRequest,\n metadataByInterceptor,\n }\n}\n\nexport async function runApiInterceptorsAfter(args: {\n routePath: string\n method: ApiInterceptorMethod\n request: InterceptorRequest\n response: InterceptorResponse\n context: Omit<InterceptorContext, 'metadata'>\n metadataByInterceptor?: Record<string, Record<string, unknown> | undefined>\n}): Promise<RunInterceptorsAfterResult> {\n const { routePath, method, context } = args\n let body: Record<string, unknown> = { ...(args.response.body ?? {}) }\n let headers: Record<string, string> = { ...(args.response.headers ?? {}) }\n\n const interceptors = getApiInterceptorsForRoute(routePath, method)\n for (const entry of interceptors) {\n const interceptor = entry.interceptor\n if (!interceptor.after) continue\n if (!hasRequiredFeatures(interceptor.features, context.userFeatures)) continue\n\n try {\n const timeoutMs = interceptor.timeoutMs ?? DEFAULT_TIMEOUT_MS\n const result = await runWithTimeout(\n interceptor.after(\n args.request,\n { statusCode: args.response.statusCode, body, headers },\n { ...context, metadata: args.metadataByInterceptor?.[interceptor.id] },\n ),\n timeoutMs,\n interceptor.id,\n )\n if (!result) continue\n if (result.replace && typeof result.replace === 'object') {\n body = { ...result.replace }\n } else if (result.merge && typeof result.merge === 'object') {\n body = { ...body, ...result.merge }\n }\n } catch (error) {\n if (isTimeoutError(error)) {\n const timeoutBody: Record<string, unknown> = { error: 'Interceptor timeout' }\n if (process.env.NODE_ENV !== 'production') {\n timeoutBody.interceptorId = interceptor.id\n }\n return {\n ok: false,\n statusCode: 504,\n body: timeoutBody,\n headers,\n }\n }\n return {\n ok: false,\n statusCode: 500,\n body: toErrorBody(interceptor.id, error),\n headers,\n }\n }\n }\n\n return {\n ok: true,\n statusCode: args.response.statusCode,\n body,\n headers,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- import { hasAllFeatures } from "../../security/features.js";
1
+ import { authorizeFeatures } from "../../security/featurePolicy.js";
2
2
  import { resolveCrudMutationGuardService } from "./mutation-guard-service.js";
3
3
  function matchesEntity(pattern, entity) {
4
4
  if (pattern === "*") return true;
@@ -10,7 +10,7 @@ function matchesEntity(pattern, entity) {
10
10
  return false;
11
11
  }
12
12
  async function runMutationGuards(guards, input, context) {
13
- const matching = guards.filter((g) => matchesEntity(g.targetEntity, input.resourceKind)).filter((g) => g.operations.includes(input.operation)).filter((g) => hasAllFeatures(context.userFeatures, g.features)).sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50));
13
+ const matching = guards.filter((g) => matchesEntity(g.targetEntity, input.resourceKind)).filter((g) => g.operations.includes(input.operation)).filter((g) => authorizeFeatures(g.features ?? [], { grantedFeatures: context.userFeatures })).sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50));
14
14
  let payload = input.mutationPayload;
15
15
  const afterSuccessCallbacks = [];
16
16
  for (const guard of matching) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/mutation-guard-registry.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { hasAllFeatures } from '../../security/features'\nimport { resolveCrudMutationGuardService } from './mutation-guard-service'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MutationGuard {\n /** Unique guard ID (e.g., 'record_locks.lock-check', 'example.todo-limit') */\n id: string\n\n /** Target entity or '*' for all entities */\n targetEntity: string | '*'\n\n /** Which operations this guard applies to */\n operations: ('create' | 'update' | 'delete')[]\n\n /** Execution priority (lower = earlier). Default: 50 */\n priority?: number\n\n /** ACL feature gating \u2014 guard only runs if user has these features */\n features?: string[]\n\n /** Validate before mutation. Return ok:false to block, modifiedPayload to transform. */\n validate(input: MutationGuardInput): Promise<MutationGuardResult>\n\n /** Optional post-mutation callback (for cleanup, cache invalidation, etc.) */\n afterSuccess?(input: MutationGuardAfterInput): Promise<void>\n}\n\nexport interface MutationGuardInput {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string | null\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport interface MutationGuardResult {\n ok: boolean\n /** HTTP status for rejection (default: 422) */\n status?: number\n /** Error message for rejection */\n message?: string\n /** Full error body for rejection (overrides message) */\n body?: Record<string, unknown>\n /** Modified payload \u2014 merged into mutation data if ok:true */\n modifiedPayload?: Record<string, unknown>\n /** Should afterSuccess run? (default: false) */\n shouldRunAfterSuccess?: boolean\n /** Arbitrary metadata passed to afterSuccess */\n metadata?: Record<string, unknown>\n}\n\nexport interface MutationGuardAfterInput {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\n// ---------------------------------------------------------------------------\n// Entity matching\n// ---------------------------------------------------------------------------\n\nexport function matchesEntity(pattern: string, entity: string): boolean {\n if (pattern === '*') return true\n if (pattern === entity) return true\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2)\n return entity.startsWith(prefix + '.')\n }\n return false\n}\n\n// ---------------------------------------------------------------------------\n// Guard runner\n// ---------------------------------------------------------------------------\n\nexport async function runMutationGuards(\n guards: MutationGuard[],\n input: MutationGuardInput,\n context: { userFeatures: string[] },\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const matching = guards\n .filter((g) => matchesEntity(g.targetEntity, input.resourceKind))\n .filter((g) => g.operations.includes(input.operation))\n .filter((g) => hasAllFeatures(context.userFeatures, g.features))\n .sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50))\n\n let payload = input.mutationPayload\n const afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }> = []\n\n for (const guard of matching) {\n const result = await guard.validate({ ...input, mutationPayload: payload })\n if (!result.ok) {\n const body = result.body ?? { error: result.message ?? 'Operation blocked by guard', guardId: guard.id }\n return { ok: false, errorBody: body, errorStatus: result.status ?? 422, afterSuccessCallbacks: [] }\n }\n if (result.modifiedPayload) payload = { ...payload, ...result.modifiedPayload }\n if (result.shouldRunAfterSuccess && guard.afterSuccess) {\n afterSuccessCallbacks.push({ guard, metadata: result.metadata ?? null })\n }\n }\n\n return { ok: true, modifiedPayload: payload !== input.mutationPayload ? (payload ?? undefined) : undefined, afterSuccessCallbacks }\n}\n\n// ---------------------------------------------------------------------------\n// Legacy guard bridge\n// ---------------------------------------------------------------------------\n\nexport function bridgeLegacyGuard(container: AwilixContainer): MutationGuard | null {\n const legacyService = resolveCrudMutationGuardService(container)\n if (!legacyService) return null\n\n return {\n id: '_legacy.crud-mutation-guard-service',\n targetEntity: '*',\n operations: ['update', 'delete'],\n priority: 0,\n\n async validate(input) {\n const result = await legacyService.validateMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId ?? '',\n operation: input.operation,\n requestMethod: input.requestMethod,\n requestHeaders: input.requestHeaders,\n mutationPayload: input.mutationPayload,\n })\n if (!result) return { ok: true }\n if (!result.ok) return { ok: false, status: result.status, body: result.body }\n return { ok: true, shouldRunAfterSuccess: result.shouldRunAfterSuccess, metadata: result.metadata ?? undefined }\n },\n\n async afterSuccess(input) {\n await legacyService.afterMutationSuccess({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n requestMethod: input.requestMethod,\n requestHeaders: input.requestHeaders,\n metadata: input.metadata,\n })\n },\n }\n}\n"],
5
- "mappings": "AACA,SAAS,sBAAsB;AAC/B,SAAS,uCAAuC;AAyEzC,SAAS,cAAc,SAAiB,QAAyB;AACtE,MAAI,YAAY,IAAK,QAAO;AAC5B,MAAI,YAAY,OAAQ,QAAO;AAC/B,MAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,UAAM,SAAS,QAAQ,MAAM,GAAG,EAAE;AAClC,WAAO,OAAO,WAAW,SAAS,GAAG;AAAA,EACvC;AACA,SAAO;AACT;AAMA,eAAsB,kBACpB,QACA,OACA,SAOC;AACD,QAAM,WAAW,OACd,OAAO,CAAC,MAAM,cAAc,EAAE,cAAc,MAAM,YAAY,CAAC,EAC/D,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS,MAAM,SAAS,CAAC,EACpD,OAAO,CAAC,MAAM,eAAe,QAAQ,cAAc,EAAE,QAAQ,CAAC,EAC9D,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,EAAE,YAAY,GAAG;AAEzD,MAAI,UAAU,MAAM;AACpB,QAAM,wBAAmG,CAAC;AAE1G,aAAW,SAAS,UAAU;AAC5B,UAAM,SAAS,MAAM,MAAM,SAAS,EAAE,GAAG,OAAO,iBAAiB,QAAQ,CAAC;AAC1E,QAAI,CAAC,OAAO,IAAI;AACd,YAAM,OAAO,OAAO,QAAQ,EAAE,OAAO,OAAO,WAAW,8BAA8B,SAAS,MAAM,GAAG;AACvG,aAAO,EAAE,IAAI,OAAO,WAAW,MAAM,aAAa,OAAO,UAAU,KAAK,uBAAuB,CAAC,EAAE;AAAA,IACpG;AACA,QAAI,OAAO,gBAAiB,WAAU,EAAE,GAAG,SAAS,GAAG,OAAO,gBAAgB;AAC9E,QAAI,OAAO,yBAAyB,MAAM,cAAc;AACtD,4BAAsB,KAAK,EAAE,OAAO,UAAU,OAAO,YAAY,KAAK,CAAC;AAAA,IACzE;AAAA,EACF;AAEA,SAAO,EAAE,IAAI,MAAM,iBAAiB,YAAY,MAAM,kBAAmB,WAAW,SAAa,QAAW,sBAAsB;AACpI;AAMO,SAAS,kBAAkB,WAAkD;AAClF,QAAM,gBAAgB,gCAAgC,SAAS;AAC/D,MAAI,CAAC,cAAe,QAAO;AAE3B,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,cAAc;AAAA,IACd,YAAY,CAAC,UAAU,QAAQ;AAAA,IAC/B,UAAU;AAAA,IAEV,MAAM,SAAS,OAAO;AACpB,YAAM,SAAS,MAAM,cAAc,iBAAiB;AAAA,QAClD,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM,cAAc;AAAA,QAChC,WAAW,MAAM;AAAA,QACjB,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QACtB,iBAAiB,MAAM;AAAA,MACzB,CAAC;AACD,UAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,KAAK;AAC/B,UAAI,CAAC,OAAO,GAAI,QAAO,EAAE,IAAI,OAAO,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AAC7E,aAAO,EAAE,IAAI,MAAM,uBAAuB,OAAO,uBAAuB,UAAU,OAAO,YAAY,OAAU;AAAA,IACjH;AAAA,IAEA,MAAM,aAAa,OAAO;AACxB,YAAM,cAAc,qBAAqB;AAAA,QACvC,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport { authorizeFeatures } from '../../security/featurePolicy'\nimport { resolveCrudMutationGuardService } from './mutation-guard-service'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface MutationGuard {\n /** Unique guard ID (e.g., 'record_locks.lock-check', 'example.todo-limit') */\n id: string\n\n /** Target entity or '*' for all entities */\n targetEntity: string | '*'\n\n /** Which operations this guard applies to */\n operations: ('create' | 'update' | 'delete')[]\n\n /** Execution priority (lower = earlier). Default: 50 */\n priority?: number\n\n /** ACL feature gating \u2014 guard only runs if user has these features */\n features?: string[]\n\n /** Validate before mutation. Return ok:false to block, modifiedPayload to transform. */\n validate(input: MutationGuardInput): Promise<MutationGuardResult>\n\n /** Optional post-mutation callback (for cleanup, cache invalidation, etc.) */\n afterSuccess?(input: MutationGuardAfterInput): Promise<void>\n}\n\nexport interface MutationGuardInput {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string | null\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport interface MutationGuardResult {\n ok: boolean\n /** HTTP status for rejection (default: 422) */\n status?: number\n /** Error message for rejection */\n message?: string\n /** Full error body for rejection (overrides message) */\n body?: Record<string, unknown>\n /** Modified payload \u2014 merged into mutation data if ok:true */\n modifiedPayload?: Record<string, unknown>\n /** Should afterSuccess run? (default: false) */\n shouldRunAfterSuccess?: boolean\n /** Arbitrary metadata passed to afterSuccess */\n metadata?: Record<string, unknown>\n}\n\nexport interface MutationGuardAfterInput {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n metadata?: Record<string, unknown> | null\n}\n\n// ---------------------------------------------------------------------------\n// Entity matching\n// ---------------------------------------------------------------------------\n\nexport function matchesEntity(pattern: string, entity: string): boolean {\n if (pattern === '*') return true\n if (pattern === entity) return true\n if (pattern.endsWith('.*')) {\n const prefix = pattern.slice(0, -2)\n return entity.startsWith(prefix + '.')\n }\n return false\n}\n\n// ---------------------------------------------------------------------------\n// Guard runner\n// ---------------------------------------------------------------------------\n\nexport async function runMutationGuards(\n guards: MutationGuard[],\n input: MutationGuardInput,\n context: { userFeatures: string[] },\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const matching = guards\n .filter((g) => matchesEntity(g.targetEntity, input.resourceKind))\n .filter((g) => g.operations.includes(input.operation))\n .filter((g) => authorizeFeatures(g.features ?? [], { grantedFeatures: context.userFeatures }))\n .sort((a, b) => (a.priority ?? 50) - (b.priority ?? 50))\n\n let payload = input.mutationPayload\n const afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }> = []\n\n for (const guard of matching) {\n const result = await guard.validate({ ...input, mutationPayload: payload })\n if (!result.ok) {\n const body = result.body ?? { error: result.message ?? 'Operation blocked by guard', guardId: guard.id }\n return { ok: false, errorBody: body, errorStatus: result.status ?? 422, afterSuccessCallbacks: [] }\n }\n if (result.modifiedPayload) payload = { ...payload, ...result.modifiedPayload }\n if (result.shouldRunAfterSuccess && guard.afterSuccess) {\n afterSuccessCallbacks.push({ guard, metadata: result.metadata ?? null })\n }\n }\n\n return { ok: true, modifiedPayload: payload !== input.mutationPayload ? (payload ?? undefined) : undefined, afterSuccessCallbacks }\n}\n\n// ---------------------------------------------------------------------------\n// Legacy guard bridge\n// ---------------------------------------------------------------------------\n\nexport function bridgeLegacyGuard(container: AwilixContainer): MutationGuard | null {\n const legacyService = resolveCrudMutationGuardService(container)\n if (!legacyService) return null\n\n return {\n id: '_legacy.crud-mutation-guard-service',\n targetEntity: '*',\n operations: ['update', 'delete'],\n priority: 0,\n\n async validate(input) {\n const result = await legacyService.validateMutation({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId ?? '',\n operation: input.operation,\n requestMethod: input.requestMethod,\n requestHeaders: input.requestHeaders,\n mutationPayload: input.mutationPayload,\n })\n if (!result) return { ok: true }\n if (!result.ok) return { ok: false, status: result.status, body: result.body }\n return { ok: true, shouldRunAfterSuccess: result.shouldRunAfterSuccess, metadata: result.metadata ?? undefined }\n },\n\n async afterSuccess(input) {\n await legacyService.afterMutationSuccess({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n operation: input.operation,\n requestMethod: input.requestMethod,\n requestHeaders: input.requestHeaders,\n metadata: input.metadata,\n })\n },\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,yBAAyB;AAClC,SAAS,uCAAuC;AAyEzC,SAAS,cAAc,SAAiB,QAAyB;AACtE,MAAI,YAAY,IAAK,QAAO;AAC5B,MAAI,YAAY,OAAQ,QAAO;AAC/B,MAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,UAAM,SAAS,QAAQ,MAAM,GAAG,EAAE;AAClC,WAAO,OAAO,WAAW,SAAS,GAAG;AAAA,EACvC;AACA,SAAO;AACT;AAMA,eAAsB,kBACpB,QACA,OACA,SAOC;AACD,QAAM,WAAW,OACd,OAAO,CAAC,MAAM,cAAc,EAAE,cAAc,MAAM,YAAY,CAAC,EAC/D,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS,MAAM,SAAS,CAAC,EACpD,OAAO,CAAC,MAAM,kBAAkB,EAAE,YAAY,CAAC,GAAG,EAAE,iBAAiB,QAAQ,aAAa,CAAC,CAAC,EAC5F,KAAK,CAAC,GAAG,OAAO,EAAE,YAAY,OAAO,EAAE,YAAY,GAAG;AAEzD,MAAI,UAAU,MAAM;AACpB,QAAM,wBAAmG,CAAC;AAE1G,aAAW,SAAS,UAAU;AAC5B,UAAM,SAAS,MAAM,MAAM,SAAS,EAAE,GAAG,OAAO,iBAAiB,QAAQ,CAAC;AAC1E,QAAI,CAAC,OAAO,IAAI;AACd,YAAM,OAAO,OAAO,QAAQ,EAAE,OAAO,OAAO,WAAW,8BAA8B,SAAS,MAAM,GAAG;AACvG,aAAO,EAAE,IAAI,OAAO,WAAW,MAAM,aAAa,OAAO,UAAU,KAAK,uBAAuB,CAAC,EAAE;AAAA,IACpG;AACA,QAAI,OAAO,gBAAiB,WAAU,EAAE,GAAG,SAAS,GAAG,OAAO,gBAAgB;AAC9E,QAAI,OAAO,yBAAyB,MAAM,cAAc;AACtD,4BAAsB,KAAK,EAAE,OAAO,UAAU,OAAO,YAAY,KAAK,CAAC;AAAA,IACzE;AAAA,EACF;AAEA,SAAO,EAAE,IAAI,MAAM,iBAAiB,YAAY,MAAM,kBAAmB,WAAW,SAAa,QAAW,sBAAsB;AACpI;AAMO,SAAS,kBAAkB,WAAkD;AAClF,QAAM,gBAAgB,gCAAgC,SAAS;AAC/D,MAAI,CAAC,cAAe,QAAO;AAE3B,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,cAAc;AAAA,IACd,YAAY,CAAC,UAAU,QAAQ;AAAA,IAC/B,UAAU;AAAA,IAEV,MAAM,SAAS,OAAO;AACpB,YAAM,SAAS,MAAM,cAAc,iBAAiB;AAAA,QAClD,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM,cAAc;AAAA,QAChC,WAAW,MAAM;AAAA,QACjB,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QACtB,iBAAiB,MAAM;AAAA,MACzB,CAAC;AACD,UAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,KAAK;AAC/B,UAAI,CAAC,OAAO,GAAI,QAAO,EAAE,IAAI,OAAO,QAAQ,OAAO,QAAQ,MAAM,OAAO,KAAK;AAC7E,aAAO,EAAE,IAAI,MAAM,uBAAuB,OAAO,uBAAuB,UAAU,OAAO,YAAY,OAAU;AAAA,IACjH;AAAA,IAEA,MAAM,aAAa,OAAO;AACxB,YAAM,cAAc,qBAAqB;AAAA,QACvC,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,eAAe,MAAM;AAAA,QACrB,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1 +1,5 @@
1
+ const CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY = "__omQueryIndexManaged";
2
+ export {
3
+ CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY
4
+ };
1
5
  //# sourceMappingURL=types.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": [],
4
- "sourcesContent": [],
5
- "mappings": "",
3
+ "sources": ["../../../src/lib/crud/types.ts"],
4
+ "sourcesContent": ["export type CrudEventAction = 'created' | 'updated' | 'deleted'\n\n/** Internal payload marker: the data engine owns this CRUD event's query-index decision. */\nexport const CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY = '__omQueryIndexManaged' as const\n\nexport type CrudEntityIdentifiers = {\n id: string\n organizationId: string | null\n tenantId: string | null\n}\n\nexport type CrudEmitContext<TEntity = unknown> = {\n action: CrudEventAction\n entity: TEntity\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n actorUserId?: string | null\n}\n\nexport type CrudEventsConfig<TEntity = unknown> = {\n module: string\n entity: string\n persistent?: boolean\n buildPayload?(ctx: CrudEmitContext<TEntity>): unknown\n}\n\nexport type CrudIndexerConfig<TEntity = unknown> = {\n entityType: string\n buildUpsertPayload?(ctx: CrudEmitContext<TEntity>): unknown\n buildDeletePayload?(ctx: CrudEmitContext<TEntity>): unknown\n cacheAliases?: string[]\n}\n\nexport type CrudIdentifierResolver<TEntity = unknown> = (entity: TEntity, action: CrudEventAction) => CrudEntityIdentifiers\n"],
5
+ "mappings": "AAGO,MAAM,uCAAuC;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,19 @@
1
+ import { parseBooleanWithDefault } from "../boolean.js";
2
+ let alwaysConsistentFlag = null;
3
+ function parseAlwaysConsistentEnv(raw) {
4
+ return parseBooleanWithDefault(raw, false);
5
+ }
6
+ function isReadProjectionAlwaysConsistent() {
7
+ if (alwaysConsistentFlag !== null) return alwaysConsistentFlag;
8
+ alwaysConsistentFlag = parseAlwaysConsistentEnv(process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT);
9
+ return alwaysConsistentFlag;
10
+ }
11
+ function __resetAlwaysConsistentCacheForTests() {
12
+ alwaysConsistentFlag = null;
13
+ }
14
+ export {
15
+ __resetAlwaysConsistentCacheForTests,
16
+ isReadProjectionAlwaysConsistent,
17
+ parseAlwaysConsistentEnv
18
+ };
19
+ //# sourceMappingURL=consistency.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/data/consistency.ts"],
4
+ "sourcesContent": ["import { parseBooleanWithDefault } from '../boolean'\n\nlet alwaysConsistentFlag: boolean | null = null\n\nexport function parseAlwaysConsistentEnv(raw: string | undefined | null): boolean {\n return parseBooleanWithDefault(raw, false)\n}\n\nexport function isReadProjectionAlwaysConsistent(): boolean {\n if (alwaysConsistentFlag !== null) return alwaysConsistentFlag\n alwaysConsistentFlag = parseAlwaysConsistentEnv(process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT)\n return alwaysConsistentFlag\n}\n\nexport function __resetAlwaysConsistentCacheForTests(): void {\n alwaysConsistentFlag = null\n}\n"],
5
+ "mappings": "AAAA,SAAS,+BAA+B;AAExC,IAAI,uBAAuC;AAEpC,SAAS,yBAAyB,KAAyC;AAChF,SAAO,wBAAwB,KAAK,KAAK;AAC3C;AAEO,SAAS,mCAA4C;AAC1D,MAAI,yBAAyB,KAAM,QAAO;AAC1C,yBAAuB,yBAAyB,QAAQ,IAAI,iCAAiC;AAC7F,SAAO;AACT;AAEO,SAAS,uCAA6C;AAC3D,yBAAuB;AACzB;",
6
+ "names": []
7
+ }
@@ -2,11 +2,15 @@ import { sql } from "kysely";
2
2
  import { setRecordCustomFields } from "@open-mercato/core/modules/entities/lib/helpers";
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
+ import {
6
+ CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY
7
+ } from "../crud/types.js";
5
8
  import { CrudHttpError } from "../crud/errors.js";
6
9
  import { resolveRegisteredEntityTableName } from "../query/engine.js";
7
10
  import { getEntityIds } from "../encryption/entityIds.js";
8
11
  import { normalizeCustomFieldValues } from "../custom-fields/normalize.js";
9
12
  import { parseBooleanToken } from "../boolean.js";
13
+ import { isReadProjectionAlwaysConsistent } from "./consistency.js";
10
14
  import { isEventDeclared } from "../../modules/events/index.js";
11
15
  import { createLogger } from "../logger/index.js";
12
16
  const logger = createLogger("shared").child({ component: "data-engine" });
@@ -383,12 +387,17 @@ class DefaultDataEngine {
383
387
  if (events && !suppress?.skipEvents) {
384
388
  const eventName = `${events.module}.${events.entity}.${action}`;
385
389
  warnIfUndeclaredEvent(eventName, "emitOrmEntityEvent");
386
- const payload = events.buildPayload ? events.buildPayload(ctx) : {
390
+ const builtPayload = events.buildPayload ? events.buildPayload(ctx) : {
387
391
  id: ctx.identifiers.id,
388
392
  organizationId: ctx.identifiers.organizationId,
389
393
  tenantId: ctx.identifiers.tenantId,
390
394
  ...ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}
391
395
  };
396
+ const payload = indexer && builtPayload && typeof builtPayload === "object" && !Array.isArray(builtPayload) ? Object.defineProperty(
397
+ { ...builtPayload },
398
+ CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY,
399
+ { value: true, enumerable: false }
400
+ ) : builtPayload;
392
401
  try {
393
402
  await bus.emitEvent(eventName, payload, {
394
403
  persistent: !!events.persistent,
@@ -399,6 +408,7 @@ class DefaultDataEngine {
399
408
  }
400
409
  }
401
410
  if (indexer && !suppress?.skipReindex) {
411
+ const alwaysConsistent = isReadProjectionAlwaysConsistent();
402
412
  const resolveCoverageBaseDelta = () => {
403
413
  if (action === "created") return 1;
404
414
  if (action === "deleted") return -1;
@@ -416,9 +426,13 @@ class DefaultDataEngine {
416
426
  enrichedPayload.crudAction = action;
417
427
  if (coverageBaseDelta !== void 0) enrichedPayload.coverageBaseDelta = coverageBaseDelta;
418
428
  if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin;
419
- await bus.emitEvent("query_index.delete_one", enrichedPayload).catch((err) => {
420
- logger.error("query_index.delete_one emit failed", { err });
421
- });
429
+ if (alwaysConsistent) {
430
+ await bus.emitEvent("query_index.delete_one", enrichedPayload, { rethrowHandlerErrors: true });
431
+ } else {
432
+ await bus.emitEvent("query_index.delete_one", enrichedPayload).catch((err) => {
433
+ logger.error("query_index.delete_one emit failed", { err });
434
+ });
435
+ }
422
436
  } else {
423
437
  const payload = indexer.buildUpsertPayload ? indexer.buildUpsertPayload(ctx) : {
424
438
  entityType: indexer.entityType,
@@ -430,17 +444,26 @@ class DefaultDataEngine {
430
444
  enrichedPayload.crudAction = action;
431
445
  if (coverageBaseDelta !== void 0) enrichedPayload.coverageBaseDelta = coverageBaseDelta;
432
446
  if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin;
433
- await bus.emitEvent("query_index.upsert_one", enrichedPayload).catch((err) => {
434
- logger.error("query_index.upsert_one emit failed", { err });
435
- });
447
+ if (alwaysConsistent) {
448
+ await bus.emitEvent("query_index.upsert_one", enrichedPayload, { rethrowHandlerErrors: true });
449
+ } else {
450
+ await bus.emitEvent("query_index.upsert_one", enrichedPayload).catch((err) => {
451
+ logger.error("query_index.upsert_one emit failed", { err });
452
+ });
453
+ }
436
454
  }
437
- if (shouldTriggerCoverageRefresh(indexer.entityType, ctx.identifiers.tenantId ?? null)) {
438
- void bus.emitEvent("query_index.coverage.refresh", {
455
+ if (alwaysConsistent || shouldTriggerCoverageRefresh(indexer.entityType, ctx.identifiers.tenantId ?? null)) {
456
+ const coveragePayload = {
439
457
  entityType: indexer.entityType,
440
458
  tenantId: ctx.identifiers.tenantId ?? null,
441
459
  organizationId: null,
442
460
  delayMs: 0
443
- }).catch(() => void 0);
461
+ };
462
+ if (alwaysConsistent) {
463
+ await bus.emitEvent("query_index.coverage.refresh", coveragePayload, { rethrowHandlerErrors: true });
464
+ } else {
465
+ void bus.emitEvent("query_index.coverage.refresh", coveragePayload).catch(() => void 0);
466
+ }
444
467
  }
445
468
  }
446
469
  }
@@ -495,7 +518,10 @@ class DefaultDataEngine {
495
518
  indexer: entry.indexer,
496
519
  suppress
497
520
  });
498
- } catch {
521
+ } catch (error) {
522
+ if (isReadProjectionAlwaysConsistent()) {
523
+ throw error;
524
+ }
499
525
  }
500
526
  }
501
527
  }
@@ -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 type { BulkImportSuppression } from '../commands/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'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'data-engine' })\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 logger.warn('Emitting undeclared event \u2014 declare it in the owning module events.ts (createModuleEvents) so the event registry stays authoritative', { context, eventName })\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 actorUserId?: 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 actorUserId?: string | null\n /** Bulk-import deferral: skip the domain event and/or inline reindex for this emit. */\n suppress?: BulkImportSuppression\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 actorUserId?: string | null\n }): void\n\n /**\n * Drain queued side effects. When `suppress` is passed (a bulk-import backfill), the\n * flagged per-record events / reindex are skipped for every drained entry; the caller\n * is responsible for rebuilding the `query_index` afterwards.\n */\n flushOrmEntityChanges(suppress?: BulkImportSuppression): 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 .where('tenant_id' as any, tenantId === null ? 'is' : '=', tenantId 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 chain = tenantId === null\n ? chain.where('tenant_id' as any, 'is', null as any)\n : chain.where('tenant_id' as any, '=', tenantId)\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 tenantId = opts.tenantId ?? 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 chain = tenantId === null\n ? chain.where('tenant_id' as any, 'is', null as any)\n : chain.where('tenant_id' as any, '=', tenantId)\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 actorUserId?: string | null\n suppress?: BulkImportSuppression\n }): Promise<void> {\n const { action, entity, events, indexer, identifiers, syncOrigin, suppress } = opts\n // Bulk-import deferral: an entry may suppress its domain event and/or inline reindex. When both\n // the config is absent AND (for the present one) suppressed, there is nothing left to do.\n const emitEvents = !!events && !suppress?.skipEvents\n const runIndexer = !!indexer && !suppress?.skipReindex\n if (!emitEvents && !runIndexer) 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 actorUserId: opts.actorUserId ?? null,\n }\n\n if (events && !suppress?.skipEvents) {\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 && !suppress?.skipReindex) {\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 logger.error('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 logger.error('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 actorUserId?: 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 existing.actorUserId = opts.actorUserId ?? 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 actorUserId: opts.actorUserId ?? 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(suppress?: BulkImportSuppression): 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 actorUserId: entry.actorUserId ?? null,\n events: entry.events as CrudEventsConfig<unknown>,\n indexer: entry.indexer as CrudIndexerConfig<unknown>,\n suppress,\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 {\n CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY,\n type CrudEventAction,\n type CrudEventsConfig,\n type CrudIndexerConfig,\n type CrudEntityIdentifiers,\n} from '../crud/types'\nimport type { BulkImportSuppression } from '../commands/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 { isReadProjectionAlwaysConsistent } from './consistency'\nimport { isEventDeclared } from '../../modules/events'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'data-engine' })\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 logger.warn('Emitting undeclared event \u2014 declare it in the owning module events.ts (createModuleEvents) so the event registry stays authoritative', { context, eventName })\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 actorUserId?: 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 actorUserId?: string | null\n /** Bulk-import deferral: skip the domain event and/or inline reindex for this emit. */\n suppress?: BulkImportSuppression\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 actorUserId?: string | null\n }): void\n\n /**\n * Drain queued side effects. When `suppress` is passed (a bulk-import backfill), the\n * flagged per-record events / reindex are skipped for every drained entry; the caller\n * is responsible for rebuilding the `query_index` afterwards.\n */\n flushOrmEntityChanges(suppress?: BulkImportSuppression): 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 .where('tenant_id' as any, tenantId === null ? 'is' : '=', tenantId 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 chain = tenantId === null\n ? chain.where('tenant_id' as any, 'is', null as any)\n : chain.where('tenant_id' as any, '=', tenantId)\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 tenantId = opts.tenantId ?? 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 chain = tenantId === null\n ? chain.where('tenant_id' as any, 'is', null as any)\n : chain.where('tenant_id' as any, '=', tenantId)\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 actorUserId?: string | null\n suppress?: BulkImportSuppression\n }): Promise<void> {\n const { action, entity, events, indexer, identifiers, syncOrigin, suppress } = opts\n // Bulk-import deferral: an entry may suppress its domain event and/or inline reindex. When both\n // the config is absent AND (for the present one) suppressed, there is nothing left to do.\n const emitEvents = !!events && !suppress?.skipEvents\n const runIndexer = !!indexer && !suppress?.skipReindex\n if (!emitEvents && !runIndexer) 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 actorUserId: opts.actorUserId ?? null,\n }\n\n if (events && !suppress?.skipEvents) {\n const eventName = `${events.module}.${events.entity}.${action}`\n warnIfUndeclaredEvent(eventName, 'emitOrmEntityEvent')\n const builtPayload = 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 // A configured indexer means this data-engine call owns the query-index\n // decision, including an explicit skipReindex suppression. Mark object\n // payloads so the legacy domain-event bridge does not enqueue the same\n // record a second time. Keep the marker non-enumerable so client broadcasts\n // and persisted domain payloads retain their existing public shape.\n // Primitive custom payloads cannot be bridged in any case because they do\n // not expose the record id.\n const payload = indexer && builtPayload && typeof builtPayload === 'object' && !Array.isArray(builtPayload)\n ? Object.defineProperty(\n { ...(builtPayload as Record<string, unknown>) },\n CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY,\n { value: true, enumerable: false },\n )\n : builtPayload\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 && !suppress?.skipReindex) {\n const alwaysConsistent = isReadProjectionAlwaysConsistent()\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 // Always-consistent mode rethrows so drift is loud and retryable.\n if (alwaysConsistent) {\n await bus.emitEvent('query_index.delete_one', enrichedPayload, { rethrowHandlerErrors: true })\n } else {\n await bus.emitEvent('query_index.delete_one', enrichedPayload).catch((err: unknown) => {\n logger.error('query_index.delete_one emit failed', { err })\n })\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 if (alwaysConsistent) {\n await bus.emitEvent('query_index.upsert_one', enrichedPayload, { rethrowHandlerErrors: true })\n } else {\n await bus.emitEvent('query_index.upsert_one', enrichedPayload).catch((err: unknown) => {\n logger.error('query_index.upsert_one emit failed', { err })\n })\n }\n }\n\n if (alwaysConsistent || shouldTriggerCoverageRefresh(indexer.entityType, ctx.identifiers.tenantId ?? null)) {\n const coveragePayload = {\n entityType: indexer.entityType,\n tenantId: ctx.identifiers.tenantId ?? null,\n organizationId: null,\n delayMs: 0,\n }\n if (alwaysConsistent) {\n await bus.emitEvent('query_index.coverage.refresh', coveragePayload, { rethrowHandlerErrors: true })\n } else {\n void bus.emitEvent('query_index.coverage.refresh', coveragePayload).catch(() => undefined)\n }\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 actorUserId?: 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 existing.actorUserId = opts.actorUserId ?? 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 actorUserId: opts.actorUserId ?? 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(suppress?: BulkImportSuppression): 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 actorUserId: entry.actorUserId ?? null,\n events: entry.events as CrudEventsConfig<unknown>,\n indexer: entry.indexer as CrudIndexerConfig<unknown>,\n suppress,\n })\n } catch (error) {\n if (isReadProjectionAlwaysConsistent()) {\n throw error\n }\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
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.7-develop.6775.1.c2313bb8a3";
1
+ const APP_VERSION = "0.6.7-develop.6785.1.1dd7cfac55";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6775.1.c2313bb8a3'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6785.1.1dd7cfac55'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }