@open-mercato/shared 0.7.1-develop.7149.1.7efa6e1612 → 0.7.1-develop.7151.1.00d0391847
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/lib/crud/factory.js +43 -3
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/types.js.map +1 -1
- package/dist/lib/data/engine.js +32 -1
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/email/config.js +20 -0
- package/dist/lib/email/config.js.map +2 -2
- package/dist/lib/email/send.js +25 -22
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/email/transport.js +19 -0
- package/dist/lib/email/transport.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +173 -1
- package/src/lib/crud/factory.ts +67 -12
- package/src/lib/crud/types.ts +23 -0
- package/src/lib/data/__tests__/engine.default-indexer.test.ts +188 -0
- package/src/lib/data/engine.ts +74 -1
- package/src/lib/email/__tests__/send.test.ts +140 -69
- package/src/lib/email/config.ts +26 -1
- package/src/lib/email/send.ts +59 -37
- package/src/lib/email/transport.ts +29 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
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"],
|
|
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\n/**\n * Declares that a CRUD write maintains the `query_index` projection for `entityType`.\n *\n * On `makeCrudRoute`'s built-in write path (`create` / `update` / `del`) the route emits the\n * projection event itself. On the command path (`actions.*`) the command handler owns the\n * side-effect mark and the command bus owns the flush, so the route's declaration is applied\n * to the handler's mark: a handler that calls `emitCrudSideEffects({ events })` without an\n * `indexer` still indexes the record under this `entityType`, and one that passes its own\n * `indexer` keeps it. A handler that marks no side effect at all indexes nothing \u2014 the route\n * logs a warning naming the command when that happens.\n *\n * Two limits of that hand-down are worth knowing before you rely on it:\n *\n * - It is scoped to one `CommandBus.execute()`, and the declaration lives on the request's\n * `DataEngine` instance. That is sound because `createRequestContainer()` registers\n * `dataEngine` per request; re-registering it as a transient would leave the command marking\n * on a different instance than the route declared on, so nothing is indexed and every write\n * logs the warning.\n * - `CommandBus.undo()` runs outside any route, so no declaration is active there. An undo\n * handler that must maintain the projection MUST pass its own `indexer` to\n * `emitCrudUndoSideEffects` \u2014 otherwise undoing a delete restores the row in the database and\n * leaves it missing from `query_index` until the next full rebuild.\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
5
|
"mappings": "AAGO,MAAM,uCAAuC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/data/engine.js
CHANGED
|
@@ -56,6 +56,32 @@ class DefaultDataEngine {
|
|
|
56
56
|
this.em = em;
|
|
57
57
|
this.container = container;
|
|
58
58
|
this.pendingSideEffects = /* @__PURE__ */ new Map();
|
|
59
|
+
this.defaultIndexer = null;
|
|
60
|
+
this.indexedDefaultEntityClass = false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Per-command state, deliberately held on the engine instance rather than threaded through
|
|
64
|
+
* `CommandRuntimeContext` the way the bulk-import flags are. That is sound only because
|
|
65
|
+
* `createRequestContainer()` registers `dataEngine` per request (`lib/di/container.ts`), so
|
|
66
|
+
* one engine instance never spans two requests, and no `makeCrudRoute` verb runs two commands
|
|
67
|
+
* concurrently against it. An application that re-registers `dataEngine` as a transient would
|
|
68
|
+
* break both assumptions: the command would mark on a different instance than the route
|
|
69
|
+
* declared on, so nothing is indexed and every write logs the undischarged-declaration warning.
|
|
70
|
+
*/
|
|
71
|
+
setDefaultIndexerConfig(config) {
|
|
72
|
+
this.defaultIndexer = config;
|
|
73
|
+
this.indexedDefaultEntityClass = false;
|
|
74
|
+
}
|
|
75
|
+
hasIndexedDefaultEntityClass() {
|
|
76
|
+
return this.indexedDefaultEntityClass;
|
|
77
|
+
}
|
|
78
|
+
matchesDefaultEntityClass(entity) {
|
|
79
|
+
const declared = this.defaultIndexer?.entityClass;
|
|
80
|
+
if (typeof declared !== "function") return false;
|
|
81
|
+
return entity instanceof declared;
|
|
82
|
+
}
|
|
83
|
+
resolveDefaultIndexer(entity) {
|
|
84
|
+
return this.matchesDefaultEntityClass(entity) ? this.defaultIndexer?.indexer : void 0;
|
|
59
85
|
}
|
|
60
86
|
async setCustomFields(opts) {
|
|
61
87
|
const { entityId, recordId, organizationId = null, tenantId = null, values } = opts;
|
|
@@ -477,6 +503,7 @@ class DefaultDataEngine {
|
|
|
477
503
|
const { entity, identifiers } = opts;
|
|
478
504
|
if (!entity) return;
|
|
479
505
|
if (!identifiers?.id) return;
|
|
506
|
+
const indexer = opts.indexer ?? this.resolveDefaultIndexer(entity);
|
|
480
507
|
const key = this.buildSideEffectKey(opts.action, identifiers);
|
|
481
508
|
const existing = this.pendingSideEffects.get(key);
|
|
482
509
|
if (existing) {
|
|
@@ -490,6 +517,7 @@ class DefaultDataEngine {
|
|
|
490
517
|
existing.actorUserId = opts.actorUserId ?? null;
|
|
491
518
|
if (opts.events) existing.events = opts.events;
|
|
492
519
|
if (opts.indexer) existing.indexer = opts.indexer;
|
|
520
|
+
else if (!existing.indexer && indexer) existing.indexer = indexer;
|
|
493
521
|
this.pendingSideEffects.set(key, existing);
|
|
494
522
|
return;
|
|
495
523
|
}
|
|
@@ -505,7 +533,7 @@ class DefaultDataEngine {
|
|
|
505
533
|
actorUserId: opts.actorUserId ?? null
|
|
506
534
|
};
|
|
507
535
|
if (opts.events) entry.events = opts.events;
|
|
508
|
-
if (
|
|
536
|
+
if (indexer) entry.indexer = indexer;
|
|
509
537
|
this.pendingSideEffects.set(key, entry);
|
|
510
538
|
}
|
|
511
539
|
async flushOrmEntityChanges(suppress) {
|
|
@@ -513,6 +541,9 @@ class DefaultDataEngine {
|
|
|
513
541
|
const entries = Array.from(this.pendingSideEffects.values());
|
|
514
542
|
this.pendingSideEffects.clear();
|
|
515
543
|
for (const entry of entries) {
|
|
544
|
+
if (entry.indexer && !suppress?.skipReindex && this.matchesDefaultEntityClass(entry.entity)) {
|
|
545
|
+
this.indexedDefaultEntityClass = true;
|
|
546
|
+
}
|
|
516
547
|
try {
|
|
517
548
|
await this.emitOrmEntityEvent({
|
|
518
549
|
action: entry.action,
|
|
@@ -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 {\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 }, {\n persistent: true,\n tenantId,\n organizationId,\n emitterModuleId: mod,\n })\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 emitterModuleId: events.module,\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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",
|
|
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\n/**\n * A `makeCrudRoute` route-level `indexer:` declaration, handed to the data engine for the\n * duration of one command-bus write. Command handlers own the side-effect mark on the\n * `actions.*` path, and most of them mark `events:` only \u2014 without this the route's\n * declaration would reach no code at all. `entityClass` scopes the default to the route's\n * own ORM entity so a handler that also marks a sibling entity in the same request (a tag\n * assignment alongside a tag, say) is never indexed under the route's `entityType`.\n */\nexport type DefaultCrudIndexerConfig = {\n indexer: CrudIndexerConfig<unknown>\n entityClass: abstract new (...args: never[]) => object\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 /**\n * Declare the indexer a CRUD route configured, for marks made during one command-bus\n * write that do not carry an indexer of their own. Pass `null` to clear it. Optional so\n * third-party `DataEngine` implementations stay valid; callers invoke it with `?.`.\n */\n setDefaultIndexerConfig?(config: DefaultCrudIndexerConfig | null): void\n\n /**\n * Whether any side effect drained since the current default was declared carried an\n * indexer for that default's entity class \u2014 false means the declared query-index\n * obligation was discharged by nobody. Optional for the same reason as the setter.\n */\n hasIndexedDefaultEntityClass?(): boolean\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 private defaultIndexer: DefaultCrudIndexerConfig | null = null\n private indexedDefaultEntityClass = false\n constructor(private em: EntityManager, private container: AwilixContainer) {}\n\n /**\n * Per-command state, deliberately held on the engine instance rather than threaded through\n * `CommandRuntimeContext` the way the bulk-import flags are. That is sound only because\n * `createRequestContainer()` registers `dataEngine` per request (`lib/di/container.ts`), so\n * one engine instance never spans two requests, and no `makeCrudRoute` verb runs two commands\n * concurrently against it. An application that re-registers `dataEngine` as a transient would\n * break both assumptions: the command would mark on a different instance than the route\n * declared on, so nothing is indexed and every write logs the undischarged-declaration warning.\n */\n setDefaultIndexerConfig(config: DefaultCrudIndexerConfig | null): void {\n this.defaultIndexer = config\n this.indexedDefaultEntityClass = false\n }\n\n hasIndexedDefaultEntityClass(): boolean {\n return this.indexedDefaultEntityClass\n }\n\n private matchesDefaultEntityClass(entity: unknown): boolean {\n const declared = this.defaultIndexer?.entityClass\n // `OrmEntityConfig.entity` is `any` and this repository treats `EntitySchema` instances as a\n // first-class entity shape (`lib/bootstrap/types.ts`). An `EntitySchema` is an object rather\n // than a constructor, so `instanceof` against it throws \u2014 and it would throw inside\n // `markOrmEntityChange`, outside the best-effort try/catch that guards the flush, turning\n // every write on such a route into a 500.\n if (typeof declared !== 'function') return false\n return entity instanceof declared\n }\n\n private resolveDefaultIndexer(entity: unknown): CrudIndexerConfig<unknown> | undefined {\n return this.matchesDefaultEntityClass(entity) ? this.defaultIndexer?.indexer : undefined\n }\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 }, {\n persistent: true,\n tenantId,\n organizationId,\n emitterModuleId: mod,\n })\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 emitterModuleId: events.module,\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 // A command handler that marks `events:` only still discharges the route's declared\n // query-index obligation \u2014 the route hands its `indexer:` down as the default so the\n // handler's own entity and identifiers (the accurate ones) drive the projection write.\n const indexer = opts.indexer ?? this.resolveDefaultIndexer(entity)\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 // Explicit always wins, on the merge branch too: a second `events:`-only mark on the same\n // key must not let the route default overwrite the `indexer:` an earlier mark installed,\n // which would silently drop that handler's own `buildUpsertPayload`.\n if (opts.indexer) existing.indexer = opts.indexer as CrudIndexerConfig<unknown>\n else if (!existing.indexer && indexer) existing.indexer = indexer\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 (indexer) entry.indexer = indexer\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 if (entry.indexer && !suppress?.skipReindex && this.matchesDefaultEntityClass(entry.entity)) {\n this.indexedDefaultEntityClass = true\n }\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
|
}
|
package/dist/lib/email/config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parseBooleanWithDefault } from "../boolean.js";
|
|
2
|
+
import { getRegisteredEmailTransport } from "./transport.js";
|
|
1
3
|
function normalizeEnvString(value) {
|
|
2
4
|
if (typeof value !== "string") return void 0;
|
|
3
5
|
const trimmed = value.trim();
|
|
@@ -6,7 +8,25 @@ function normalizeEnvString(value) {
|
|
|
6
8
|
function resolveDefaultEmailFromAddress() {
|
|
7
9
|
return normalizeEnvString(process.env.NOTIFICATIONS_EMAIL_FROM) || normalizeEnvString(process.env.EMAIL_FROM) || normalizeEnvString(process.env.ADMIN_EMAIL);
|
|
8
10
|
}
|
|
11
|
+
function isEmailDeliveryDisabled() {
|
|
12
|
+
const explicitlyDisabled = parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false);
|
|
13
|
+
if (explicitlyDisabled) return true;
|
|
14
|
+
const testMode = parseBooleanWithDefault(process.env.OM_TEST_MODE, false);
|
|
15
|
+
if (!testMode) return false;
|
|
16
|
+
const testCaptureDeliveryEnabled = process.env.SYSTEM_EMAIL_PROVIDER === "__test_seed__" && parseBooleanWithDefault(process.env.OM_ENABLE_TEST_CHANNEL_SEEDING, false) && parseBooleanWithDefault(process.env.OM_ENABLE_TEST_EMAIL_CAPTURE_DELIVERY, false);
|
|
17
|
+
return !testCaptureDeliveryEnabled;
|
|
18
|
+
}
|
|
19
|
+
function isEmailDeliveryConfigured() {
|
|
20
|
+
if (isEmailDeliveryDisabled()) return false;
|
|
21
|
+
if (!resolveDefaultEmailFromAddress()) return false;
|
|
22
|
+
const transport = getRegisteredEmailTransport();
|
|
23
|
+
if (!transport) return false;
|
|
24
|
+
return transport.isConfigured ? transport.isConfigured() : true;
|
|
25
|
+
}
|
|
9
26
|
export {
|
|
27
|
+
isEmailDeliveryConfigured,
|
|
28
|
+
isEmailDeliveryDisabled,
|
|
29
|
+
normalizeEnvString,
|
|
10
30
|
resolveDefaultEmailFromAddress
|
|
11
31
|
};
|
|
12
32
|
//# sourceMappingURL=config.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/email/config.ts"],
|
|
4
|
-
"sourcesContent": ["function normalizeEnvString(value: string | null | undefined): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport function resolveDefaultEmailFromAddress(): string | undefined {\n return (\n normalizeEnvString(process.env.NOTIFICATIONS_EMAIL_FROM) ||\n normalizeEnvString(process.env.EMAIL_FROM) ||\n normalizeEnvString(process.env.ADMIN_EMAIL)\n )\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,mBAAmB,OAAsD;
|
|
4
|
+
"sourcesContent": ["import { parseBooleanWithDefault } from '../boolean'\nimport { getRegisteredEmailTransport } from './transport'\n\nexport function normalizeEnvString(value: string | null | undefined): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport function resolveDefaultEmailFromAddress(): string | undefined {\n return (\n normalizeEnvString(process.env.NOTIFICATIONS_EMAIL_FROM) ||\n normalizeEnvString(process.env.EMAIL_FROM) ||\n normalizeEnvString(process.env.ADMIN_EMAIL)\n )\n}\n\nexport function isEmailDeliveryDisabled(): boolean {\n const explicitlyDisabled = parseBooleanWithDefault(process.env.OM_DISABLE_EMAIL_DELIVERY, false)\n if (explicitlyDisabled) return true\n\n const testMode = parseBooleanWithDefault(process.env.OM_TEST_MODE, false)\n if (!testMode) return false\n\n const testCaptureDeliveryEnabled =\n process.env.SYSTEM_EMAIL_PROVIDER === '__test_seed__'\n && parseBooleanWithDefault(process.env.OM_ENABLE_TEST_CHANNEL_SEEDING, false)\n && parseBooleanWithDefault(process.env.OM_ENABLE_TEST_EMAIL_CAPTURE_DELIVERY, false)\n return !testCaptureDeliveryEnabled\n}\n\nexport function isEmailDeliveryConfigured(): boolean {\n if (isEmailDeliveryDisabled()) return false\n if (!resolveDefaultEmailFromAddress()) return false\n const transport = getRegisteredEmailTransport()\n if (!transport) return false\n return transport.isConfigured ? transport.isConfigured() : true\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,+BAA+B;AACxC,SAAS,mCAAmC;AAErC,SAAS,mBAAmB,OAAsD;AACvF,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAEO,SAAS,iCAAqD;AACnE,SACE,mBAAmB,QAAQ,IAAI,wBAAwB,KACvD,mBAAmB,QAAQ,IAAI,UAAU,KACzC,mBAAmB,QAAQ,IAAI,WAAW;AAE9C;AAEO,SAAS,0BAAmC;AACjD,QAAM,qBAAqB,wBAAwB,QAAQ,IAAI,2BAA2B,KAAK;AAC/F,MAAI,mBAAoB,QAAO;AAE/B,QAAM,WAAW,wBAAwB,QAAQ,IAAI,cAAc,KAAK;AACxE,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,6BACJ,QAAQ,IAAI,0BAA0B,mBACnC,wBAAwB,QAAQ,IAAI,gCAAgC,KAAK,KACzE,wBAAwB,QAAQ,IAAI,uCAAuC,KAAK;AACrF,SAAO,CAAC;AACV;AAEO,SAAS,4BAAqC;AACnD,MAAI,wBAAwB,EAAG,QAAO;AACtC,MAAI,CAAC,+BAA+B,EAAG,QAAO;AAC9C,QAAM,YAAY,4BAA4B;AAC9C,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,UAAU,eAAe,UAAU,aAAa,IAAI;AAC7D;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/email/send.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { Resend } from "resend";
|
|
2
1
|
import React from "react";
|
|
3
2
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
5
|
import { parseBooleanWithDefault } from "../boolean.js";
|
|
7
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isEmailDeliveryDisabled,
|
|
8
|
+
resolveDefaultEmailFromAddress
|
|
9
|
+
} from "./config.js";
|
|
10
|
+
import { getRegisteredEmailTransport } from "./transport.js";
|
|
8
11
|
const DEFAULT_TEST_EMAIL_CAPTURE_PATH = join(tmpdir(), "open-mercato-email-capture.jsonl");
|
|
9
12
|
function resolveTestEmailCapturePath() {
|
|
10
13
|
return process.env.OM_TEST_EMAIL_CAPTURE_PATH?.trim() || DEFAULT_TEST_EMAIL_CAPTURE_PATH;
|
|
@@ -56,30 +59,30 @@ async function captureEmailForTests(options) {
|
|
|
56
59
|
await appendFile(capturePath, `${JSON.stringify(record)}
|
|
57
60
|
`, "utf8");
|
|
58
61
|
}
|
|
59
|
-
async function sendEmail(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const apiKey = process.env.RESEND_API_KEY;
|
|
64
|
-
if (!apiKey) throw new Error("RESEND_API_KEY is not set");
|
|
65
|
-
const resend = new Resend(apiKey);
|
|
66
|
-
const fromAddr = from || resolveDefaultEmailFromAddress();
|
|
62
|
+
async function sendEmail(options) {
|
|
63
|
+
await captureEmailForTests(options);
|
|
64
|
+
if (isEmailDeliveryDisabled()) return;
|
|
65
|
+
const fromAddr = options.from || resolveDefaultEmailFromAddress();
|
|
67
66
|
if (!fromAddr) {
|
|
68
67
|
throw new Error("EMAIL_FROM_NOT_CONFIGURED: set NOTIFICATIONS_EMAIL_FROM, EMAIL_FROM, or ADMIN_EMAIL");
|
|
69
68
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
from: fromAddr,
|
|
74
|
-
react,
|
|
75
|
-
...replyTo ? { reply_to: replyTo } : {},
|
|
76
|
-
...attachments?.length ? { attachments } : {}
|
|
77
|
-
};
|
|
78
|
-
const result = await resend.emails.send(payload);
|
|
79
|
-
const errorMessage = typeof result?.error === "string" ? result.error : typeof result?.error?.message === "string" ? result.error.message : null;
|
|
80
|
-
if (errorMessage) {
|
|
81
|
-
throw new Error(`RESEND_SEND_FAILED: ${errorMessage}`);
|
|
69
|
+
const transport = getRegisteredEmailTransport();
|
|
70
|
+
if (!transport) {
|
|
71
|
+
throw new Error("EMAIL_TRANSPORT_NOT_CONFIGURED: enable an outbound email provider module");
|
|
82
72
|
}
|
|
73
|
+
await transport.send({
|
|
74
|
+
to: options.to,
|
|
75
|
+
subject: options.subject,
|
|
76
|
+
react: options.react,
|
|
77
|
+
html: options.html,
|
|
78
|
+
text: options.text,
|
|
79
|
+
from: fromAddr,
|
|
80
|
+
fromIsInstanceDefault: !options.from,
|
|
81
|
+
replyTo: options.replyTo,
|
|
82
|
+
attachments: options.attachments,
|
|
83
|
+
tenantId: options.tenantId,
|
|
84
|
+
organizationId: options.organizationId
|
|
85
|
+
});
|
|
83
86
|
}
|
|
84
87
|
export {
|
|
85
88
|
sendEmail
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/email/send.ts"],
|
|
4
|
-
"sourcesContent": ["import
|
|
5
|
-
"mappings": "AAAA,
|
|
4
|
+
"sourcesContent": ["import React from 'react'\nimport { appendFile, mkdir } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { parseBooleanWithDefault } from '../boolean'\nimport {\n isEmailDeliveryDisabled,\n resolveDefaultEmailFromAddress,\n} from './config'\nimport { getRegisteredEmailTransport } from './transport'\n\nexport type EmailAttachment = {\n filename: string\n content: string\n contentType?: string\n}\n\nexport type SendEmailOptions = {\n to: string\n subject: string\n react?: React.ReactElement\n html?: string\n text?: string\n from?: string\n replyTo?: string\n attachments?: EmailAttachment[]\n tenantId?: string\n organizationId?: string | null\n}\n\nexport type ResolvedEmailPayload = {\n to: string\n subject: string\n react?: React.ReactElement\n html?: string\n text?: string\n from: string\n /**\n * True when `from` was filled in from the instance-wide environment defaults rather than chosen by\n * the caller. Transports use this to decide whether a tenant's own configured sender may take\n * precedence: `from` is never empty by the time it reaches a transport, so without this flag a\n * per-tenant sender is unreachable. Absent means \"caller chose it\" for older transports.\n */\n fromIsInstanceDefault?: boolean\n replyTo?: string\n attachments?: EmailAttachment[]\n tenantId?: string\n organizationId?: string | null\n}\n\ntype CapturedEmail = {\n to: string\n subject: string\n from: string | null\n replyTo: string | null\n links: string[]\n text: string\n capturedAt: string\n}\n\ntype ReactElementProps = {\n href?: unknown\n children?: unknown\n}\n\nconst DEFAULT_TEST_EMAIL_CAPTURE_PATH = join(tmpdir(), 'open-mercato-email-capture.jsonl')\n\nfunction resolveTestEmailCapturePath(): string {\n return process.env.OM_TEST_EMAIL_CAPTURE_PATH?.trim() || DEFAULT_TEST_EMAIL_CAPTURE_PATH\n}\n\nfunction readElementProps(node: React.ReactElement): ReactElementProps {\n return node.props as ReactElementProps\n}\n\nfunction collectEmailLinks(node: unknown, links: string[] = []): string[] {\n if (node == null || typeof node === 'boolean') return links\n if (Array.isArray(node)) {\n for (const child of node) collectEmailLinks(child, links)\n return links\n }\n if (React.isValidElement(node)) {\n const props = readElementProps(node)\n if (typeof props.href === 'string' && props.href.length > 0) links.push(props.href)\n collectEmailLinks(props.children, links)\n }\n return links\n}\n\nfunction collectEmailText(node: unknown, parts: string[] = []): string[] {\n if (node == null || typeof node === 'boolean') return parts\n if (typeof node === 'string' || typeof node === 'number') {\n parts.push(String(node))\n return parts\n }\n if (Array.isArray(node)) {\n for (const child of node) collectEmailText(child, parts)\n return parts\n }\n if (React.isValidElement(node)) {\n collectEmailText(readElementProps(node).children, parts)\n }\n return parts\n}\n\nasync function captureEmailForTests(options: SendEmailOptions): Promise<void> {\n if (!parseBooleanWithDefault(process.env.OM_TEST_MODE, false)) return\n\n const capturePath = resolveTestEmailCapturePath()\n const record: CapturedEmail = {\n to: options.to,\n subject: options.subject,\n from: options.from ?? resolveDefaultEmailFromAddress() ?? null,\n replyTo: options.replyTo ?? null,\n links: collectEmailLinks(options.react),\n text: collectEmailText(options.react).join(' ').replace(/\\s+/g, ' ').trim(),\n capturedAt: new Date().toISOString(),\n }\n\n await mkdir(dirname(capturePath), { recursive: true })\n await appendFile(capturePath, `${JSON.stringify(record)}\\n`, 'utf8')\n}\n\nexport async function sendEmail(options: SendEmailOptions): Promise<void> {\n await captureEmailForTests(options)\n if (isEmailDeliveryDisabled()) return\n\n const fromAddr = options.from || resolveDefaultEmailFromAddress()\n if (!fromAddr) {\n throw new Error('EMAIL_FROM_NOT_CONFIGURED: set NOTIFICATIONS_EMAIL_FROM, EMAIL_FROM, or ADMIN_EMAIL')\n }\n\n const transport = getRegisteredEmailTransport()\n if (!transport) {\n throw new Error('EMAIL_TRANSPORT_NOT_CONFIGURED: enable an outbound email provider module')\n }\n\n await transport.send({\n to: options.to,\n subject: options.subject,\n react: options.react,\n html: options.html,\n text: options.text,\n from: fromAddr,\n fromIsInstanceDefault: !options.from,\n replyTo: options.replyTo,\n attachments: options.attachments,\n tenantId: options.tenantId,\n organizationId: options.organizationId,\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const EMAIL_TRANSPORT_REGISTRY = /* @__PURE__ */ Symbol.for("open-mercato.email.transport");
|
|
2
|
+
function emailTransportRoot() {
|
|
3
|
+
return globalThis;
|
|
4
|
+
}
|
|
5
|
+
function registerEmailTransport(transport) {
|
|
6
|
+
emailTransportRoot()[EMAIL_TRANSPORT_REGISTRY] = transport;
|
|
7
|
+
}
|
|
8
|
+
function getRegisteredEmailTransport() {
|
|
9
|
+
return emailTransportRoot()[EMAIL_TRANSPORT_REGISTRY] ?? null;
|
|
10
|
+
}
|
|
11
|
+
function clearRegisteredEmailTransportForTests() {
|
|
12
|
+
emailTransportRoot()[EMAIL_TRANSPORT_REGISTRY] = null;
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
clearRegisteredEmailTransportForTests,
|
|
16
|
+
getRegisteredEmailTransport,
|
|
17
|
+
registerEmailTransport
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/email/transport.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ResolvedEmailPayload } from './send'\n\nexport type EmailTransport = {\n id: string\n send: (payload: ResolvedEmailPayload) => Promise<void>\n isConfigured?: () => boolean\n}\n\nconst EMAIL_TRANSPORT_REGISTRY = Symbol.for('open-mercato.email.transport')\n\ntype EmailTransportRegistryGlobal = typeof globalThis & {\n [EMAIL_TRANSPORT_REGISTRY]?: EmailTransport | null\n}\n\nfunction emailTransportRoot(): EmailTransportRegistryGlobal {\n return globalThis as EmailTransportRegistryGlobal\n}\n\nexport function registerEmailTransport(transport: EmailTransport): void {\n emailTransportRoot()[EMAIL_TRANSPORT_REGISTRY] = transport\n}\n\nexport function getRegisteredEmailTransport(): EmailTransport | null {\n return emailTransportRoot()[EMAIL_TRANSPORT_REGISTRY] ?? null\n}\n\nexport function clearRegisteredEmailTransportForTests(): void {\n emailTransportRoot()[EMAIL_TRANSPORT_REGISTRY] = null\n}\n"],
|
|
5
|
+
"mappings": "AAQA,MAAM,2BAA2B,uBAAO,IAAI,8BAA8B;AAM1E,SAAS,qBAAmD;AAC1D,SAAO;AACT;AAEO,SAAS,uBAAuB,WAAiC;AACtE,qBAAmB,EAAE,wBAAwB,IAAI;AACnD;AAEO,SAAS,8BAAqD;AACnE,SAAO,mBAAmB,EAAE,wBAAwB,KAAK;AAC3D;AAEO,SAAS,wCAA8C;AAC5D,qBAAmB,EAAE,wBAAwB,IAAI;AACnD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -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.7.1-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.7.1-develop.7151.1.00d0391847';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7151.1.00d0391847",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"@mikro-orm/core": "^7.1.8",
|
|
114
114
|
"@mikro-orm/decorators": "^7.1.8",
|
|
115
115
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
116
|
-
"@open-mercato/cache": "0.7.1-develop.
|
|
116
|
+
"@open-mercato/cache": "0.7.1-develop.7151.1.00d0391847",
|
|
117
117
|
"@types/html-to-text": "^9.0.4",
|
|
118
118
|
"@types/sanitize-html": "^2.16.1",
|
|
119
119
|
"dotenv": "^17.4.2",
|