@open-mercato/shared 0.6.5-develop.5382.1.f542de69af → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/data/engine.js +25 -1
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/mikro.js +38 -9
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/encryption/kms.js +41 -6
- package/dist/lib/encryption/kms.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +4 -5
- package/src/lib/data/__tests__/engine.custom-entity-storage-guard.test.ts +78 -0
- package/src/lib/data/engine.ts +40 -0
- package/src/lib/db/__tests__/mikro.test.ts +82 -0
- package/src/lib/db/mikro.ts +55 -16
- package/src/lib/encryption/__tests__/kms.test.ts +80 -0
- package/src/lib/encryption/kms.ts +55 -7
- package/src/lib/query/types.ts +10 -0
- package/src/modules/navigation/backendChrome.ts +9 -0
package/dist/lib/data/engine.js
CHANGED
|
@@ -3,6 +3,8 @@ import { setRecordCustomFields } from "@open-mercato/core/modules/entities/lib/h
|
|
|
3
3
|
import { validateCustomFieldValuesServer } from "@open-mercato/core/modules/entities/lib/validation";
|
|
4
4
|
import { sanitizeCustomFieldHtmlRichTextValuesServer } from "@open-mercato/core/modules/entities/lib/htmlRichTextSanitizer";
|
|
5
5
|
import { CrudHttpError } from "../crud/errors.js";
|
|
6
|
+
import { resolveRegisteredEntityTableName } from "../query/engine.js";
|
|
7
|
+
import { getEntityIds } from "../encryption/entityIds.js";
|
|
6
8
|
import { normalizeCustomFieldValues } from "../custom-fields/normalize.js";
|
|
7
9
|
import { parseBooleanToken } from "../boolean.js";
|
|
8
10
|
import { isEventDeclared } from "../../modules/events/index.js";
|
|
@@ -29,6 +31,22 @@ function shouldTriggerCoverageRefresh(entityType, tenantId) {
|
|
|
29
31
|
coverageRefreshTracker.set(key, now);
|
|
30
32
|
return true;
|
|
31
33
|
}
|
|
34
|
+
const SYSTEM_ENTITY_RECORDS_BLOCKED_CODE = "system_entity_records_blocked";
|
|
35
|
+
function isOrmBackedSystemEntityId(em, entityId) {
|
|
36
|
+
const registry = getEntityIds(false);
|
|
37
|
+
const moduleIds = Object.values(registry).flatMap((moduleEntities) => Object.values(moduleEntities ?? {}));
|
|
38
|
+
if (moduleIds.length > 0 && !moduleIds.includes(entityId)) return false;
|
|
39
|
+
return resolveRegisteredEntityTableName(em, entityId) !== null;
|
|
40
|
+
}
|
|
41
|
+
function assertCustomEntityStorageEntityId(em, entityId) {
|
|
42
|
+
if (isOrmBackedSystemEntityId(em, entityId)) {
|
|
43
|
+
throw new CrudHttpError(400, {
|
|
44
|
+
error: "Records are available for custom entities only",
|
|
45
|
+
code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE,
|
|
46
|
+
entityId
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
32
50
|
class DefaultDataEngine {
|
|
33
51
|
constructor(em, container) {
|
|
34
52
|
this.em = em;
|
|
@@ -132,6 +150,7 @@ class DefaultDataEngine {
|
|
|
132
150
|
}
|
|
133
151
|
}
|
|
134
152
|
async createCustomEntityRecord(opts) {
|
|
153
|
+
assertCustomEntityStorageEntityId(this.em, opts.entityId);
|
|
135
154
|
const db = this.getKysely();
|
|
136
155
|
await this.ensureStorageTableExists();
|
|
137
156
|
const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {
|
|
@@ -202,6 +221,7 @@ class DefaultDataEngine {
|
|
|
202
221
|
return { id };
|
|
203
222
|
}
|
|
204
223
|
async updateCustomEntityRecord(opts) {
|
|
224
|
+
assertCustomEntityStorageEntityId(this.em, opts.entityId);
|
|
205
225
|
const db = this.getKysely();
|
|
206
226
|
const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {
|
|
207
227
|
entityId: opts.entityId,
|
|
@@ -261,6 +281,7 @@ class DefaultDataEngine {
|
|
|
261
281
|
}
|
|
262
282
|
}
|
|
263
283
|
async deleteCustomEntityRecord(opts) {
|
|
284
|
+
assertCustomEntityStorageEntityId(this.em, opts.entityId);
|
|
264
285
|
const db = this.getKysely();
|
|
265
286
|
const id = String(opts.recordId);
|
|
266
287
|
const orgId = opts.organizationId ?? null;
|
|
@@ -477,6 +498,9 @@ class DefaultDataEngine {
|
|
|
477
498
|
}
|
|
478
499
|
export {
|
|
479
500
|
DefaultDataEngine,
|
|
480
|
-
|
|
501
|
+
SYSTEM_ENTITY_RECORDS_BLOCKED_CODE,
|
|
502
|
+
__resetUndeclaredEventWarningsForTests,
|
|
503
|
+
assertCustomEntityStorageEntityId,
|
|
504
|
+
isOrmBackedSystemEntityId
|
|
481
505
|
};
|
|
482
506
|
//# sourceMappingURL=engine.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/data/engine.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityData, EntityName, FilterQuery, RequiredEntityData } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { type Kysely, sql } from 'kysely'\nimport { setRecordCustomFields } from '@open-mercato/core/modules/entities/lib/helpers'\nimport { validateCustomFieldValuesServer } from '@open-mercato/core/modules/entities/lib/validation'\nimport { sanitizeCustomFieldHtmlRichTextValuesServer } from '@open-mercato/core/modules/entities/lib/htmlRichTextSanitizer'\nimport type { EventBus } from '@open-mercato/events/types'\nimport type {\n CrudEventAction,\n CrudEventsConfig,\n CrudIndexerConfig,\n CrudEntityIdentifiers,\n} from '../crud/types'\nimport { CrudHttpError } from '../crud/errors'\nimport { normalizeCustomFieldValues } from '../custom-fields/normalize'\nimport { parseBooleanToken } from '../boolean'\nimport { isEventDeclared } from '../../modules/events'\n\nconst undeclaredEventWarned = new Set<string>()\n\nfunction warnIfUndeclaredEvent(eventName: string, context: string): void {\n if (isEventDeclared(eventName)) return\n if (undeclaredEventWarned.has(eventName)) return\n undeclaredEventWarned.add(eventName)\n console.warn(\n `[data-engine] ${context} is emitting undeclared event \"${eventName}\". ` +\n `Declare it in the owning module's events.ts (createModuleEvents) so the event registry stays authoritative.`,\n )\n}\n\n/** Internal: clear the undeclared-event warning cache. Exposed for tests. */\nexport function __resetUndeclaredEventWarningsForTests(): void {\n undeclaredEventWarned.clear()\n}\n\nconst COVERAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000\nconst coverageRefreshTracker = new Map<string, number>()\n\nfunction shouldTriggerCoverageRefresh(entityType: string | undefined, tenantId: string | null): boolean {\n if (!entityType) return false\n const key = `${entityType}|${tenantId ?? '__null__'}`\n const now = Date.now()\n const last = coverageRefreshTracker.get(key) ?? 0\n if (now - last < COVERAGE_REFRESH_INTERVAL_MS) return false\n coverageRefreshTracker.set(key, now)\n return true\n}\n\ntype CustomEntityValues = Record<string, unknown>\n\ntype QueuedCrudSideEffect = {\n action: CrudEventAction\n entity: unknown\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n events?: CrudEventsConfig<unknown>\n indexer?: CrudIndexerConfig<unknown>\n}\n\nexport interface DataEngine {\n setCustomFields(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, string | number | boolean | null | undefined | Array<string | number | boolean | null | undefined>>\n notify?: boolean // default true -> emit '<module>.<entity>.updated'\n }): Promise<void>\n\n // Storage for user-defined entities (doc-based)\n createCustomEntityRecord(opts: {\n entityId: string // '<module>:<entity>'\n recordId?: string // optional; auto-generate if not provided\n organizationId?: string | null\n tenantId?: string | null\n values: CustomEntityValues\n notify?: boolean // keep event emitting as it is via setCustomFields (updated)\n }): Promise<{ id: string }>\n\n updateCustomEntityRecord(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: CustomEntityValues\n notify?: boolean // keep event emitting as it is via setCustomFields (updated)\n }): Promise<void>\n\n deleteCustomEntityRecord(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n soft?: boolean // default true: sets deleted_at\n notify?: boolean // keep event emitting as it is (no extra events here)\n }): Promise<void>\n\n // Generic ORM-backed entity operations used by CrudFactory\n createOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n data: EntityData<T>\n }): Promise<T>\n\n updateOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n apply: (current: T) => Promise<void> | void\n }): Promise<T | null>\n\n deleteOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n soft?: boolean\n softDeleteField?: keyof T & string\n }): Promise<T | null>\n\n emitOrmEntityEvent<T>(opts: {\n action: CrudEventAction\n entity: T\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): Promise<void>\n\n markOrmEntityChange<T>(opts: {\n action: CrudEventAction\n entity: T | null | undefined\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): void\n\n flushOrmEntityChanges(): Promise<void>\n}\n\nexport class DefaultDataEngine implements DataEngine {\n private pendingSideEffects = new Map<string, QueuedCrudSideEffect>()\n constructor(private em: EntityManager, private container: AwilixContainer) {}\n\n async setCustomFields(opts: Parameters<DataEngine['setCustomFields']>[0]): Promise<void> {\n const { entityId, recordId, organizationId = null, tenantId = null, values } = opts\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId,\n organizationId,\n tenantId,\n values,\n })\n await this.validateCustomFieldValues(entityId, organizationId, tenantId, sanitizedValues as Record<string, unknown>)\n let encryptionService: any = null\n try {\n encryptionService = this.container.resolve('tenantEncryptionService') as any\n } catch {\n encryptionService = null\n }\n await setRecordCustomFields(this.em, {\n entityId,\n recordId,\n organizationId,\n tenantId,\n values: sanitizedValues,\n encryptionService,\n })\n if (opts.notify !== false) {\n let bus: EventBus | null = null\n try {\n bus = (this.container.resolve('eventBus') as EventBus)\n } catch {\n bus = null\n }\n if (bus) {\n const [mod, ent] = (entityId || '').split(':')\n if (mod && ent) {\n const eventName = `${mod}.${ent}.updated`\n warnIfUndeclaredEvent(eventName, 'setCustomFields')\n try {\n await bus.emitEvent(eventName, { id: recordId, organizationId, tenantId }, { persistent: true })\n } catch {\n // non-blocking\n }\n }\n }\n }\n }\n\n private normalizeDocValues(values: CustomEntityValues): CustomEntityValues {\n const out: CustomEntityValues = {}\n for (const [k, v] of Object.entries(values || {})) {\n // Never allow callers to override reserved identifiers in the doc\n if (k === 'id' || k === 'entity_id' || k === 'entityId') continue\n // Accept both 'cf_<key>' and 'cf:<key>' inputs and normalize to 'cf:<key>'\n if (k.startsWith('cf_')) out[`cf:${k.slice(3)}`] = v\n else out[k] = v\n }\n return out\n }\n\n private backcompatEavEnabled(): boolean {\n try {\n return parseBooleanToken(process.env.ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM ?? '') === true\n } catch { return false }\n }\n\n private getKysely(): Kysely<any> {\n return this.em.getKysely<any>()\n }\n\n private async ensureStorageTableExists(): Promise<void> {\n const db = this.getKysely()\n const exists = await db\n .selectFrom('information_schema.tables' as any)\n .select(sql`1`.as('present'))\n .where('table_name' as any, '=', 'custom_entities_storage')\n .executeTakeFirst()\n if (!exists) {\n throw new Error('custom_entities_storage table is missing. Run migrations (yarn db:migrate).')\n }\n }\n\n private normalizeValuesForValidation(values: Record<string, unknown> | undefined | null): Record<string, unknown> {\n if (!values) return {}\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (value === undefined) continue\n if (key.startsWith('cf_') || key.startsWith('cf:')) {\n const normalized = key.slice(3)\n if (normalized) out[normalized] = value\n continue\n }\n out[key] = value\n }\n return out\n }\n\n private async validateCustomFieldValues(\n entityId: string,\n organizationId: string | null,\n tenantId: string | null,\n values: Record<string, unknown> | undefined | null,\n ): Promise<void> {\n const prepared = this.normalizeValuesForValidation(values)\n if (!entityId || Object.keys(prepared).length === 0) return\n const result = await validateCustomFieldValuesServer(this.em, {\n entityId,\n organizationId,\n tenantId,\n values: prepared,\n })\n if (!result.ok) {\n throw new CrudHttpError(400, { error: 'Validation failed', fields: result.fieldErrors })\n }\n }\n\n async createCustomEntityRecord(opts: Parameters<DataEngine['createCustomEntityRecord']>[0]): Promise<{ id: string }> {\n const db = this.getKysely()\n await this.ensureStorageTableExists()\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId: opts.entityId,\n organizationId: opts.organizationId ?? null,\n tenantId: opts.tenantId ?? null,\n values: opts.values || {},\n })\n await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues)\n const rawId = String(opts.recordId ?? '').trim()\n const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(rawId)\n const sentinel = rawId.toLowerCase()\n const shouldGenerate = !rawId || !isUuid || sentinel === 'create' || sentinel === 'new' || sentinel === 'null' || sentinel === 'undefined'\n const id = shouldGenerate ? ((): string => {\n const g = globalThis as { crypto?: { randomUUID?: () => string } }\n if (g.crypto?.randomUUID) return g.crypto.randomUUID()\n // Fallback UUIDv4 generator\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === 'x' ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n })() : rawId\n const orgId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const doc: Record<string, unknown> = { id, ...this.normalizeDocValues(sanitizedValues || {}) }\n\n const now = sql`now()`\n const payload = {\n entity_type: opts.entityId,\n entity_id: id,\n organization_id: orgId,\n tenant_id: tenantId,\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: now,\n created_at: now,\n deleted_at: null,\n }\n\n // Upsert by scoped uniqueness\n try {\n await db\n .insertInto('custom_entities_storage' as any)\n .values(payload as any)\n .onConflict((oc) => oc\n .columns(['entity_type', 'entity_id', 'organization_id'])\n .doUpdateSet({\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any))\n .execute()\n } catch {\n // Fallback for global scope uniqueness\n try {\n const updated = await db\n .updateTable('custom_entities_storage' as any)\n .set({\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any)\n .where('entity_type' as any, '=', opts.entityId)\n .where('entity_id' as any, '=', id)\n .where('organization_id' as any, orgId === null ? 'is' : '=', orgId as any)\n .executeTakeFirst()\n if (!updated || Number(updated.numUpdatedRows ?? 0) === 0) {\n await db.insertInto('custom_entities_storage' as any).values(payload as any).execute()\n }\n } catch (err) {\n // Surface a clear error so it doesn't silently fall back only to EAV\n throw err\n }\n }\n\n // Optional EAV backward compatibility (disabled by default)\n if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {\n await this.setCustomFields({\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: tenantId,\n values: normalizeCustomFieldValues(sanitizedValues),\n notify: opts.notify, // defaults to true\n })\n }\n\n return { id }\n }\n\n async updateCustomEntityRecord(opts: Parameters<DataEngine['updateCustomEntityRecord']>[0]): Promise<void> {\n const db = this.getKysely()\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId: opts.entityId,\n organizationId: opts.organizationId ?? null,\n tenantId: opts.tenantId ?? null,\n values: opts.values || {},\n })\n await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues)\n const id = String(opts.recordId)\n const orgId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n\n // Merge doc shallowly: load existing doc and overlay\n await this.ensureStorageTableExists()\n const applyScope = <T extends { where: (col: any, op: any, val?: any) => T }>(q: T) => {\n let chain = q.where('entity_type' as any, '=', opts.entityId)\n chain = chain.where('entity_id' as any, '=', id)\n chain = orgId === null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', orgId)\n return chain\n }\n const row = await applyScope(\n db.selectFrom('custom_entities_storage' as any).select(['doc' as any])\n ).executeTakeFirst()\n const prevDoc: Record<string, unknown> = (row as any)?.doc || { id }\n const nextDoc: Record<string, unknown> = { ...prevDoc, ...this.normalizeDocValues(sanitizedValues || {}), id }\n try {\n const updated = await applyScope(\n db.updateTable('custom_entities_storage' as any).set({\n doc: sql`${JSON.stringify(nextDoc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any) as any\n ).executeTakeFirst()\n if (!updated || Number((updated as any).numUpdatedRows ?? 0) === 0) {\n await db.insertInto('custom_entities_storage' as any).values({\n entity_type: opts.entityId,\n entity_id: id,\n organization_id: orgId,\n tenant_id: tenantId,\n doc: sql`${JSON.stringify(nextDoc)}::jsonb`,\n created_at: sql`now()`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any).execute()\n }\n } catch (err) {\n throw err\n }\n\n // Optional EAV backward compatibility (disabled by default)\n if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {\n await this.setCustomFields({\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: tenantId,\n values: normalizeCustomFieldValues(sanitizedValues),\n notify: opts.notify, // defaults to true\n })\n }\n }\n\n async deleteCustomEntityRecord(opts: Parameters<DataEngine['deleteCustomEntityRecord']>[0]): Promise<void> {\n const db = this.getKysely()\n const id = String(opts.recordId)\n const orgId = opts.organizationId ?? null\n const soft = opts.soft !== false\n\n const applyScope = <T extends { where: (col: any, op: any, val?: any) => T }>(q: T) => {\n let chain = q.where('entity_type' as any, '=', opts.entityId)\n chain = chain.where('entity_id' as any, '=', id)\n chain = orgId === null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', orgId)\n return chain\n }\n\n if (soft) {\n await applyScope(\n db.updateTable('custom_entities_storage' as any).set({\n deleted_at: sql`now()`,\n updated_at: sql`now()`,\n } as any) as any\n ).execute()\n } else {\n await applyScope(db.deleteFrom('custom_entities_storage' as any) as any).execute()\n }\n\n // Soft-delete EAV values to preserve current behavior\n try {\n const { CustomFieldValue } = await import('@open-mercato/core/modules/entities/data/entities')\n const values = await this.em.find(CustomFieldValue, {\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: opts.tenantId ?? null,\n })\n const now = new Date()\n const mutated = values.filter((record) => {\n if (record.deletedAt) return false\n record.deletedAt = now\n return true\n })\n if (mutated.length) {\n for (const record of values) this.em.persist(record)\n await this.em.flush()\n }\n } catch { /* non-blocking */ }\n }\n\n async createOrmEntity<T extends object>(opts: { entity: EntityName<T>; data: EntityData<T> }): Promise<T> {\n const entity = this.em.create(\n opts.entity as EntityName<T>,\n opts.data as unknown as RequiredEntityData<T>\n )\n await this.em.persist(entity).flush()\n return entity\n }\n\n async updateOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n apply: (current: T) => Promise<void> | void\n }): Promise<T | null> {\n const current = await this.em.findOne(opts.entity as EntityName<T>, opts.where as FilterQuery<NoInfer<T>>)\n if (!current) return null\n await opts.apply(current)\n await this.em.persist(current).flush()\n return current\n }\n\n async deleteOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n soft?: boolean\n softDeleteField?: keyof T & string\n }): Promise<T | null> {\n const current = await this.em.findOne(opts.entity as EntityName<T>, opts.where as FilterQuery<NoInfer<T>>)\n if (!current) return null\n if (opts.soft !== false) {\n const field = opts.softDeleteField || ('deletedAt' as keyof T & string)\n if (typeof current === 'object' && current !== null) {\n ;(current as Record<string, unknown>)[field] = new Date()\n await this.em.persist(current).flush()\n }\n } else {\n await this.em.remove(current).flush()\n }\n return current\n }\n\n async emitOrmEntityEvent<T>(opts: {\n action: CrudEventAction\n entity: T\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): Promise<void> {\n const { action, entity, events, indexer, identifiers, syncOrigin } = opts\n if (!events && !indexer) return\n if (!identifiers?.id) return\n\n let bus: EventBus | null = null\n try {\n bus = (this.container.resolve('eventBus') as EventBus)\n } catch {\n bus = null\n }\n if (!bus) return\n\n const ctx = {\n action,\n entity,\n identifiers: {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n },\n syncOrigin: syncOrigin ?? null,\n }\n\n if (events) {\n const eventName = `${events.module}.${events.entity}.${action}`\n warnIfUndeclaredEvent(eventName, 'emitOrmEntityEvent')\n const payload = events.buildPayload\n ? events.buildPayload(ctx)\n : {\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),\n }\n try {\n await bus.emitEvent(eventName, payload, {\n persistent: !!events.persistent,\n tenantId: ctx.identifiers.tenantId ?? null,\n organizationId: ctx.identifiers.organizationId ?? null,\n })\n } catch {\n // non-blocking\n }\n }\n\n if (indexer) {\n const resolveCoverageBaseDelta = (): number | undefined => {\n if (action === 'created') return 1\n if (action === 'deleted') return -1\n return undefined\n }\n const coverageBaseDelta = resolveCoverageBaseDelta()\n\n if (action === 'deleted') {\n const payload = indexer.buildDeletePayload\n ? indexer.buildDeletePayload(ctx)\n : {\n entityType: indexer.entityType,\n recordId: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }\n const enrichedPayload = payload as Record<string, unknown>\n enrichedPayload.crudAction = action\n if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta\n if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin\n // Await the index update so query-index reads (the `customValues`/scalar\n // projection that list endpoints serve) are consistent the moment the write\n // returns. The subscriber removes the projection row + tokens synchronously and\n // defers the coverage recompute + fulltext delete, so this stays bounded.\n // Errors are logged, not thrown \u2014 index drift never fails the originating write.\n await bus.emitEvent('query_index.delete_one', enrichedPayload).catch((err: unknown) => {\n console.error('[data-engine] query_index.delete_one emit failed', err)\n })\n } else {\n const payload = indexer.buildUpsertPayload\n ? indexer.buildUpsertPayload(ctx)\n : {\n entityType: indexer.entityType,\n recordId: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }\n const enrichedPayload = payload as Record<string, unknown>\n enrichedPayload.crudAction = action\n if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta\n if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin\n // Await the projection upsert so list reads observe the new doc immediately\n // (see delete_one above). The subscriber updates `entity_indexes` synchronously\n // and defers the heavy token-reindex pipeline (build doc + encrypt + decrypt +\n // tokenize + DELETE + chunked INSERT) so write latency stays bounded.\n await bus.emitEvent('query_index.upsert_one', enrichedPayload).catch((err: unknown) => {\n console.error('[data-engine] query_index.upsert_one emit failed', err)\n })\n }\n\n if (shouldTriggerCoverageRefresh(indexer.entityType, ctx.identifiers.tenantId ?? null)) {\n void bus.emitEvent('query_index.coverage.refresh', {\n entityType: indexer.entityType,\n tenantId: ctx.identifiers.tenantId ?? null,\n organizationId: null,\n delayMs: 0,\n }).catch(() => undefined)\n }\n }\n }\n\n markOrmEntityChange<T>(opts: {\n action: CrudEventAction\n entity: T | null | undefined\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): void {\n const { entity, identifiers } = opts\n if (!entity) return\n if (!identifiers?.id) return\n const key = this.buildSideEffectKey(opts.action, identifiers)\n const existing = this.pendingSideEffects.get(key)\n if (existing) {\n existing.entity = entity\n existing.identifiers = {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n }\n existing.syncOrigin = opts.syncOrigin ?? null\n if (opts.events) existing.events = opts.events as CrudEventsConfig<unknown>\n if (opts.indexer) existing.indexer = opts.indexer as CrudIndexerConfig<unknown>\n this.pendingSideEffects.set(key, existing)\n return\n }\n const entry: QueuedCrudSideEffect = {\n action: opts.action,\n entity,\n identifiers: {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n },\n syncOrigin: opts.syncOrigin ?? null,\n }\n if (opts.events) entry.events = opts.events as CrudEventsConfig<unknown>\n if (opts.indexer) entry.indexer = opts.indexer as CrudIndexerConfig<unknown>\n this.pendingSideEffects.set(key, entry)\n }\n\n async flushOrmEntityChanges(): Promise<void> {\n if (!this.pendingSideEffects.size) return\n const entries = Array.from(this.pendingSideEffects.values())\n this.pendingSideEffects.clear()\n for (const entry of entries) {\n try {\n await this.emitOrmEntityEvent({\n action: entry.action,\n entity: entry.entity,\n identifiers: entry.identifiers,\n syncOrigin: entry.syncOrigin ?? null,\n events: entry.events as CrudEventsConfig<unknown>,\n indexer: entry.indexer as CrudIndexerConfig<unknown>,\n })\n } catch {\n // best-effort; continue with remaining side effects\n }\n }\n }\n\n private buildSideEffectKey(action: CrudEventAction, identifiers: CrudEntityIdentifiers): string {\n const id = identifiers.id ?? ''\n const org = identifiers.organizationId ?? ''\n const tenant = identifiers.tenantId ?? ''\n return [action, id, org, tenant].join('|')\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityData, EntityName, FilterQuery, RequiredEntityData } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { type Kysely, sql } from 'kysely'\nimport { setRecordCustomFields } from '@open-mercato/core/modules/entities/lib/helpers'\nimport { validateCustomFieldValuesServer } from '@open-mercato/core/modules/entities/lib/validation'\nimport { sanitizeCustomFieldHtmlRichTextValuesServer } from '@open-mercato/core/modules/entities/lib/htmlRichTextSanitizer'\nimport type { EventBus } from '@open-mercato/events/types'\nimport type {\n CrudEventAction,\n CrudEventsConfig,\n CrudIndexerConfig,\n CrudEntityIdentifiers,\n} from '../crud/types'\nimport { CrudHttpError } from '../crud/errors'\nimport { resolveRegisteredEntityTableName } from '../query/engine'\nimport { getEntityIds } from '../encryption/entityIds'\nimport { normalizeCustomFieldValues } from '../custom-fields/normalize'\nimport { parseBooleanToken } from '../boolean'\nimport { isEventDeclared } from '../../modules/events'\n\nconst undeclaredEventWarned = new Set<string>()\n\nfunction warnIfUndeclaredEvent(eventName: string, context: string): void {\n if (isEventDeclared(eventName)) return\n if (undeclaredEventWarned.has(eventName)) return\n undeclaredEventWarned.add(eventName)\n console.warn(\n `[data-engine] ${context} is emitting undeclared event \"${eventName}\". ` +\n `Declare it in the owning module's events.ts (createModuleEvents) so the event registry stays authoritative.`,\n )\n}\n\n/** Internal: clear the undeclared-event warning cache. Exposed for tests. */\nexport function __resetUndeclaredEventWarningsForTests(): void {\n undeclaredEventWarned.clear()\n}\n\nconst COVERAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000\nconst coverageRefreshTracker = new Map<string, number>()\n\nfunction shouldTriggerCoverageRefresh(entityType: string | undefined, tenantId: string | null): boolean {\n if (!entityType) return false\n const key = `${entityType}|${tenantId ?? '__null__'}`\n const now = Date.now()\n const last = coverageRefreshTracker.get(key) ?? 0\n if (now - last < COVERAGE_REFRESH_INTERVAL_MS) return false\n coverageRefreshTracker.set(key, now)\n return true\n}\n\ntype CustomEntityValues = Record<string, unknown>\n\ntype QueuedCrudSideEffect = {\n action: CrudEventAction\n entity: unknown\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n events?: CrudEventsConfig<unknown>\n indexer?: CrudIndexerConfig<unknown>\n}\n\nexport interface DataEngine {\n setCustomFields(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: Record<string, string | number | boolean | null | undefined | Array<string | number | boolean | null | undefined>>\n notify?: boolean // default true -> emit '<module>.<entity>.updated'\n }): Promise<void>\n\n // Storage for user-defined entities (doc-based)\n createCustomEntityRecord(opts: {\n entityId: string // '<module>:<entity>'\n recordId?: string // optional; auto-generate if not provided\n organizationId?: string | null\n tenantId?: string | null\n values: CustomEntityValues\n notify?: boolean // keep event emitting as it is via setCustomFields (updated)\n }): Promise<{ id: string }>\n\n updateCustomEntityRecord(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n values: CustomEntityValues\n notify?: boolean // keep event emitting as it is via setCustomFields (updated)\n }): Promise<void>\n\n deleteCustomEntityRecord(opts: {\n entityId: string\n recordId: string\n organizationId?: string | null\n tenantId?: string | null\n soft?: boolean // default true: sets deleted_at\n notify?: boolean // keep event emitting as it is (no extra events here)\n }): Promise<void>\n\n // Generic ORM-backed entity operations used by CrudFactory\n createOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n data: EntityData<T>\n }): Promise<T>\n\n updateOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n apply: (current: T) => Promise<void> | void\n }): Promise<T | null>\n\n deleteOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n soft?: boolean\n softDeleteField?: keyof T & string\n }): Promise<T | null>\n\n emitOrmEntityEvent<T>(opts: {\n action: CrudEventAction\n entity: T\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): Promise<void>\n\n markOrmEntityChange<T>(opts: {\n action: CrudEventAction\n entity: T | null | undefined\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): void\n\n flushOrmEntityChanges(): Promise<void>\n}\n\nexport const SYSTEM_ENTITY_RECORDS_BLOCKED_CODE = 'system_entity_records_blocked'\n\n/**\n * A system entity for doc-storage purposes is an id that modules declare in the\n * generated entity-id registry AND that resolves to a registered ORM table. Both\n * conditions matter: `resolveRegisteredEntityTableName` matches class-name candidates\n * from the entity segment alone, so a runtime-registered custom entity whose name\n * happens to collide with some ORM class (e.g. `user:todo` vs the example module's\n * `Todo`) must never be classified as system. When the registry is not populated\n * (exotic bootstraps, unit harnesses) the check conservatively falls back to the\n * ORM-table match alone so the #2939 protection never switches off.\n */\nexport function isOrmBackedSystemEntityId(em: EntityManager, entityId: string): boolean {\n const registry = getEntityIds(false)\n const moduleIds = Object.values(registry).flatMap((moduleEntities) => Object.values(moduleEntities ?? {}))\n if (moduleIds.length > 0 && !moduleIds.includes(entityId)) return false\n return resolveRegisteredEntityTableName(em, entityId) !== null\n}\n\n/**\n * Doc storage (`custom_entities_storage`) is for custom entities only. A system\n * entity's records live in its own module tables/APIs \u2014 writing doc rows for it\n * poisons read-path classification (#2939) and must be rejected at the deepest\n * seam so no caller (API, AI tool, workflow) can do it.\n */\nexport function assertCustomEntityStorageEntityId(em: EntityManager, entityId: string): void {\n if (isOrmBackedSystemEntityId(em, entityId)) {\n throw new CrudHttpError(400, {\n error: 'Records are available for custom entities only',\n code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE,\n entityId,\n })\n }\n}\n\nexport class DefaultDataEngine implements DataEngine {\n private pendingSideEffects = new Map<string, QueuedCrudSideEffect>()\n constructor(private em: EntityManager, private container: AwilixContainer) {}\n\n async setCustomFields(opts: Parameters<DataEngine['setCustomFields']>[0]): Promise<void> {\n const { entityId, recordId, organizationId = null, tenantId = null, values } = opts\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId,\n organizationId,\n tenantId,\n values,\n })\n await this.validateCustomFieldValues(entityId, organizationId, tenantId, sanitizedValues as Record<string, unknown>)\n let encryptionService: any = null\n try {\n encryptionService = this.container.resolve('tenantEncryptionService') as any\n } catch {\n encryptionService = null\n }\n await setRecordCustomFields(this.em, {\n entityId,\n recordId,\n organizationId,\n tenantId,\n values: sanitizedValues,\n encryptionService,\n })\n if (opts.notify !== false) {\n let bus: EventBus | null = null\n try {\n bus = (this.container.resolve('eventBus') as EventBus)\n } catch {\n bus = null\n }\n if (bus) {\n const [mod, ent] = (entityId || '').split(':')\n if (mod && ent) {\n const eventName = `${mod}.${ent}.updated`\n warnIfUndeclaredEvent(eventName, 'setCustomFields')\n try {\n await bus.emitEvent(eventName, { id: recordId, organizationId, tenantId }, { persistent: true })\n } catch {\n // non-blocking\n }\n }\n }\n }\n }\n\n private normalizeDocValues(values: CustomEntityValues): CustomEntityValues {\n const out: CustomEntityValues = {}\n for (const [k, v] of Object.entries(values || {})) {\n // Never allow callers to override reserved identifiers in the doc\n if (k === 'id' || k === 'entity_id' || k === 'entityId') continue\n // Accept both 'cf_<key>' and 'cf:<key>' inputs and normalize to 'cf:<key>'\n if (k.startsWith('cf_')) out[`cf:${k.slice(3)}`] = v\n else out[k] = v\n }\n return out\n }\n\n private backcompatEavEnabled(): boolean {\n try {\n return parseBooleanToken(process.env.ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM ?? '') === true\n } catch { return false }\n }\n\n private getKysely(): Kysely<any> {\n return this.em.getKysely<any>()\n }\n\n private async ensureStorageTableExists(): Promise<void> {\n const db = this.getKysely()\n const exists = await db\n .selectFrom('information_schema.tables' as any)\n .select(sql`1`.as('present'))\n .where('table_name' as any, '=', 'custom_entities_storage')\n .executeTakeFirst()\n if (!exists) {\n throw new Error('custom_entities_storage table is missing. Run migrations (yarn db:migrate).')\n }\n }\n\n private normalizeValuesForValidation(values: Record<string, unknown> | undefined | null): Record<string, unknown> {\n if (!values) return {}\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (value === undefined) continue\n if (key.startsWith('cf_') || key.startsWith('cf:')) {\n const normalized = key.slice(3)\n if (normalized) out[normalized] = value\n continue\n }\n out[key] = value\n }\n return out\n }\n\n private async validateCustomFieldValues(\n entityId: string,\n organizationId: string | null,\n tenantId: string | null,\n values: Record<string, unknown> | undefined | null,\n ): Promise<void> {\n const prepared = this.normalizeValuesForValidation(values)\n if (!entityId || Object.keys(prepared).length === 0) return\n const result = await validateCustomFieldValuesServer(this.em, {\n entityId,\n organizationId,\n tenantId,\n values: prepared,\n })\n if (!result.ok) {\n throw new CrudHttpError(400, { error: 'Validation failed', fields: result.fieldErrors })\n }\n }\n\n async createCustomEntityRecord(opts: Parameters<DataEngine['createCustomEntityRecord']>[0]): Promise<{ id: string }> {\n assertCustomEntityStorageEntityId(this.em, opts.entityId)\n const db = this.getKysely()\n await this.ensureStorageTableExists()\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId: opts.entityId,\n organizationId: opts.organizationId ?? null,\n tenantId: opts.tenantId ?? null,\n values: opts.values || {},\n })\n await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues)\n const rawId = String(opts.recordId ?? '').trim()\n const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(rawId)\n const sentinel = rawId.toLowerCase()\n const shouldGenerate = !rawId || !isUuid || sentinel === 'create' || sentinel === 'new' || sentinel === 'null' || sentinel === 'undefined'\n const id = shouldGenerate ? ((): string => {\n const g = globalThis as { crypto?: { randomUUID?: () => string } }\n if (g.crypto?.randomUUID) return g.crypto.randomUUID()\n // Fallback UUIDv4 generator\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0\n const v = c === 'x' ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n })() : rawId\n const orgId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n const doc: Record<string, unknown> = { id, ...this.normalizeDocValues(sanitizedValues || {}) }\n\n const now = sql`now()`\n const payload = {\n entity_type: opts.entityId,\n entity_id: id,\n organization_id: orgId,\n tenant_id: tenantId,\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: now,\n created_at: now,\n deleted_at: null,\n }\n\n // Upsert by scoped uniqueness\n try {\n await db\n .insertInto('custom_entities_storage' as any)\n .values(payload as any)\n .onConflict((oc) => oc\n .columns(['entity_type', 'entity_id', 'organization_id'])\n .doUpdateSet({\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any))\n .execute()\n } catch {\n // Fallback for global scope uniqueness\n try {\n const updated = await db\n .updateTable('custom_entities_storage' as any)\n .set({\n doc: sql`${JSON.stringify(doc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any)\n .where('entity_type' as any, '=', opts.entityId)\n .where('entity_id' as any, '=', id)\n .where('organization_id' as any, orgId === null ? 'is' : '=', orgId as any)\n .executeTakeFirst()\n if (!updated || Number(updated.numUpdatedRows ?? 0) === 0) {\n await db.insertInto('custom_entities_storage' as any).values(payload as any).execute()\n }\n } catch (err) {\n // Surface a clear error so it doesn't silently fall back only to EAV\n throw err\n }\n }\n\n // Optional EAV backward compatibility (disabled by default)\n if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {\n await this.setCustomFields({\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: tenantId,\n values: normalizeCustomFieldValues(sanitizedValues),\n notify: opts.notify, // defaults to true\n })\n }\n\n return { id }\n }\n\n async updateCustomEntityRecord(opts: Parameters<DataEngine['updateCustomEntityRecord']>[0]): Promise<void> {\n assertCustomEntityStorageEntityId(this.em, opts.entityId)\n const db = this.getKysely()\n const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {\n entityId: opts.entityId,\n organizationId: opts.organizationId ?? null,\n tenantId: opts.tenantId ?? null,\n values: opts.values || {},\n })\n await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues)\n const id = String(opts.recordId)\n const orgId = opts.organizationId ?? null\n const tenantId = opts.tenantId ?? null\n\n // Merge doc shallowly: load existing doc and overlay\n await this.ensureStorageTableExists()\n const applyScope = <T extends { where: (col: any, op: any, val?: any) => T }>(q: T) => {\n let chain = q.where('entity_type' as any, '=', opts.entityId)\n chain = chain.where('entity_id' as any, '=', id)\n chain = orgId === null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', orgId)\n return chain\n }\n const row = await applyScope(\n db.selectFrom('custom_entities_storage' as any).select(['doc' as any])\n ).executeTakeFirst()\n const prevDoc: Record<string, unknown> = (row as any)?.doc || { id }\n const nextDoc: Record<string, unknown> = { ...prevDoc, ...this.normalizeDocValues(sanitizedValues || {}), id }\n try {\n const updated = await applyScope(\n db.updateTable('custom_entities_storage' as any).set({\n doc: sql`${JSON.stringify(nextDoc)}::jsonb`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any) as any\n ).executeTakeFirst()\n if (!updated || Number((updated as any).numUpdatedRows ?? 0) === 0) {\n await db.insertInto('custom_entities_storage' as any).values({\n entity_type: opts.entityId,\n entity_id: id,\n organization_id: orgId,\n tenant_id: tenantId,\n doc: sql`${JSON.stringify(nextDoc)}::jsonb`,\n created_at: sql`now()`,\n updated_at: sql`now()`,\n deleted_at: null,\n } as any).execute()\n }\n } catch (err) {\n throw err\n }\n\n // Optional EAV backward compatibility (disabled by default)\n if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {\n await this.setCustomFields({\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: tenantId,\n values: normalizeCustomFieldValues(sanitizedValues),\n notify: opts.notify, // defaults to true\n })\n }\n }\n\n async deleteCustomEntityRecord(opts: Parameters<DataEngine['deleteCustomEntityRecord']>[0]): Promise<void> {\n assertCustomEntityStorageEntityId(this.em, opts.entityId)\n const db = this.getKysely()\n const id = String(opts.recordId)\n const orgId = opts.organizationId ?? null\n const soft = opts.soft !== false\n\n const applyScope = <T extends { where: (col: any, op: any, val?: any) => T }>(q: T) => {\n let chain = q.where('entity_type' as any, '=', opts.entityId)\n chain = chain.where('entity_id' as any, '=', id)\n chain = orgId === null\n ? chain.where('organization_id' as any, 'is', null as any)\n : chain.where('organization_id' as any, '=', orgId)\n return chain\n }\n\n if (soft) {\n await applyScope(\n db.updateTable('custom_entities_storage' as any).set({\n deleted_at: sql`now()`,\n updated_at: sql`now()`,\n } as any) as any\n ).execute()\n } else {\n await applyScope(db.deleteFrom('custom_entities_storage' as any) as any).execute()\n }\n\n // Soft-delete EAV values to preserve current behavior\n try {\n const { CustomFieldValue } = await import('@open-mercato/core/modules/entities/data/entities')\n const values = await this.em.find(CustomFieldValue, {\n entityId: opts.entityId,\n recordId: id,\n organizationId: orgId,\n tenantId: opts.tenantId ?? null,\n })\n const now = new Date()\n const mutated = values.filter((record) => {\n if (record.deletedAt) return false\n record.deletedAt = now\n return true\n })\n if (mutated.length) {\n for (const record of values) this.em.persist(record)\n await this.em.flush()\n }\n } catch { /* non-blocking */ }\n }\n\n async createOrmEntity<T extends object>(opts: { entity: EntityName<T>; data: EntityData<T> }): Promise<T> {\n const entity = this.em.create(\n opts.entity as EntityName<T>,\n opts.data as unknown as RequiredEntityData<T>\n )\n await this.em.persist(entity).flush()\n return entity\n }\n\n async updateOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n apply: (current: T) => Promise<void> | void\n }): Promise<T | null> {\n const current = await this.em.findOne(opts.entity as EntityName<T>, opts.where as FilterQuery<NoInfer<T>>)\n if (!current) return null\n await opts.apply(current)\n await this.em.persist(current).flush()\n return current\n }\n\n async deleteOrmEntity<T extends object>(opts: {\n entity: EntityName<T>\n where: FilterQuery<T>\n soft?: boolean\n softDeleteField?: keyof T & string\n }): Promise<T | null> {\n const current = await this.em.findOne(opts.entity as EntityName<T>, opts.where as FilterQuery<NoInfer<T>>)\n if (!current) return null\n if (opts.soft !== false) {\n const field = opts.softDeleteField || ('deletedAt' as keyof T & string)\n if (typeof current === 'object' && current !== null) {\n ;(current as Record<string, unknown>)[field] = new Date()\n await this.em.persist(current).flush()\n }\n } else {\n await this.em.remove(current).flush()\n }\n return current\n }\n\n async emitOrmEntityEvent<T>(opts: {\n action: CrudEventAction\n entity: T\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): Promise<void> {\n const { action, entity, events, indexer, identifiers, syncOrigin } = opts\n if (!events && !indexer) return\n if (!identifiers?.id) return\n\n let bus: EventBus | null = null\n try {\n bus = (this.container.resolve('eventBus') as EventBus)\n } catch {\n bus = null\n }\n if (!bus) return\n\n const ctx = {\n action,\n entity,\n identifiers: {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n },\n syncOrigin: syncOrigin ?? null,\n }\n\n if (events) {\n const eventName = `${events.module}.${events.entity}.${action}`\n warnIfUndeclaredEvent(eventName, 'emitOrmEntityEvent')\n const payload = events.buildPayload\n ? events.buildPayload(ctx)\n : {\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n ...(ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}),\n }\n try {\n await bus.emitEvent(eventName, payload, {\n persistent: !!events.persistent,\n tenantId: ctx.identifiers.tenantId ?? null,\n organizationId: ctx.identifiers.organizationId ?? null,\n })\n } catch {\n // non-blocking\n }\n }\n\n if (indexer) {\n const resolveCoverageBaseDelta = (): number | undefined => {\n if (action === 'created') return 1\n if (action === 'deleted') return -1\n return undefined\n }\n const coverageBaseDelta = resolveCoverageBaseDelta()\n\n if (action === 'deleted') {\n const payload = indexer.buildDeletePayload\n ? indexer.buildDeletePayload(ctx)\n : {\n entityType: indexer.entityType,\n recordId: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }\n const enrichedPayload = payload as Record<string, unknown>\n enrichedPayload.crudAction = action\n if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta\n if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin\n // Await the index update so query-index reads (the `customValues`/scalar\n // projection that list endpoints serve) are consistent the moment the write\n // returns. The subscriber removes the projection row + tokens synchronously and\n // defers the coverage recompute + fulltext delete, so this stays bounded.\n // Errors are logged, not thrown \u2014 index drift never fails the originating write.\n await bus.emitEvent('query_index.delete_one', enrichedPayload).catch((err: unknown) => {\n console.error('[data-engine] query_index.delete_one emit failed', err)\n })\n } else {\n const payload = indexer.buildUpsertPayload\n ? indexer.buildUpsertPayload(ctx)\n : {\n entityType: indexer.entityType,\n recordId: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }\n const enrichedPayload = payload as Record<string, unknown>\n enrichedPayload.crudAction = action\n if (coverageBaseDelta !== undefined) enrichedPayload.coverageBaseDelta = coverageBaseDelta\n if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin\n // Await the projection upsert so list reads observe the new doc immediately\n // (see delete_one above). The subscriber updates `entity_indexes` synchronously\n // and defers the heavy token-reindex pipeline (build doc + encrypt + decrypt +\n // tokenize + DELETE + chunked INSERT) so write latency stays bounded.\n await bus.emitEvent('query_index.upsert_one', enrichedPayload).catch((err: unknown) => {\n console.error('[data-engine] query_index.upsert_one emit failed', err)\n })\n }\n\n if (shouldTriggerCoverageRefresh(indexer.entityType, ctx.identifiers.tenantId ?? null)) {\n void bus.emitEvent('query_index.coverage.refresh', {\n entityType: indexer.entityType,\n tenantId: ctx.identifiers.tenantId ?? null,\n organizationId: null,\n delayMs: 0,\n }).catch(() => undefined)\n }\n }\n }\n\n markOrmEntityChange<T>(opts: {\n action: CrudEventAction\n entity: T | null | undefined\n events?: CrudEventsConfig<T>\n indexer?: CrudIndexerConfig<T>\n identifiers: CrudEntityIdentifiers\n syncOrigin?: string | null\n }): void {\n const { entity, identifiers } = opts\n if (!entity) return\n if (!identifiers?.id) return\n const key = this.buildSideEffectKey(opts.action, identifiers)\n const existing = this.pendingSideEffects.get(key)\n if (existing) {\n existing.entity = entity\n existing.identifiers = {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n }\n existing.syncOrigin = opts.syncOrigin ?? null\n if (opts.events) existing.events = opts.events as CrudEventsConfig<unknown>\n if (opts.indexer) existing.indexer = opts.indexer as CrudIndexerConfig<unknown>\n this.pendingSideEffects.set(key, existing)\n return\n }\n const entry: QueuedCrudSideEffect = {\n action: opts.action,\n entity,\n identifiers: {\n id: identifiers.id,\n organizationId: identifiers.organizationId ?? null,\n tenantId: identifiers.tenantId ?? null,\n },\n syncOrigin: opts.syncOrigin ?? null,\n }\n if (opts.events) entry.events = opts.events as CrudEventsConfig<unknown>\n if (opts.indexer) entry.indexer = opts.indexer as CrudIndexerConfig<unknown>\n this.pendingSideEffects.set(key, entry)\n }\n\n async flushOrmEntityChanges(): Promise<void> {\n if (!this.pendingSideEffects.size) return\n const entries = Array.from(this.pendingSideEffects.values())\n this.pendingSideEffects.clear()\n for (const entry of entries) {\n try {\n await this.emitOrmEntityEvent({\n action: entry.action,\n entity: entry.entity,\n identifiers: entry.identifiers,\n syncOrigin: entry.syncOrigin ?? null,\n events: entry.events as CrudEventsConfig<unknown>,\n indexer: entry.indexer as CrudIndexerConfig<unknown>,\n })\n } catch {\n // best-effort; continue with remaining side effects\n }\n }\n }\n\n private buildSideEffectKey(action: CrudEventAction, identifiers: CrudEntityIdentifiers): string {\n const id = identifiers.id ?? ''\n const org = identifiers.organizationId ?? ''\n const tenant = identifiers.tenantId ?? ''\n return [action, id, org, tenant].join('|')\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/lib/db/mikro.js
CHANGED
|
@@ -25,6 +25,28 @@ function getOrmEntities() {
|
|
|
25
25
|
}
|
|
26
26
|
return entities;
|
|
27
27
|
}
|
|
28
|
+
function parsePositiveIntEnv(raw) {
|
|
29
|
+
const parsed = parseInt(raw || "");
|
|
30
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
31
|
+
}
|
|
32
|
+
function resolvePoolConfig(env = process.env) {
|
|
33
|
+
const idleSessionTimeoutEnv = parseInt(env.DB_IDLE_SESSION_TIMEOUT_MS || "");
|
|
34
|
+
const idleInTxTimeoutEnv = parseInt(env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || "");
|
|
35
|
+
return {
|
|
36
|
+
poolMin: parseInt(env.DB_POOL_MIN || "2"),
|
|
37
|
+
poolMax: parseInt(env.DB_POOL_MAX || "20"),
|
|
38
|
+
poolIdleTimeout: parseInt(env.DB_POOL_IDLE_TIMEOUT || "3000"),
|
|
39
|
+
poolAcquireTimeout: parseInt(env.DB_POOL_ACQUIRE_TIMEOUT || "6000"),
|
|
40
|
+
idleSessionTimeoutMs: Number.isFinite(idleSessionTimeoutEnv) ? idleSessionTimeoutEnv : env.NODE_ENV === "production" ? void 0 : 6e5,
|
|
41
|
+
// Finite default in every environment (including production) so a leaked or idle
|
|
42
|
+
// open transaction cannot pin a pool connection indefinitely and exhaust the pool.
|
|
43
|
+
// Mirrors the long-standing dev value; override (incl. 0 to disable) via env.
|
|
44
|
+
idleInTransactionTimeoutMs: Number.isFinite(idleInTxTimeoutEnv) ? idleInTxTimeoutEnv : 12e4,
|
|
45
|
+
// Opt-in guards against runaway statements and lock waits. No timeout when unset.
|
|
46
|
+
statementTimeoutMs: parsePositiveIntEnv(env.DB_STATEMENT_TIMEOUT_MS),
|
|
47
|
+
lockTimeoutMs: parsePositiveIntEnv(env.DB_LOCK_TIMEOUT_MS)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
28
50
|
async function getOrm() {
|
|
29
51
|
if (ormInstance) {
|
|
30
52
|
return ormInstance;
|
|
@@ -34,14 +56,16 @@ async function getOrm() {
|
|
|
34
56
|
if (!clientUrl) {
|
|
35
57
|
throw new Error("DATABASE_URL is not set");
|
|
36
58
|
}
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
59
|
+
const {
|
|
60
|
+
poolMin,
|
|
61
|
+
poolMax,
|
|
62
|
+
poolIdleTimeout,
|
|
63
|
+
poolAcquireTimeout,
|
|
64
|
+
idleSessionTimeoutMs,
|
|
65
|
+
idleInTransactionTimeoutMs,
|
|
66
|
+
statementTimeoutMs,
|
|
67
|
+
lockTimeoutMs
|
|
68
|
+
} = resolvePoolConfig();
|
|
45
69
|
const connectionOptions = idleSessionTimeoutMs && idleSessionTimeoutMs > 0 ? `-c idle_session_timeout=${idleSessionTimeoutMs}` : void 0;
|
|
46
70
|
const sslConfig = getSslConfig();
|
|
47
71
|
if (process.env.OM_DB_POOL_DEBUG === "1" || process.env.OM_INTEGRATION_TEST === "true") {
|
|
@@ -52,6 +76,8 @@ async function getOrm() {
|
|
|
52
76
|
poolAcquireTimeout,
|
|
53
77
|
idleSessionTimeoutMs,
|
|
54
78
|
idleInTransactionTimeoutMs,
|
|
79
|
+
statementTimeoutMs,
|
|
80
|
+
lockTimeoutMs,
|
|
55
81
|
nodeEnv: process.env.NODE_ENV
|
|
56
82
|
});
|
|
57
83
|
}
|
|
@@ -80,6 +106,8 @@ async function getOrm() {
|
|
|
80
106
|
driverOptions: {
|
|
81
107
|
connectionTimeoutMillis: poolAcquireTimeout,
|
|
82
108
|
idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,
|
|
109
|
+
statement_timeout: statementTimeoutMs,
|
|
110
|
+
lock_timeout: lockTimeoutMs,
|
|
83
111
|
options: connectionOptions,
|
|
84
112
|
ssl: sslConfig,
|
|
85
113
|
onPoolCreated: (pool) => {
|
|
@@ -108,6 +136,7 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
108
136
|
export {
|
|
109
137
|
getOrm,
|
|
110
138
|
getOrmEntities,
|
|
111
|
-
registerOrmEntities
|
|
139
|
+
registerOrmEntities,
|
|
140
|
+
resolvePoolConfig
|
|
112
141
|
};
|
|
113
142
|
//# sourceMappingURL=mikro.js.map
|
package/dist/lib/db/mikro.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/lib/db/mikro.ts"],
|
|
4
|
-
"sourcesContent": ["import 'dotenv/config'\nimport 'reflect-metadata'\nimport { MikroORM } from '@mikro-orm/core'\nimport { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy'\nimport { PostgreSqlDriver, type EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport { getSslConfig } from './ssl'\n\nexport type AppMikroORM = MikroORM<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>\n\nlet ormInstance: AppMikroORM | null = null\n\n// Use globalThis so standalone apps survive duplicated shared package module instances.\nconst GLOBAL_ENTITIES_KEY = '__openMercatoOrmEntities__'\n\nfunction getRegisteredEntities(): any[] | null {\n return (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] as any[] | null ?? null\n}\n\nfunction setRegisteredEntities(entities: any[]): void {\n (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] = entities\n}\n\nexport function registerOrmEntities(entities: any[]) {\n if (getRegisteredEntities() !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] ORM entities re-registered (this may occur during HMR)')\n }\n setRegisteredEntities(entities)\n}\n\nexport function getOrmEntities(): any[] {\n const entities = getRegisteredEntities()\n if (!entities) {\n throw new Error('[Bootstrap] ORM entities not registered. Call registerOrmEntities() at bootstrap.')\n }\n return entities\n}\n\nexport
|
|
5
|
-
"mappings": "AAAA,OAAO;AACP,OAAO;AACP,SAAS,gBAAgB;AACzB,SAAS,+BAA+B;AACxC,SAAS,wBAAuE;AAChF,SAAS,oBAAoB;AAI7B,IAAI,cAAkC;AAGtC,MAAM,sBAAsB;AAE5B,SAAS,wBAAsC;AAC7C,SAAQ,WAAuC,mBAAmB,KAAqB;AACzF;AAEA,SAAS,sBAAsB,UAAuB;AACpD,EAAC,WAAuC,mBAAmB,IAAI;AACjE;AAEO,SAAS,oBAAoB,UAAiB;AACnD,MAAI,sBAAsB,MAAM,QAAQ,QAAQ,IAAI,aAAa,eAAe;AAC9E,YAAQ,MAAM,oEAAoE;AAAA,EACpF;AACA,wBAAsB,QAAQ;AAChC;AAEO,SAAS,iBAAwB;AACtC,QAAM,WAAW,sBAAsB;AACvC,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,MAAM,mFAAmF;AAAA,EACrG;AACA,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["import 'dotenv/config'\nimport 'reflect-metadata'\nimport { MikroORM } from '@mikro-orm/core'\nimport { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy'\nimport { PostgreSqlDriver, type EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport { getSslConfig } from './ssl'\n\nexport type AppMikroORM = MikroORM<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>\n\nlet ormInstance: AppMikroORM | null = null\n\n// Use globalThis so standalone apps survive duplicated shared package module instances.\nconst GLOBAL_ENTITIES_KEY = '__openMercatoOrmEntities__'\n\nfunction getRegisteredEntities(): any[] | null {\n return (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] as any[] | null ?? null\n}\n\nfunction setRegisteredEntities(entities: any[]): void {\n (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] = entities\n}\n\nexport function registerOrmEntities(entities: any[]) {\n if (getRegisteredEntities() !== null && process.env.NODE_ENV === 'development') {\n console.debug('[Bootstrap] ORM entities re-registered (this may occur during HMR)')\n }\n setRegisteredEntities(entities)\n}\n\nexport function getOrmEntities(): any[] {\n const entities = getRegisteredEntities()\n if (!entities) {\n throw new Error('[Bootstrap] ORM entities not registered. Call registerOrmEntities() at bootstrap.')\n }\n return entities\n}\n\nexport type ResolvedPoolConfig = {\n poolMin: number\n poolMax: number\n poolIdleTimeout: number\n poolAcquireTimeout: number\n idleSessionTimeoutMs: number | undefined\n idleInTransactionTimeoutMs: number | undefined\n statementTimeoutMs: number | undefined\n lockTimeoutMs: number | undefined\n}\n\n// Parse an optional positive-millisecond env var. Returns undefined when unset,\n// non-numeric, or non-positive so callers treat \"no value\" as \"no timeout\".\nfunction parsePositiveIntEnv(raw: string | undefined): number | undefined {\n const parsed = parseInt(raw || '')\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined\n}\n\nexport function resolvePoolConfig(env: NodeJS.ProcessEnv = process.env): ResolvedPoolConfig {\n const idleSessionTimeoutEnv = parseInt(env.DB_IDLE_SESSION_TIMEOUT_MS || '')\n const idleInTxTimeoutEnv = parseInt(env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || '')\n return {\n poolMin: parseInt(env.DB_POOL_MIN || '2'),\n poolMax: parseInt(env.DB_POOL_MAX || '20'),\n poolIdleTimeout: parseInt(env.DB_POOL_IDLE_TIMEOUT || '3000'),\n poolAcquireTimeout: parseInt(env.DB_POOL_ACQUIRE_TIMEOUT || '6000'),\n idleSessionTimeoutMs: Number.isFinite(idleSessionTimeoutEnv)\n ? idleSessionTimeoutEnv\n : env.NODE_ENV === 'production'\n ? undefined\n : 600_000,\n // Finite default in every environment (including production) so a leaked or idle\n // open transaction cannot pin a pool connection indefinitely and exhaust the pool.\n // Mirrors the long-standing dev value; override (incl. 0 to disable) via env.\n idleInTransactionTimeoutMs: Number.isFinite(idleInTxTimeoutEnv) ? idleInTxTimeoutEnv : 120_000,\n // Opt-in guards against runaway statements and lock waits. No timeout when unset.\n statementTimeoutMs: parsePositiveIntEnv(env.DB_STATEMENT_TIMEOUT_MS),\n lockTimeoutMs: parsePositiveIntEnv(env.DB_LOCK_TIMEOUT_MS),\n }\n}\n\nexport async function getOrm() {\n if (ormInstance) {\n return ormInstance\n }\n\n const entities = getOrmEntities()\n const clientUrl = process.env.DATABASE_URL\n if (!clientUrl) {\n throw new Error('DATABASE_URL is not set')\n }\n\n // Parse connection pool settings from environment\n const {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n } = resolvePoolConfig()\n const connectionOptions =\n idleSessionTimeoutMs && idleSessionTimeoutMs > 0\n ? `-c idle_session_timeout=${idleSessionTimeoutMs}`\n : undefined\n\n const sslConfig = getSslConfig()\n\n if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {\n console.log('[orm] pool config', {\n poolMin,\n poolMax,\n poolIdleTimeout,\n poolAcquireTimeout,\n idleSessionTimeoutMs,\n idleInTransactionTimeoutMs,\n statementTimeoutMs,\n lockTimeoutMs,\n nodeEnv: process.env.NODE_ENV,\n })\n }\n\n ormInstance = await MikroORM.init<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>({\n driver: PostgreSqlDriver,\n clientUrl,\n entities,\n debug: false,\n // v7 no longer defaults to ReflectMetadataProvider. Entities in this repo use\n // `@mikro-orm/decorators/legacy`, which relies on TypeScript `emitDecoratorMetadata`\n // + reflect-metadata for type inference (nullability, column types). Without this,\n // inferred types are silently wrong at runtime.\n metadataProvider: ReflectMetadataProvider,\n // MikroORM v7 pool shape (min/max/idleTimeoutMillis). Knex-era `acquireTimeoutMillis` /\n // `destroyTimeoutMillis` were removed; acquire wait maps to pg `connectionTimeoutMillis`\n // below under `driverOptions`. Mirror `connectionTimeoutMillis` here too \u2014 older Mikro\n // versions read it from `pool`; v7 reads from `driverOptions` but accepting both\n // costs nothing and protects us from upstream config-merge regressions.\n pool: {\n min: poolMin,\n max: poolMax,\n idleTimeoutMillis: poolIdleTimeout,\n acquireTimeoutMillis: poolAcquireTimeout,\n } as any,\n // Driver options are merged into pg.PoolConfig (ClientConfig + pg-pool).\n driverOptions: {\n connectionTimeoutMillis: poolAcquireTimeout,\n idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,\n statement_timeout: statementTimeoutMs,\n lock_timeout: lockTimeoutMs,\n options: connectionOptions,\n ssl: sslConfig,\n onPoolCreated: (pool: any) => {\n if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {\n console.log('[orm] pg pool created with options', {\n max: pool.options?.max,\n min: pool.options?.min,\n idleTimeoutMillis: pool.options?.idleTimeoutMillis,\n connectionTimeoutMillis: pool.options?.connectionTimeoutMillis,\n })\n }\n },\n },\n })\n\n return ormInstance\n}\n\n\nasync function closeOrmIfLoaded(): Promise<void> {\n if (ormInstance) {\n await ormInstance.close(true)\n ormInstance = null\n }\n}\n\n// In dev mode, handle reloads cleanly without leaving dangling connections.\nif (process.env.NODE_ENV !== 'production') {\n void closeOrmIfLoaded()\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,11 +4,17 @@ import { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from "./toggl
|
|
|
4
4
|
import { parseBooleanToken } from "../boolean.js";
|
|
5
5
|
import { fetchWithTimeout, resolveTimeoutMs } from "../http/fetchWithTimeout.js";
|
|
6
6
|
const DEFAULT_VAULT_REQUEST_TIMEOUT_MS = 1e3;
|
|
7
|
+
const DEFAULT_VAULT_RECOVERY_COOLDOWN_MS = 3e4;
|
|
7
8
|
function resolveVaultRequestTimeoutMs() {
|
|
8
9
|
const raw = process.env.VAULT_REQUEST_TIMEOUT_MS;
|
|
9
10
|
const parsed = raw ? Number.parseInt(raw, 10) : void 0;
|
|
10
11
|
return resolveTimeoutMs(parsed, DEFAULT_VAULT_REQUEST_TIMEOUT_MS);
|
|
11
12
|
}
|
|
13
|
+
function resolveVaultRecoveryCooldownMs() {
|
|
14
|
+
const raw = process.env.VAULT_RECOVERY_COOLDOWN_MS;
|
|
15
|
+
const parsed = raw ? Number.parseInt(raw, 10) : void 0;
|
|
16
|
+
return resolveTimeoutMs(parsed, DEFAULT_VAULT_RECOVERY_COOLDOWN_MS);
|
|
17
|
+
}
|
|
12
18
|
class FallbackKmsService {
|
|
13
19
|
constructor(primary, fallback, onFallback) {
|
|
14
20
|
this.primary = primary;
|
|
@@ -115,14 +121,24 @@ class HashicorpVaultKmsService {
|
|
|
115
121
|
constructor(opts = {}) {
|
|
116
122
|
this.cache = /* @__PURE__ */ new Map();
|
|
117
123
|
this.healthy = true;
|
|
124
|
+
// Sticky terminal failure (missing VAULT_ADDR/VAULT_TOKEN): no amount of
|
|
125
|
+
// re-probing fixes a misconfiguration, so this never self-heals — only a
|
|
126
|
+
// restart with corrected config does.
|
|
127
|
+
this.misconfigured = false;
|
|
128
|
+
// Timestamp of the last transient failure (timeout / network blip / 5xx).
|
|
129
|
+
// Drives the half-open circuit breaker in isHealthy(): after the cooldown the
|
|
130
|
+
// instance reports healthy again so the next call re-probes Vault.
|
|
131
|
+
this.lastTransientFailureAt = null;
|
|
118
132
|
this.vaultAddr = normalizeEnv(opts.vaultAddr || process.env.VAULT_ADDR || "");
|
|
119
133
|
this.vaultToken = normalizeEnv(opts.vaultToken || process.env.VAULT_TOKEN || "");
|
|
120
134
|
this.mountPath = (opts.mountPath || process.env.VAULT_KV_PATH || "secret/data").replace(/\/+$/, "");
|
|
121
135
|
this.ttlMs = opts.ttlMs ?? 15 * 60 * 1e3;
|
|
122
136
|
this.requestTimeoutMs = resolveTimeoutMs(opts.requestTimeoutMs, resolveVaultRequestTimeoutMs());
|
|
137
|
+
this.recoveryCooldownMs = resolveTimeoutMs(opts.recoveryCooldownMs, resolveVaultRecoveryCooldownMs());
|
|
123
138
|
this.debugEnabled = isEncryptionDebugEnabled();
|
|
124
139
|
if (!this.vaultAddr || !this.vaultToken) {
|
|
125
140
|
this.healthy = false;
|
|
141
|
+
this.misconfigured = true;
|
|
126
142
|
if (this.debugEnabled) {
|
|
127
143
|
console.warn("\u26A0\uFE0F [encryption][kms] Vault misconfigured (missing VAULT_ADDR or VAULT_TOKEN)");
|
|
128
144
|
}
|
|
@@ -138,11 +154,25 @@ class HashicorpVaultKmsService {
|
|
|
138
154
|
this.loggedInit = false;
|
|
139
155
|
}
|
|
140
156
|
isHealthy() {
|
|
141
|
-
|
|
157
|
+
if (this.misconfigured) return false;
|
|
158
|
+
if (this.healthy) return true;
|
|
159
|
+
if (this.lastTransientFailureAt === null) return false;
|
|
160
|
+
return this.now() - this.lastTransientFailureAt >= this.recoveryCooldownMs;
|
|
142
161
|
}
|
|
143
162
|
now() {
|
|
144
163
|
return Date.now();
|
|
145
164
|
}
|
|
165
|
+
// Vault responded successfully (or is provably reachable): close the breaker.
|
|
166
|
+
markHealthy() {
|
|
167
|
+
this.healthy = true;
|
|
168
|
+
this.lastTransientFailureAt = null;
|
|
169
|
+
}
|
|
170
|
+
// Transient infra failure (timeout / network blip / 5xx): open the breaker and
|
|
171
|
+
// start the recovery cooldown so a later call can re-probe and self-heal.
|
|
172
|
+
markTransientFailure() {
|
|
173
|
+
this.healthy = false;
|
|
174
|
+
this.lastTransientFailureAt = this.now();
|
|
175
|
+
}
|
|
146
176
|
cacheHit(tenantId) {
|
|
147
177
|
const entry = this.cache.get(tenantId);
|
|
148
178
|
if (!entry) return null;
|
|
@@ -155,6 +185,7 @@ class HashicorpVaultKmsService {
|
|
|
155
185
|
async readVault(path) {
|
|
156
186
|
if (!this.vaultAddr || !this.vaultToken) {
|
|
157
187
|
this.healthy = false;
|
|
188
|
+
this.misconfigured = true;
|
|
158
189
|
return null;
|
|
159
190
|
}
|
|
160
191
|
try {
|
|
@@ -164,16 +195,18 @@ class HashicorpVaultKmsService {
|
|
|
164
195
|
timeoutMs: this.requestTimeoutMs
|
|
165
196
|
});
|
|
166
197
|
if (!res.ok) {
|
|
167
|
-
|
|
198
|
+
if (res.status >= 500) this.markTransientFailure();
|
|
199
|
+
else this.markHealthy();
|
|
168
200
|
console.warn("\u26A0\uFE0F [encryption][kms] Vault read failed", { path, status: res.status });
|
|
169
201
|
return null;
|
|
170
202
|
}
|
|
203
|
+
this.markHealthy();
|
|
171
204
|
if (this.debugEnabled) {
|
|
172
205
|
console.info("\u{1F50D} [encryption][kms] Vault read ok", { path });
|
|
173
206
|
}
|
|
174
207
|
return await res.json();
|
|
175
208
|
} catch (err) {
|
|
176
|
-
this.
|
|
209
|
+
this.markTransientFailure();
|
|
177
210
|
console.warn("\u26A0\uFE0F [encryption][kms] Vault read error", {
|
|
178
211
|
path,
|
|
179
212
|
error: err?.message || String(err),
|
|
@@ -185,6 +218,7 @@ class HashicorpVaultKmsService {
|
|
|
185
218
|
async writeVault(path, key, opts) {
|
|
186
219
|
if (!this.vaultAddr || !this.vaultToken) {
|
|
187
220
|
this.healthy = false;
|
|
221
|
+
this.misconfigured = true;
|
|
188
222
|
return "error";
|
|
189
223
|
}
|
|
190
224
|
const body = { data: { key } };
|
|
@@ -200,18 +234,19 @@ class HashicorpVaultKmsService {
|
|
|
200
234
|
timeoutMs: this.requestTimeoutMs
|
|
201
235
|
});
|
|
202
236
|
if (res.ok) {
|
|
203
|
-
this.
|
|
237
|
+
this.markHealthy();
|
|
204
238
|
return "ok";
|
|
205
239
|
}
|
|
206
240
|
if (typeof opts?.cas === "number" && res.status === 400) {
|
|
241
|
+
this.markHealthy();
|
|
207
242
|
console.warn("\u26A0\uFE0F [encryption][kms] Vault write CAS conflict (concurrent DEK create)", { path, status: res.status });
|
|
208
243
|
return "conflict";
|
|
209
244
|
}
|
|
210
|
-
this.
|
|
245
|
+
this.markTransientFailure();
|
|
211
246
|
console.warn("\u26A0\uFE0F [encryption][kms] Vault write failed", { path, status: res.status });
|
|
212
247
|
return "error";
|
|
213
248
|
} catch (err) {
|
|
214
|
-
this.
|
|
249
|
+
this.markTransientFailure();
|
|
215
250
|
console.warn("\u26A0\uFE0F [encryption][kms] Vault write error", {
|
|
216
251
|
path,
|
|
217
252
|
error: err?.message || String(err),
|