@open-mercato/shared 0.4.2-canary-c02407ff85
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/build.mjs +101 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/api/crud.js +47 -0
- package/dist/lib/api/crud.js.map +7 -0
- package/dist/lib/api/scoped.js +140 -0
- package/dist/lib/api/scoped.js.map +7 -0
- package/dist/lib/auth/jwt.js +34 -0
- package/dist/lib/auth/jwt.js.map +7 -0
- package/dist/lib/auth/server.js +157 -0
- package/dist/lib/auth/server.js.map +7 -0
- package/dist/lib/boolean.js +22 -0
- package/dist/lib/boolean.js.map +7 -0
- package/dist/lib/bootstrap/appResolver.js +43 -0
- package/dist/lib/bootstrap/appResolver.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +108 -0
- package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
- package/dist/lib/bootstrap/factory.js +59 -0
- package/dist/lib/bootstrap/factory.js.map +7 -0
- package/dist/lib/bootstrap/index.js +11 -0
- package/dist/lib/bootstrap/index.js.map +7 -0
- package/dist/lib/bootstrap/types.js +1 -0
- package/dist/lib/bootstrap/types.js.map +7 -0
- package/dist/lib/cache/segments.js +36 -0
- package/dist/lib/cache/segments.js.map +7 -0
- package/dist/lib/cli/progress.js +46 -0
- package/dist/lib/cli/progress.js.map +7 -0
- package/dist/lib/commands/command-bus.js +285 -0
- package/dist/lib/commands/command-bus.js.map +7 -0
- package/dist/lib/commands/customFieldSnapshots.js +66 -0
- package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
- package/dist/lib/commands/helpers.js +98 -0
- package/dist/lib/commands/helpers.js.map +7 -0
- package/dist/lib/commands/index.js +8 -0
- package/dist/lib/commands/index.js.map +7 -0
- package/dist/lib/commands/operationMetadata.js +32 -0
- package/dist/lib/commands/operationMetadata.js.map +7 -0
- package/dist/lib/commands/registry.js +43 -0
- package/dist/lib/commands/registry.js.map +7 -0
- package/dist/lib/commands/scope.js +44 -0
- package/dist/lib/commands/scope.js.map +7 -0
- package/dist/lib/commands/types.js +8 -0
- package/dist/lib/commands/types.js.map +7 -0
- package/dist/lib/crud/cache-stats.js +98 -0
- package/dist/lib/crud/cache-stats.js.map +7 -0
- package/dist/lib/crud/cache.js +175 -0
- package/dist/lib/crud/cache.js.map +7 -0
- package/dist/lib/crud/custom-fields-client.js +52 -0
- package/dist/lib/crud/custom-fields-client.js.map +7 -0
- package/dist/lib/crud/custom-fields.js +467 -0
- package/dist/lib/crud/custom-fields.js.map +7 -0
- package/dist/lib/crud/errors.js +24 -0
- package/dist/lib/crud/errors.js.map +7 -0
- package/dist/lib/crud/exporters.js +154 -0
- package/dist/lib/crud/exporters.js.map +7 -0
- package/dist/lib/crud/factory.js +1311 -0
- package/dist/lib/crud/factory.js.map +7 -0
- package/dist/lib/crud/types.js +1 -0
- package/dist/lib/crud/types.js.map +7 -0
- package/dist/lib/custom-fields/normalize.js +36 -0
- package/dist/lib/custom-fields/normalize.js.map +7 -0
- package/dist/lib/data/engine.js +396 -0
- package/dist/lib/data/engine.js.map +7 -0
- package/dist/lib/db/escapeLikePattern.js +5 -0
- package/dist/lib/db/escapeLikePattern.js.map +7 -0
- package/dist/lib/db/mikro.js +82 -0
- package/dist/lib/db/mikro.js.map +7 -0
- package/dist/lib/di/container.js +94 -0
- package/dist/lib/di/container.js.map +7 -0
- package/dist/lib/email/send.js +12 -0
- package/dist/lib/email/send.js.map +7 -0
- package/dist/lib/encryption/aes.js +58 -0
- package/dist/lib/encryption/aes.js.map +7 -0
- package/dist/lib/encryption/customFieldValues.js +49 -0
- package/dist/lib/encryption/customFieldValues.js.map +7 -0
- package/dist/lib/encryption/entityFields.js +26 -0
- package/dist/lib/encryption/entityFields.js.map +7 -0
- package/dist/lib/encryption/entityIds.js +80 -0
- package/dist/lib/encryption/entityIds.js.map +7 -0
- package/dist/lib/encryption/find.js +45 -0
- package/dist/lib/encryption/find.js.map +7 -0
- package/dist/lib/encryption/indexDoc.js +69 -0
- package/dist/lib/encryption/indexDoc.js.map +7 -0
- package/dist/lib/encryption/kms.js +282 -0
- package/dist/lib/encryption/kms.js.map +7 -0
- package/dist/lib/encryption/subscriber.js +330 -0
- package/dist/lib/encryption/subscriber.js.map +7 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
- package/dist/lib/encryption/toggles.js +18 -0
- package/dist/lib/encryption/toggles.js.map +7 -0
- package/dist/lib/entities/naming.js +9 -0
- package/dist/lib/entities/naming.js.map +7 -0
- package/dist/lib/entities/system-entities.js +43 -0
- package/dist/lib/entities/system-entities.js.map +7 -0
- package/dist/lib/frontend/organizationEvents.js +41 -0
- package/dist/lib/frontend/organizationEvents.js.map +7 -0
- package/dist/lib/frontend/useOrganizationScope.js +32 -0
- package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
- package/dist/lib/hotkeys/index.js +128 -0
- package/dist/lib/hotkeys/index.js.map +7 -0
- package/dist/lib/i18n/app-dictionaries.js +17 -0
- package/dist/lib/i18n/app-dictionaries.js.map +7 -0
- package/dist/lib/i18n/config.js +7 -0
- package/dist/lib/i18n/config.js.map +7 -0
- package/dist/lib/i18n/context.js +50 -0
- package/dist/lib/i18n/context.js.map +7 -0
- package/dist/lib/i18n/server.js +68 -0
- package/dist/lib/i18n/server.js.map +7 -0
- package/dist/lib/i18n/translate.js +45 -0
- package/dist/lib/i18n/translate.js.map +7 -0
- package/dist/lib/indexers/error-log.js +82 -0
- package/dist/lib/indexers/error-log.js.map +7 -0
- package/dist/lib/indexers/status-log.js +80 -0
- package/dist/lib/indexers/status-log.js.map +7 -0
- package/dist/lib/lib/auth/jwt.js +34 -0
- package/dist/lib/lib/auth/jwt.js.map +7 -0
- package/dist/lib/lib/auth/server.js +77 -0
- package/dist/lib/lib/auth/server.js.map +7 -0
- package/dist/lib/lib/email/send.js +12 -0
- package/dist/lib/lib/email/send.js.map +7 -0
- package/dist/lib/lib/i18n/config.js +7 -0
- package/dist/lib/lib/i18n/config.js.map +7 -0
- package/dist/lib/lib/i18n/context.js +31 -0
- package/dist/lib/lib/i18n/context.js.map +7 -0
- package/dist/lib/lib/utils.js +9 -0
- package/dist/lib/lib/utils.js.map +7 -0
- package/dist/lib/location/countries.js +68 -0
- package/dist/lib/location/countries.js.map +7 -0
- package/dist/lib/modules/index.js +6 -0
- package/dist/lib/modules/index.js.map +7 -0
- package/dist/lib/modules/registry.js +18 -0
- package/dist/lib/modules/registry.js.map +7 -0
- package/dist/lib/openapi/crud.js +137 -0
- package/dist/lib/openapi/crud.js.map +7 -0
- package/dist/lib/openapi/generator.js +1131 -0
- package/dist/lib/openapi/generator.js.map +7 -0
- package/dist/lib/openapi/index.js +10 -0
- package/dist/lib/openapi/index.js.map +7 -0
- package/dist/lib/openapi/sanitize.js +110 -0
- package/dist/lib/openapi/sanitize.js.map +7 -0
- package/dist/lib/openapi/types.js +1 -0
- package/dist/lib/openapi/types.js.map +7 -0
- package/dist/lib/profiler/index.js +258 -0
- package/dist/lib/profiler/index.js.map +7 -0
- package/dist/lib/query/engine.js +729 -0
- package/dist/lib/query/engine.js.map +7 -0
- package/dist/lib/query/join-utils.js +195 -0
- package/dist/lib/query/join-utils.js.map +7 -0
- package/dist/lib/query/types.js +9 -0
- package/dist/lib/query/types.js.map +7 -0
- package/dist/lib/search/config.js +32 -0
- package/dist/lib/search/config.js.map +7 -0
- package/dist/lib/search/tokenize.js +34 -0
- package/dist/lib/search/tokenize.js.map +7 -0
- package/dist/lib/slugify.js +24 -0
- package/dist/lib/slugify.js.map +7 -0
- package/dist/lib/testing/bootstrap.js +51 -0
- package/dist/lib/testing/bootstrap.js.map +7 -0
- package/dist/lib/testing/index.js +17 -0
- package/dist/lib/testing/index.js.map +7 -0
- package/dist/lib/testing/renderWithProviders.js +15 -0
- package/dist/lib/testing/renderWithProviders.js.map +7 -0
- package/dist/lib/url.js +12 -0
- package/dist/lib/url.js.map +7 -0
- package/dist/lib/utils.js +13 -0
- package/dist/lib/utils.js.map +7 -0
- package/dist/lib/version.js +7 -0
- package/dist/lib/version.js.map +7 -0
- package/dist/modules/dashboard/widgets.js +1 -0
- package/dist/modules/dashboard/widgets.js.map +7 -0
- package/dist/modules/dsl.js +30 -0
- package/dist/modules/dsl.js.map +7 -0
- package/dist/modules/entities/kinds.js +22 -0
- package/dist/modules/entities/kinds.js.map +7 -0
- package/dist/modules/entities/options.js +26 -0
- package/dist/modules/entities/options.js.map +7 -0
- package/dist/modules/entities/validation.js +102 -0
- package/dist/modules/entities/validation.js.map +7 -0
- package/dist/modules/entities/validators.js +88 -0
- package/dist/modules/entities/validators.js.map +7 -0
- package/dist/modules/entities.js +1 -0
- package/dist/modules/entities.js.map +7 -0
- package/dist/modules/navigation/sidebarPreferences.js +50 -0
- package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
- package/dist/modules/perspectives/types.js +1 -0
- package/dist/modules/perspectives/types.js.map +7 -0
- package/dist/modules/registry.js +96 -0
- package/dist/modules/registry.js.map +7 -0
- package/dist/modules/search.js +15 -0
- package/dist/modules/search.js.map +7 -0
- package/dist/modules/vector.js +1 -0
- package/dist/modules/vector.js.map +7 -0
- package/dist/modules/widgets/injection-loader.js +180 -0
- package/dist/modules/widgets/injection-loader.js.map +7 -0
- package/dist/modules/widgets/injection.js +1 -0
- package/dist/modules/widgets/injection.js.map +7 -0
- package/dist/security/features.js +23 -0
- package/dist/security/features.js.map +7 -0
- package/dist/types/pg.d.js +1 -0
- package/dist/types/pg.d.js.map +7 -0
- package/dist/types/react-email.d.js +1 -0
- package/dist/types/react-email.d.js.map +7 -0
- package/dist/types/resend.d.js +1 -0
- package/dist/types/resend.d.js.map +7 -0
- package/jest.config.cjs +22 -0
- package/package.json +88 -0
- package/src/index.ts +0 -0
- package/src/lib/api/__tests__/scoped.test.ts +38 -0
- package/src/lib/api/crud.ts +59 -0
- package/src/lib/api/scoped.ts +239 -0
- package/src/lib/auth/jwt.ts +39 -0
- package/src/lib/auth/server.ts +199 -0
- package/src/lib/boolean.ts +17 -0
- package/src/lib/bootstrap/appResolver.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +177 -0
- package/src/lib/bootstrap/factory.ts +108 -0
- package/src/lib/bootstrap/index.ts +23 -0
- package/src/lib/bootstrap/types.ts +31 -0
- package/src/lib/cache/segments.ts +56 -0
- package/src/lib/cli/progress.ts +55 -0
- package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
- package/src/lib/commands/__tests__/helpers.test.ts +42 -0
- package/src/lib/commands/command-bus.ts +349 -0
- package/src/lib/commands/customFieldSnapshots.ts +86 -0
- package/src/lib/commands/helpers.ts +143 -0
- package/src/lib/commands/index.ts +4 -0
- package/src/lib/commands/operationMetadata.ts +40 -0
- package/src/lib/commands/registry.ts +46 -0
- package/src/lib/commands/scope.ts +59 -0
- package/src/lib/commands/types.ts +63 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
- package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
- package/src/lib/crud/cache-stats.ts +127 -0
- package/src/lib/crud/cache.ts +205 -0
- package/src/lib/crud/custom-fields-client.ts +54 -0
- package/src/lib/crud/custom-fields.ts +607 -0
- package/src/lib/crud/errors.ts +23 -0
- package/src/lib/crud/exporters.ts +188 -0
- package/src/lib/crud/factory.ts +1622 -0
- package/src/lib/crud/types.ts +29 -0
- package/src/lib/custom-fields/normalize.ts +45 -0
- package/src/lib/data/engine.ts +562 -0
- package/src/lib/db/escapeLikePattern.ts +2 -0
- package/src/lib/db/mikro.ts +100 -0
- package/src/lib/di/container.ts +105 -0
- package/src/lib/email/send.ts +18 -0
- package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
- package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
- package/src/lib/encryption/aes.ts +64 -0
- package/src/lib/encryption/customFieldValues.ts +67 -0
- package/src/lib/encryption/entityFields.ts +39 -0
- package/src/lib/encryption/entityIds.ts +107 -0
- package/src/lib/encryption/find.ts +81 -0
- package/src/lib/encryption/indexDoc.ts +104 -0
- package/src/lib/encryption/kms.ts +337 -0
- package/src/lib/encryption/subscriber.ts +416 -0
- package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
- package/src/lib/encryption/toggles.ts +15 -0
- package/src/lib/entities/naming.ts +6 -0
- package/src/lib/entities/system-entities.ts +43 -0
- package/src/lib/frontend/organizationEvents.ts +55 -0
- package/src/lib/frontend/useOrganizationScope.ts +30 -0
- package/src/lib/hotkeys/index.ts +168 -0
- package/src/lib/i18n/app-dictionaries.ts +18 -0
- package/src/lib/i18n/config.ts +4 -0
- package/src/lib/i18n/context.tsx +66 -0
- package/src/lib/i18n/server.ts +74 -0
- package/src/lib/i18n/translate.ts +54 -0
- package/src/lib/indexers/error-log.ts +106 -0
- package/src/lib/indexers/status-log.ts +119 -0
- package/src/lib/lib/auth/jwt.ts +39 -0
- package/src/lib/lib/auth/server.ts +94 -0
- package/src/lib/lib/email/send.ts +18 -0
- package/src/lib/lib/i18n/config.ts +4 -0
- package/src/lib/lib/i18n/context.tsx +38 -0
- package/src/lib/lib/utils.ts +6 -0
- package/src/lib/location/countries.ts +97 -0
- package/src/lib/modules/index.ts +1 -0
- package/src/lib/modules/registry.ts +18 -0
- package/src/lib/openapi/crud.ts +218 -0
- package/src/lib/openapi/generator.ts +1311 -0
- package/src/lib/openapi/index.ts +4 -0
- package/src/lib/openapi/sanitize.ts +137 -0
- package/src/lib/openapi/types.ts +79 -0
- package/src/lib/profiler/index.ts +371 -0
- package/src/lib/query/__tests__/engine.test.ts +274 -0
- package/src/lib/query/engine.ts +837 -0
- package/src/lib/query/join-utils.ts +238 -0
- package/src/lib/query/types.ts +121 -0
- package/src/lib/search/config.ts +49 -0
- package/src/lib/search/tokenize.ts +45 -0
- package/src/lib/slugify.ts +28 -0
- package/src/lib/testing/bootstrap.ts +124 -0
- package/src/lib/testing/index.ts +15 -0
- package/src/lib/testing/renderWithProviders.tsx +31 -0
- package/src/lib/url.ts +12 -0
- package/src/lib/utils.ts +17 -0
- package/src/lib/version.ts +5 -0
- package/src/modules/__tests__/dsl.test.ts +35 -0
- package/src/modules/__tests__/registry.test.ts +300 -0
- package/src/modules/dashboard/widgets.ts +57 -0
- package/src/modules/dsl.ts +32 -0
- package/src/modules/entities/__tests__/validation.test.ts +52 -0
- package/src/modules/entities/kinds.ts +20 -0
- package/src/modules/entities/options.ts +36 -0
- package/src/modules/entities/validation.ts +118 -0
- package/src/modules/entities/validators.ts +93 -0
- package/src/modules/entities.ts +102 -0
- package/src/modules/navigation/sidebarPreferences.ts +62 -0
- package/src/modules/perspectives/types.ts +40 -0
- package/src/modules/registry.ts +249 -0
- package/src/modules/search.ts +325 -0
- package/src/modules/vector.ts +122 -0
- package/src/modules/widgets/__tests__/injection.test.ts +48 -0
- package/src/modules/widgets/injection-loader.ts +235 -0
- package/src/modules/widgets/injection.ts +120 -0
- package/src/security/features.ts +22 -0
- package/src/types/pg.d.ts +2 -0
- package/src/types/react-email.d.ts +2 -0
- package/src/types/resend.d.ts +2 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import 'dotenv/config'
|
|
2
|
+
import 'reflect-metadata'
|
|
3
|
+
import { MikroORM } from '@mikro-orm/core'
|
|
4
|
+
import { PostgreSqlDriver } from '@mikro-orm/postgresql'
|
|
5
|
+
|
|
6
|
+
let ormInstance: MikroORM<PostgreSqlDriver> | null = null
|
|
7
|
+
|
|
8
|
+
// Registration pattern for publishable packages
|
|
9
|
+
let _entities: any[] | null = null
|
|
10
|
+
|
|
11
|
+
export function registerOrmEntities(entities: any[]) {
|
|
12
|
+
if (_entities !== null && process.env.NODE_ENV === 'development') {
|
|
13
|
+
console.debug('[Bootstrap] ORM entities re-registered (this may occur during HMR)')
|
|
14
|
+
}
|
|
15
|
+
_entities = entities
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getOrmEntities(): any[] {
|
|
19
|
+
if (!_entities) {
|
|
20
|
+
throw new Error('[Bootstrap] ORM entities not registered. Call registerOrmEntities() at bootstrap.')
|
|
21
|
+
}
|
|
22
|
+
return _entities
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getOrm() {
|
|
26
|
+
if (ormInstance) {
|
|
27
|
+
return ormInstance
|
|
28
|
+
}
|
|
29
|
+
const entities = getOrmEntities()
|
|
30
|
+
const clientUrl = process.env.DATABASE_URL
|
|
31
|
+
if (!clientUrl) throw new Error('DATABASE_URL is not set')
|
|
32
|
+
|
|
33
|
+
// Parse connection pool settings from environment
|
|
34
|
+
const poolMin = parseInt(process.env.DB_POOL_MIN || '2')
|
|
35
|
+
const poolMax = parseInt(process.env.DB_POOL_MAX || '50')
|
|
36
|
+
const poolIdleTimeout = parseInt(process.env.DB_POOL_IDLE_TIMEOUT || '3000')
|
|
37
|
+
const poolAcquireTimeout = parseInt(process.env.DB_POOL_ACQUIRE_TIMEOUT || '6000')
|
|
38
|
+
const idleSessionTimeoutEnv = parseInt(process.env.DB_IDLE_SESSION_TIMEOUT_MS || '')
|
|
39
|
+
const idleInTxTimeoutEnv = parseInt(process.env.DB_IDLE_IN_TRANSACTION_TIMEOUT_MS || '')
|
|
40
|
+
const idleSessionTimeoutMs = Number.isFinite(idleSessionTimeoutEnv)
|
|
41
|
+
? idleSessionTimeoutEnv
|
|
42
|
+
: process.env.NODE_ENV === 'production'
|
|
43
|
+
? undefined
|
|
44
|
+
: 600_000
|
|
45
|
+
const idleInTransactionTimeoutMs = Number.isFinite(idleInTxTimeoutEnv)
|
|
46
|
+
? idleInTxTimeoutEnv
|
|
47
|
+
: process.env.NODE_ENV === 'production'
|
|
48
|
+
? undefined
|
|
49
|
+
: 120_000
|
|
50
|
+
const connectionOptions =
|
|
51
|
+
idleSessionTimeoutMs && idleSessionTimeoutMs > 0
|
|
52
|
+
? `-c idle_session_timeout=${idleSessionTimeoutMs}`
|
|
53
|
+
: undefined
|
|
54
|
+
|
|
55
|
+
ormInstance = await MikroORM.init<PostgreSqlDriver>({
|
|
56
|
+
driver: PostgreSqlDriver,
|
|
57
|
+
clientUrl,
|
|
58
|
+
entities,
|
|
59
|
+
debug: false,
|
|
60
|
+
// Connection pooling configuration
|
|
61
|
+
pool: {
|
|
62
|
+
min: poolMin,
|
|
63
|
+
max: poolMax,
|
|
64
|
+
idleTimeoutMillis: poolIdleTimeout,
|
|
65
|
+
acquireTimeoutMillis: poolAcquireTimeout,
|
|
66
|
+
// Close idle connections after 30 seconds
|
|
67
|
+
destroyTimeoutMillis: process.env.NODE_ENV === 'production' ? 30000 : 3000,
|
|
68
|
+
},
|
|
69
|
+
// Connection options
|
|
70
|
+
driverOptions: {
|
|
71
|
+
// Enable connection pooling
|
|
72
|
+
connection: {
|
|
73
|
+
// Maximum number of connections in the pool
|
|
74
|
+
max: poolMax,
|
|
75
|
+
// Minimum number of connections in the pool
|
|
76
|
+
min: poolMin,
|
|
77
|
+
// Close connections after this many milliseconds of inactivity
|
|
78
|
+
idleTimeoutMillis: poolIdleTimeout,
|
|
79
|
+
// Maximum time to wait for a connection from the pool
|
|
80
|
+
acquireTimeoutMillis: poolAcquireTimeout,
|
|
81
|
+
idle_in_transaction_session_timeout: idleInTransactionTimeoutMs,
|
|
82
|
+
options: connectionOptions,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
return ormInstance
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async function closeOrmIfLoaded(): Promise<void> {
|
|
91
|
+
if (ormInstance) {
|
|
92
|
+
await ormInstance.close(true)
|
|
93
|
+
ormInstance = null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// In dev mode, handle reloads cleanly without leaving dangling connections.
|
|
98
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
99
|
+
void closeOrmIfLoaded()
|
|
100
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createContainer, asValue, AwilixContainer, InjectionMode } from 'awilix'
|
|
2
|
+
import { RequestContext } from '@mikro-orm/core'
|
|
3
|
+
import { getOrm } from '@open-mercato/shared/lib/db/mikro'
|
|
4
|
+
import { EntityManager } from '@mikro-orm/postgresql'
|
|
5
|
+
import { BasicQueryEngine } from '@open-mercato/shared/lib/query/engine'
|
|
6
|
+
import { DefaultDataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
7
|
+
import { commandRegistry, CommandBus } from '@open-mercato/shared/lib/commands'
|
|
8
|
+
|
|
9
|
+
export type AppContainer = AwilixContainer
|
|
10
|
+
export type DiRegistrar = (container: AwilixContainer) => void
|
|
11
|
+
|
|
12
|
+
// Registration pattern for publishable packages
|
|
13
|
+
// Use globalThis to survive tsx/esbuild module duplication issue where the same
|
|
14
|
+
// file can be loaded as multiple module instances when mixing dynamic and static imports
|
|
15
|
+
const GLOBAL_KEY = '__openMercatoDiRegistrars__'
|
|
16
|
+
|
|
17
|
+
function getGlobalRegistrars(): DiRegistrar[] | null {
|
|
18
|
+
return (globalThis as any)[GLOBAL_KEY] ?? null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function setGlobalRegistrars(registrars: DiRegistrar[]): void {
|
|
22
|
+
(globalThis as any)[GLOBAL_KEY] = registrars
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerDiRegistrars(registrars: DiRegistrar[]) {
|
|
26
|
+
const existing = getGlobalRegistrars()
|
|
27
|
+
if (existing !== null && process.env.NODE_ENV === 'development') {
|
|
28
|
+
console.debug('[Bootstrap] DI registrars re-registered (this may occur during HMR)')
|
|
29
|
+
}
|
|
30
|
+
setGlobalRegistrars(registrars)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getDiRegistrars(): DiRegistrar[] {
|
|
34
|
+
const registrars = getGlobalRegistrars()
|
|
35
|
+
if (!registrars) {
|
|
36
|
+
throw new Error('[Bootstrap] DI registrars not registered. Call registerDiRegistrars() at bootstrap.')
|
|
37
|
+
}
|
|
38
|
+
return registrars
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createRequestContainer(): Promise<AppContainer> {
|
|
42
|
+
const diRegistrars = getDiRegistrars()
|
|
43
|
+
const orm = await getOrm()
|
|
44
|
+
// Use a fresh event manager so request-level subscribers (e.g., encryption) don't pile up globally
|
|
45
|
+
const baseEm = (RequestContext.getEntityManager() as any) ?? orm.em
|
|
46
|
+
const em = baseEm.fork({ clear: true, freshEventManager: true, useContext: true }) as unknown as EntityManager
|
|
47
|
+
const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
|
|
48
|
+
// Core registrations
|
|
49
|
+
container.register({
|
|
50
|
+
em: asValue(em),
|
|
51
|
+
queryEngine: asValue(new BasicQueryEngine(em, undefined, () => {
|
|
52
|
+
try { return container.resolve('tenantEncryptionService') as any } catch { return null }
|
|
53
|
+
})),
|
|
54
|
+
dataEngine: asValue(new DefaultDataEngine(em, container as any)),
|
|
55
|
+
commandRegistry: asValue(commandRegistry),
|
|
56
|
+
commandBus: asValue(new CommandBus()),
|
|
57
|
+
})
|
|
58
|
+
// Allow modules to override/extend
|
|
59
|
+
for (const reg of diRegistrars) {
|
|
60
|
+
try { reg?.(container) } catch {}
|
|
61
|
+
}
|
|
62
|
+
// Core bootstrap (cache, event bus, encryption subscriber/KMS, module subscribers)
|
|
63
|
+
try {
|
|
64
|
+
const { bootstrap } = await import('@open-mercato/core/bootstrap') as any
|
|
65
|
+
if (bootstrap && typeof bootstrap === 'function') {
|
|
66
|
+
// Avoid double bootstrap if caller already wired it
|
|
67
|
+
const alreadyBootstrapped = !!container.registrations?.eventBus
|
|
68
|
+
if (!alreadyBootstrapped) {
|
|
69
|
+
await bootstrap(container)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch { /* optional */ }
|
|
73
|
+
// App-level DI override (last chance)
|
|
74
|
+
// This import path resolves only in the app context, not in packages
|
|
75
|
+
try {
|
|
76
|
+
// @ts-ignore - @/di only exists in app context, not in packages
|
|
77
|
+
const appDi = await import('@/di') as any
|
|
78
|
+
if (appDi?.register) {
|
|
79
|
+
try {
|
|
80
|
+
const maybe = appDi.register(container)
|
|
81
|
+
if (maybe && typeof maybe.then === 'function') await maybe
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
// Ensure tenant encryption subscriber is always registered on the fresh request-scoped EM
|
|
86
|
+
try {
|
|
87
|
+
const emForEnc = container.resolve('em') as any
|
|
88
|
+
const tenantEncryptionService = container.hasRegistration('tenantEncryptionService')
|
|
89
|
+
? (container.resolve('tenantEncryptionService') as any)
|
|
90
|
+
: null
|
|
91
|
+
if (emForEnc && tenantEncryptionService?.isEnabled?.()) {
|
|
92
|
+
const { registerTenantEncryptionSubscriber } = await import('@open-mercato/shared/lib/encryption/subscriber')
|
|
93
|
+
registerTenantEncryptionSubscriber(emForEnc, tenantEncryptionService)
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// best-effort; do not block container creation
|
|
97
|
+
}
|
|
98
|
+
return container
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
102
|
+
require('server-only')
|
|
103
|
+
} catch {
|
|
104
|
+
// allow CLI/generator usage where Next server-only is not present
|
|
105
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Resend } from 'resend'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
export type SendEmailOptions = {
|
|
5
|
+
to: string
|
|
6
|
+
subject: string
|
|
7
|
+
react: React.ReactElement
|
|
8
|
+
from?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function sendEmail({ to, subject, react, from }: SendEmailOptions) {
|
|
12
|
+
const apiKey = process.env.RESEND_API_KEY
|
|
13
|
+
if (!apiKey) throw new Error('RESEND_API_KEY is not set')
|
|
14
|
+
const resend = new Resend(apiKey)
|
|
15
|
+
const fromAddr = from || process.env.EMAIL_FROM || 'no-reply@localhost'
|
|
16
|
+
await resend.emails.send({ to, subject, from: fromAddr, react })
|
|
17
|
+
}
|
|
18
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/core'
|
|
2
|
+
import { decryptCustomFieldValue, encryptCustomFieldValue, resolveTenantEncryptionService } from '../customFieldValues'
|
|
3
|
+
|
|
4
|
+
const fixedKey = Buffer.alloc(32, 1).toString('base64')
|
|
5
|
+
|
|
6
|
+
describe('customFieldValues encryption helpers', () => {
|
|
7
|
+
it('caches tenant encryption service per entity manager', () => {
|
|
8
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
9
|
+
const em = {} as EntityManager
|
|
10
|
+
const first = resolveTenantEncryptionService(em)
|
|
11
|
+
const second = resolveTenantEncryptionService(em)
|
|
12
|
+
expect(first).toBe(second)
|
|
13
|
+
warnSpy.mockRestore()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('encrypts and decrypts primitives when enabled', async () => {
|
|
17
|
+
const service = {
|
|
18
|
+
isEnabled: () => true,
|
|
19
|
+
getDek: async () => ({ key: fixedKey }),
|
|
20
|
+
} as any
|
|
21
|
+
const cache = new Map<string | null, string | null>()
|
|
22
|
+
|
|
23
|
+
const encrypted = await encryptCustomFieldValue('secret', 'tenant-1', service, cache)
|
|
24
|
+
expect(typeof encrypted).toBe('string')
|
|
25
|
+
const decrypted = await decryptCustomFieldValue(encrypted, 'tenant-1', service, cache)
|
|
26
|
+
expect(decrypted).toBe('secret')
|
|
27
|
+
|
|
28
|
+
const encryptedNumber = await encryptCustomFieldValue(42, 'tenant-1', service, cache)
|
|
29
|
+
const decryptedNumber = await decryptCustomFieldValue(encryptedNumber, 'tenant-1', service, cache)
|
|
30
|
+
expect(decryptedNumber).toBe(42)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('creates a tenant DEK on first encrypt when none exists', async () => {
|
|
34
|
+
let created = false
|
|
35
|
+
const service = {
|
|
36
|
+
isEnabled: () => true,
|
|
37
|
+
getDek: jest.fn(async () => (created ? { key: fixedKey } : null)),
|
|
38
|
+
createDek: jest.fn(async () => {
|
|
39
|
+
created = true
|
|
40
|
+
return { key: fixedKey }
|
|
41
|
+
}),
|
|
42
|
+
} as any
|
|
43
|
+
const cache = new Map<string | null, string | null>()
|
|
44
|
+
|
|
45
|
+
const encrypted = await encryptCustomFieldValue('secret', 'tenant-1', service, cache)
|
|
46
|
+
expect(typeof encrypted).toBe('string')
|
|
47
|
+
expect(encrypted).not.toBe('secret')
|
|
48
|
+
expect(service.createDek).toHaveBeenCalledTimes(1)
|
|
49
|
+
|
|
50
|
+
const decrypted = await decryptCustomFieldValue(encrypted, 'tenant-1', service, cache)
|
|
51
|
+
expect(decrypted).toBe('secret')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns original value when encryption is disabled or tenant is missing', async () => {
|
|
55
|
+
const disabledService = {
|
|
56
|
+
isEnabled: () => false,
|
|
57
|
+
getDek: async () => ({ key: fixedKey }),
|
|
58
|
+
} as any
|
|
59
|
+
expect(await encryptCustomFieldValue('plain', 'tenant-1', disabledService)).toBe('plain')
|
|
60
|
+
expect(await decryptCustomFieldValue('plain', 'tenant-1', disabledService)).toBe('plain')
|
|
61
|
+
expect(await encryptCustomFieldValue('plain', null, disabledService)).toBe('plain')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { decryptIndexDocCustomFields, decryptIndexDocForSearch, encryptIndexDocForStorage } from '../indexDoc'
|
|
2
|
+
import { decryptCustomFieldValue } from '../customFieldValues'
|
|
3
|
+
|
|
4
|
+
jest.mock('../customFieldValues', () => ({
|
|
5
|
+
decryptCustomFieldValue: jest.fn(async (value: unknown) => value),
|
|
6
|
+
}))
|
|
7
|
+
|
|
8
|
+
const decryptCustomFieldValueMock = decryptCustomFieldValue as jest.Mock
|
|
9
|
+
|
|
10
|
+
describe('encryption/indexDoc', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
decryptCustomFieldValueMock.mockReset()
|
|
13
|
+
decryptCustomFieldValueMock.mockImplementation(async (value: unknown) => value)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('decryptIndexDocCustomFields decrypts cf keys (including arrays)', async () => {
|
|
17
|
+
decryptCustomFieldValueMock.mockImplementation(async (value: unknown) => {
|
|
18
|
+
if (value === 'enc') return 'dec'
|
|
19
|
+
if (value === 'enc2') return 'dec2'
|
|
20
|
+
return value
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const doc = {
|
|
24
|
+
id: '1',
|
|
25
|
+
title: 'Encrypted',
|
|
26
|
+
'cf:secret': 'enc',
|
|
27
|
+
'cf:tags': ['enc2', 'plain'],
|
|
28
|
+
cf_secret: 'enc',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const out = await decryptIndexDocCustomFields(doc, { tenantId: 't1', organizationId: 'org1' }, {} as any)
|
|
32
|
+
expect(out).toEqual({
|
|
33
|
+
id: '1',
|
|
34
|
+
title: 'Encrypted',
|
|
35
|
+
'cf:secret': 'dec',
|
|
36
|
+
'cf:tags': ['dec2', 'plain'],
|
|
37
|
+
cf_secret: 'dec',
|
|
38
|
+
})
|
|
39
|
+
expect(decryptCustomFieldValueMock).toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('decryptIndexDocForSearch merges decrypted entity payload and decrypts cf keys', async () => {
|
|
43
|
+
decryptCustomFieldValueMock.mockImplementation(async (value: unknown) => (value === 'enc' ? 'dec' : value))
|
|
44
|
+
|
|
45
|
+
const service = {
|
|
46
|
+
isEnabled: () => true,
|
|
47
|
+
decryptEntityPayload: jest.fn(async (_entityId: string, _payload: Record<string, unknown>) => ({
|
|
48
|
+
title: 'Plain',
|
|
49
|
+
})),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const out = await decryptIndexDocForSearch(
|
|
53
|
+
'example:todo',
|
|
54
|
+
{ id: '1', title: 'Encrypted', 'cf:secret': 'enc' },
|
|
55
|
+
{ tenantId: 't1', organizationId: 'org1' },
|
|
56
|
+
service as any,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
expect(out.title).toBe('Plain')
|
|
60
|
+
expect(out['cf:secret']).toBe('dec')
|
|
61
|
+
expect(service.decryptEntityPayload).toHaveBeenCalledWith(
|
|
62
|
+
'example:todo',
|
|
63
|
+
expect.any(Object),
|
|
64
|
+
't1',
|
|
65
|
+
'org1',
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('decryptIndexDocForSearch decrypts customer entity when indexing customer profiles', async () => {
|
|
70
|
+
const service = {
|
|
71
|
+
isEnabled: () => true,
|
|
72
|
+
decryptEntityPayload: jest.fn(async (entityId: string) => (entityId === 'customers:customer_entity' ? { display_name: 'Plain' } : {})),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const out = await decryptIndexDocForSearch(
|
|
76
|
+
'customers:customer_person_profile',
|
|
77
|
+
{ id: '1', display_name: 'Encrypted' },
|
|
78
|
+
{ tenantId: 't1', organizationId: 'org1' },
|
|
79
|
+
service as any,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
expect(out.display_name).toBe('Plain')
|
|
83
|
+
expect(service.decryptEntityPayload).toHaveBeenCalledWith(
|
|
84
|
+
'customers:customer_entity',
|
|
85
|
+
expect.any(Object),
|
|
86
|
+
't1',
|
|
87
|
+
'org1',
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('encryptIndexDocForStorage encrypts entity fields using the configured map', async () => {
|
|
92
|
+
const service = {
|
|
93
|
+
isEnabled: () => true,
|
|
94
|
+
encryptEntityPayload: jest.fn(async (_entityId: string, payload: Record<string, unknown>) => ({
|
|
95
|
+
...payload,
|
|
96
|
+
resultTitle: 'enc',
|
|
97
|
+
})),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const out = await encryptIndexDocForStorage(
|
|
101
|
+
'vector:vector_search',
|
|
102
|
+
{ resultTitle: 'plain' },
|
|
103
|
+
{ tenantId: 't1', organizationId: 'org1' },
|
|
104
|
+
service as any,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
expect(out.resultTitle).toBe('enc')
|
|
108
|
+
expect(service.encryptEntityPayload).toHaveBeenCalledWith(
|
|
109
|
+
'vector:vector_search',
|
|
110
|
+
expect.any(Object),
|
|
111
|
+
't1',
|
|
112
|
+
'org1',
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import { isEncryptionDebugEnabled } from './toggles'
|
|
3
|
+
|
|
4
|
+
export type EncryptionPayload = {
|
|
5
|
+
value: string | null
|
|
6
|
+
raw: string
|
|
7
|
+
version: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function generateDek(): string {
|
|
11
|
+
return crypto.randomBytes(32).toString('base64')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function logDebug(event: string, payload: Record<string, unknown>) {
|
|
15
|
+
if (!isEncryptionDebugEnabled()) return
|
|
16
|
+
try {
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.debug('[encryption]', event, payload)
|
|
19
|
+
} catch {
|
|
20
|
+
// ignore
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function encryptWithAesGcm(value: string, dekBase64: string): EncryptionPayload {
|
|
25
|
+
const dek = Buffer.from(dekBase64, 'base64')
|
|
26
|
+
const iv = crypto.randomBytes(12)
|
|
27
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', dek, iv)
|
|
28
|
+
const ciphertext = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()])
|
|
29
|
+
const tag = cipher.getAuthTag()
|
|
30
|
+
const payload = [
|
|
31
|
+
iv.toString('base64'),
|
|
32
|
+
ciphertext.toString('base64'),
|
|
33
|
+
tag.toString('base64'),
|
|
34
|
+
'v1',
|
|
35
|
+
].join(':')
|
|
36
|
+
logDebug('encrypt', { length: ciphertext.length })
|
|
37
|
+
return { value: payload, raw: payload, version: 'v1' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function decryptWithAesGcm(payload: string, dekBase64: string): string | null {
|
|
41
|
+
if (!payload) return null
|
|
42
|
+
const parts = payload.split(':')
|
|
43
|
+
if (parts.length !== 4) return null
|
|
44
|
+
const [ivB64, ciphertextB64, tagB64, version] = parts
|
|
45
|
+
if (version !== 'v1') return null
|
|
46
|
+
const dek = Buffer.from(dekBase64, 'base64')
|
|
47
|
+
const iv = Buffer.from(ivB64, 'base64')
|
|
48
|
+
const ciphertext = Buffer.from(ciphertextB64, 'base64')
|
|
49
|
+
const tag = Buffer.from(tagB64, 'base64')
|
|
50
|
+
try {
|
|
51
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', dek, iv)
|
|
52
|
+
decipher.setAuthTag(tag)
|
|
53
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
|
|
54
|
+
logDebug('decrypt', { iv: ivB64, tag: tagB64 })
|
|
55
|
+
return decrypted
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logDebug('decrypt_error', { message: (err as Error)?.message || String(err) })
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function hashForLookup(value: string): string {
|
|
63
|
+
return crypto.createHash('sha256').update(value.toLowerCase().trim()).digest('hex')
|
|
64
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/core'
|
|
2
|
+
import { encryptWithAesGcm, decryptWithAesGcm } from './aes'
|
|
3
|
+
import { TenantDataEncryptionService } from './tenantDataEncryptionService'
|
|
4
|
+
|
|
5
|
+
const serviceCache = new WeakMap<EntityManager, TenantDataEncryptionService>()
|
|
6
|
+
|
|
7
|
+
export function resolveTenantEncryptionService(
|
|
8
|
+
em: EntityManager,
|
|
9
|
+
provided?: TenantDataEncryptionService | null,
|
|
10
|
+
): TenantDataEncryptionService | null {
|
|
11
|
+
if (provided) return provided
|
|
12
|
+
const cached = serviceCache.get(em)
|
|
13
|
+
if (cached) return cached
|
|
14
|
+
const service = new TenantDataEncryptionService(em as any)
|
|
15
|
+
serviceCache.set(em, service)
|
|
16
|
+
return service
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function resolveDekKey(
|
|
20
|
+
service: TenantDataEncryptionService | null,
|
|
21
|
+
tenantId: string | null | undefined,
|
|
22
|
+
cache?: Map<string | null, string | null>,
|
|
23
|
+
opts?: { createIfMissing?: boolean },
|
|
24
|
+
): Promise<string | null> {
|
|
25
|
+
const scopedTenantId = tenantId ?? null
|
|
26
|
+
if (!service || !service.isEnabled() || !scopedTenantId) return null
|
|
27
|
+
if (cache?.has(scopedTenantId)) return cache.get(scopedTenantId) ?? null
|
|
28
|
+
const dek = await service.getDek(scopedTenantId)
|
|
29
|
+
let key = dek?.key ?? null
|
|
30
|
+
if (!key && opts?.createIfMissing && typeof service.createDek === 'function') {
|
|
31
|
+
const created = await service.createDek(scopedTenantId)
|
|
32
|
+
key = created?.key ?? null
|
|
33
|
+
}
|
|
34
|
+
cache?.set(scopedTenantId, key)
|
|
35
|
+
return key
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function encryptCustomFieldValue(
|
|
39
|
+
value: unknown,
|
|
40
|
+
tenantId: string | null | undefined,
|
|
41
|
+
service: TenantDataEncryptionService | null,
|
|
42
|
+
cache?: Map<string | null, string | null>,
|
|
43
|
+
): Promise<unknown> {
|
|
44
|
+
if (value === undefined || value === null) return value
|
|
45
|
+
const key = await resolveDekKey(service, tenantId, cache, { createIfMissing: true })
|
|
46
|
+
if (!key) return value
|
|
47
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value)
|
|
48
|
+
return encryptWithAesGcm(serialized, key).value
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function decryptCustomFieldValue(
|
|
52
|
+
value: unknown,
|
|
53
|
+
tenantId: string | null | undefined,
|
|
54
|
+
service: TenantDataEncryptionService | null,
|
|
55
|
+
cache?: Map<string | null, string | null>,
|
|
56
|
+
): Promise<unknown> {
|
|
57
|
+
if (value === undefined || value === null || typeof value !== 'string') return value
|
|
58
|
+
const key = await resolveDekKey(service, tenantId, cache)
|
|
59
|
+
if (!key) return value
|
|
60
|
+
const decrypted = decryptWithAesGcm(value, key)
|
|
61
|
+
if (decrypted === null) return value
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(decrypted)
|
|
64
|
+
} catch {
|
|
65
|
+
return decrypted
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Registration pattern for entity fields (for Turbopack compatibility)
|
|
2
|
+
export type EntityFieldsRegistry = Record<string, Record<string, string>>
|
|
3
|
+
|
|
4
|
+
let _entityFieldsRegistry: EntityFieldsRegistry | null = null
|
|
5
|
+
|
|
6
|
+
export function registerEntityFields(registry: EntityFieldsRegistry) {
|
|
7
|
+
if (_entityFieldsRegistry !== null && process.env.NODE_ENV === 'development') {
|
|
8
|
+
console.debug('[Bootstrap] Entity fields re-registered (this may occur during HMR)')
|
|
9
|
+
}
|
|
10
|
+
_entityFieldsRegistry = registry
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get registered entity fields.
|
|
15
|
+
*
|
|
16
|
+
* @param throwIfNotRegistered - If true, throws error when entity fields are not registered.
|
|
17
|
+
* If false, returns empty object (useful during module load).
|
|
18
|
+
* Default: true
|
|
19
|
+
*/
|
|
20
|
+
export function getEntityFieldsRegistry(throwIfNotRegistered = true): EntityFieldsRegistry {
|
|
21
|
+
if (!_entityFieldsRegistry) {
|
|
22
|
+
if (throwIfNotRegistered) {
|
|
23
|
+
throw new Error('[Bootstrap] Entity fields not registered. Call registerEntityFields() at bootstrap.')
|
|
24
|
+
}
|
|
25
|
+
return {} as EntityFieldsRegistry
|
|
26
|
+
}
|
|
27
|
+
return _entityFieldsRegistry
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get fields for a specific entity by slug.
|
|
32
|
+
*
|
|
33
|
+
* @param slug - The entity slug (e.g., 'user', 'sales_order')
|
|
34
|
+
* @returns The entity's fields or undefined if not found
|
|
35
|
+
*/
|
|
36
|
+
export function getEntityFields(slug: string): Record<string, string> | undefined {
|
|
37
|
+
const registry = getEntityFieldsRegistry(false)
|
|
38
|
+
return registry[slug]
|
|
39
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { EntityMetadata } from '@mikro-orm/core'
|
|
2
|
+
|
|
3
|
+
// Registration pattern for publishable packages
|
|
4
|
+
export type EntityIds = Record<string, Record<string, string>>
|
|
5
|
+
|
|
6
|
+
let _entityIds: EntityIds | null = null
|
|
7
|
+
let _entityIdLookup: Map<string, string> | null = null
|
|
8
|
+
|
|
9
|
+
export function registerEntityIds(E: EntityIds) {
|
|
10
|
+
if (_entityIds !== null && process.env.NODE_ENV === 'development') {
|
|
11
|
+
console.debug('[Bootstrap] Entity IDs re-registered (this may occur during HMR)')
|
|
12
|
+
}
|
|
13
|
+
_entityIds = E
|
|
14
|
+
_entityIdLookup = null // Reset cache on re-registration
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get registered entity IDs.
|
|
19
|
+
*
|
|
20
|
+
* @param throwIfNotRegistered - If true, throws error when entity IDs are not registered.
|
|
21
|
+
* If false, returns empty object (useful during module load).
|
|
22
|
+
* Default: true
|
|
23
|
+
*/
|
|
24
|
+
export function getEntityIds(throwIfNotRegistered = true): EntityIds {
|
|
25
|
+
if (!_entityIds) {
|
|
26
|
+
if (throwIfNotRegistered) {
|
|
27
|
+
throw new Error('[Bootstrap] Entity IDs not registered. Call registerEntityIds() at bootstrap.')
|
|
28
|
+
}
|
|
29
|
+
return {} as EntityIds
|
|
30
|
+
}
|
|
31
|
+
return _entityIds
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const toSnake = (value: string): string =>
|
|
35
|
+
value
|
|
36
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
37
|
+
.replace(/[\s-]+/g, '_')
|
|
38
|
+
.replace(/__+/g, '_')
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
|
|
41
|
+
function getEntityIdLookup(): Map<string, string> {
|
|
42
|
+
if (_entityIdLookup) return _entityIdLookup
|
|
43
|
+
const E = getEntityIds()
|
|
44
|
+
const map = new Map<string, string>()
|
|
45
|
+
for (const mod of Object.values(E || {})) {
|
|
46
|
+
for (const [key, entityId] of Object.entries(mod || {})) {
|
|
47
|
+
const snake = toSnake(key)
|
|
48
|
+
map.set(snake, entityId)
|
|
49
|
+
// Also allow the original key and PascalCase class names to resolve
|
|
50
|
+
map.set(key.toLowerCase(), entityId)
|
|
51
|
+
map.set(
|
|
52
|
+
key
|
|
53
|
+
.split('_')
|
|
54
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
55
|
+
.join(''),
|
|
56
|
+
entityId,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
_entityIdLookup = map
|
|
61
|
+
return map
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const normalizeKey = (value: string): string =>
|
|
65
|
+
value
|
|
66
|
+
.replace(/["'`]/g, '')
|
|
67
|
+
.replace(/[\W]+/g, '_')
|
|
68
|
+
.replace(/__+/g, '_')
|
|
69
|
+
.toLowerCase()
|
|
70
|
+
|
|
71
|
+
const maybeSingularize = (value: string): string => {
|
|
72
|
+
if (value.endsWith('ies')) return `${value.slice(0, -3)}y`
|
|
73
|
+
if (value.endsWith('s')) return value.slice(0, -1)
|
|
74
|
+
return value
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveEntityIdFromMetadata(meta: EntityMetadata<any> | undefined): string | null {
|
|
78
|
+
if (!meta) return null
|
|
79
|
+
const candidates = [
|
|
80
|
+
(meta as any).className,
|
|
81
|
+
meta.name,
|
|
82
|
+
(meta as any).collection,
|
|
83
|
+
(meta as any).tableName,
|
|
84
|
+
].filter(Boolean) as string[]
|
|
85
|
+
|
|
86
|
+
for (const raw of candidates) {
|
|
87
|
+
const normalized = normalizeKey(raw)
|
|
88
|
+
const singular = maybeSingularize(normalized)
|
|
89
|
+
const snake = toSnake(raw)
|
|
90
|
+
const snakeSingular = maybeSingularize(snake)
|
|
91
|
+
const variants = [
|
|
92
|
+
normalized,
|
|
93
|
+
singular,
|
|
94
|
+
normalized.replace(/_/g, ''), // Pascal-ish fallback
|
|
95
|
+
singular.replace(/_/g, ''),
|
|
96
|
+
snake,
|
|
97
|
+
snakeSingular,
|
|
98
|
+
]
|
|
99
|
+
const lookup = getEntityIdLookup()
|
|
100
|
+
for (const candidate of variants) {
|
|
101
|
+
if (!candidate) continue
|
|
102
|
+
const id = lookup.get(candidate)
|
|
103
|
+
if (id) return id
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null
|
|
107
|
+
}
|