@open-mercato/shared 0.6.6-develop.6460.1.f94c74174c → 0.6.6-develop.6471.1.9ab5bdd79a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +1 -0
- package/dist/lib/commands/command-bus.js +4 -2
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +4 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/commands/flush.js +3 -4
- package/dist/lib/commands/flush.js.map +2 -2
- package/dist/lib/commands/helpers.js +3 -1
- package/dist/lib/commands/helpers.js.map +2 -2
- package/dist/lib/commands/registry.js +5 -3
- package/dist/lib/commands/registry.js.map +2 -2
- package/dist/lib/commands/scope.js +5 -3
- package/dist/lib/commands/scope.js.map +2 -2
- package/dist/lib/crud/advanced-filter-integration.js +3 -1
- package/dist/lib/crud/advanced-filter-integration.js.map +2 -2
- package/dist/lib/crud/cache.js +3 -1
- package/dist/lib/crud/cache.js.map +2 -2
- package/dist/lib/crud/custom-fields.js +4 -2
- package/dist/lib/crud/custom-fields.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +6 -10
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +10 -8
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-registry.js +3 -3
- package/dist/lib/crud/interceptor-registry.js.map +2 -2
- package/dist/lib/crud/mutation-guard.js +4 -2
- package/dist/lib/crud/mutation-guard.js.map +2 -2
- package/dist/lib/crud/optimistic-lock.js +5 -6
- package/dist/lib/crud/optimistic-lock.js.map +2 -2
- package/dist/lib/crud/route-mutation-guard.js +3 -1
- package/dist/lib/crud/route-mutation-guard.js.map +2 -2
- package/dist/lib/crud/sync-event-runner.js +3 -1
- package/dist/lib/crud/sync-event-runner.js.map +2 -2
- package/dist/lib/data/engine.js +5 -5
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/mikro.js +5 -3
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/di/container.js +3 -1
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/encryption/aes.js +3 -1
- package/dist/lib/encryption/aes.js.map +2 -2
- package/dist/lib/encryption/entityFields.js +3 -1
- package/dist/lib/encryption/entityFields.js.map +2 -2
- package/dist/lib/encryption/entityIds.js +3 -1
- package/dist/lib/encryption/entityIds.js.map +2 -2
- package/dist/lib/encryption/kms.js +25 -32
- package/dist/lib/encryption/kms.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +3 -1
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +3 -1
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/indexers/error-log.js +4 -2
- package/dist/lib/indexers/error-log.js.map +2 -2
- package/dist/lib/indexers/status-log.js +5 -3
- package/dist/lib/indexers/status-log.js.map +2 -2
- package/dist/lib/logger/index.js +28 -0
- package/dist/lib/logger/index.js.map +7 -0
- package/dist/lib/logger/level.js +43 -0
- package/dist/lib/logger/level.js.map +7 -0
- package/dist/lib/logger/transport.console.js +39 -0
- package/dist/lib/logger/transport.console.js.map +7 -0
- package/dist/lib/logger/transport.js +17 -0
- package/dist/lib/logger/transport.js.map +7 -0
- package/dist/lib/logger/transport.pretty.js +85 -0
- package/dist/lib/logger/transport.pretty.js.map +7 -0
- package/dist/lib/logger/transport.server.js +85 -0
- package/dist/lib/logger/transport.server.js.map +7 -0
- package/dist/lib/middleware/page-executor.js +3 -1
- package/dist/lib/middleware/page-executor.js.map +2 -2
- package/dist/lib/modules/registry.js +3 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/profiler/index.js +3 -2
- package/dist/lib/profiler/index.js.map +2 -2
- package/dist/lib/query/engine.js +5 -6
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/query-extension-runner.js +5 -11
- package/dist/lib/query/query-extension-runner.js.map +2 -2
- package/dist/lib/redis/connection.js +3 -1
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/url.js +7 -5
- package/dist/lib/url.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/analytics.js +3 -1
- package/dist/modules/analytics.js.map +2 -2
- package/dist/modules/dashboard/widgets.js +3 -1
- package/dist/modules/dashboard/widgets.js.map +2 -2
- package/dist/modules/events/factory.js +5 -3
- package/dist/modules/events/factory.js.map +2 -2
- package/dist/modules/overrides.js +16 -34
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/registry.js +3 -1
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/search.js +3 -1
- package/dist/modules/search.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +6 -6
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/dist/modules/widgets/injection-position.js +3 -1
- package/dist/modules/widgets/injection-position.js.map +2 -2
- package/package.json +7 -2
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +19 -6
- package/src/lib/commands/__tests__/flush.test.ts +24 -15
- package/src/lib/commands/__tests__/normalizeAuthorUserId.test.ts +14 -2
- package/src/lib/commands/__tests__/registry.test.ts +17 -2
- package/src/lib/commands/__tests__/scope.test.ts +21 -9
- package/src/lib/commands/command-bus.ts +5 -2
- package/src/lib/commands/command-interceptor-runner.ts +5 -2
- package/src/lib/commands/flush.ts +4 -5
- package/src/lib/commands/helpers.ts +9 -2
- package/src/lib/commands/registry.ts +6 -3
- package/src/lib/commands/scope.ts +6 -3
- package/src/lib/crud/__tests__/optimistic-lock.test.ts +21 -6
- package/src/lib/crud/__tests__/route-mutation-guard.test.ts +17 -2
- package/src/lib/crud/__tests__/sync-event-runner.test.ts +17 -3
- package/src/lib/crud/advanced-filter-integration.ts +4 -1
- package/src/lib/crud/cache.ts +4 -1
- package/src/lib/crud/custom-fields.ts +5 -2
- package/src/lib/crud/enricher-runner.ts +7 -10
- package/src/lib/crud/factory.ts +11 -10
- package/src/lib/crud/interceptor-registry.ts +4 -3
- package/src/lib/crud/mutation-guard.ts +5 -2
- package/src/lib/crud/optimistic-lock.ts +6 -10
- package/src/lib/crud/route-mutation-guard.ts +4 -1
- package/src/lib/crud/sync-event-runner.ts +4 -1
- package/src/lib/data/__tests__/engine.event-validation.test.ts +26 -15
- package/src/lib/data/engine.ts +6 -6
- package/src/lib/db/mikro.ts +6 -3
- package/src/lib/di/container.ts +4 -1
- package/src/lib/encryption/aes.ts +4 -2
- package/src/lib/encryption/entityFields.ts +5 -1
- package/src/lib/encryption/entityIds.ts +4 -1
- package/src/lib/encryption/kms.ts +27 -33
- package/src/lib/encryption/subscriber.ts +4 -2
- package/src/lib/encryption/tenantDataEncryptionService.ts +4 -2
- package/src/lib/indexers/__tests__/status-log.test.ts +22 -10
- package/src/lib/indexers/error-log.ts +5 -2
- package/src/lib/indexers/status-log.ts +6 -3
- package/src/lib/logger/__tests__/logger.test.ts +539 -0
- package/src/lib/logger/index.ts +33 -0
- package/src/lib/logger/level.ts +50 -0
- package/src/lib/logger/transport.console.ts +41 -0
- package/src/lib/logger/transport.pretty.ts +94 -0
- package/src/lib/logger/transport.server.ts +125 -0
- package/src/lib/logger/transport.ts +17 -0
- package/src/lib/middleware/page-executor.ts +4 -1
- package/src/lib/modules/__tests__/registry.test.ts +28 -21
- package/src/lib/modules/registry.ts +4 -1
- package/src/lib/profiler/index.ts +4 -2
- package/src/lib/query/__tests__/query-extension-runner.test.ts +21 -7
- package/src/lib/query/engine.ts +7 -10
- package/src/lib/query/query-extension-runner.ts +6 -11
- package/src/lib/redis/connection.ts +5 -1
- package/src/lib/url.ts +10 -5
- package/src/modules/__tests__/overrides.test.ts +17 -3
- package/src/modules/__tests__/route-overrides.test.ts +29 -20
- package/src/modules/analytics.ts +5 -1
- package/src/modules/dashboard/widgets.ts +4 -1
- package/src/modules/events/factory.ts +6 -3
- package/src/modules/overrides.ts +18 -34
- package/src/modules/registry.ts +4 -1
- package/src/modules/search.ts +4 -1
- package/src/modules/widgets/injection-loader.ts +7 -6
- package/src/modules/widgets/injection-position.ts +5 -1
package/src/lib/crud/factory.ts
CHANGED
|
@@ -71,11 +71,14 @@ import { mergeAdvancedFilters } from './advanced-filter-integration'
|
|
|
71
71
|
import { parseExtensionHeaders } from '../umes/extension-headers'
|
|
72
72
|
import { createGenericOptimisticLockReader } from './optimistic-lock'
|
|
73
73
|
import { registerOptimisticLockReaderIfAbsent } from './optimistic-lock-store'
|
|
74
|
+
import { createLogger } from '../logger'
|
|
74
75
|
|
|
75
76
|
type RbacServiceLike = {
|
|
76
77
|
getGrantedFeatures: (userId: string, opts: { tenantId: string | null; organizationId: string | null }) => Promise<string[]>
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
const logger = createLogger('shared').child({ component: 'crud' })
|
|
81
|
+
|
|
79
82
|
function resolveSortParams(queryParams: Record<string, unknown>) {
|
|
80
83
|
const rawSortField = queryParams.sortField ?? queryParams.sort ?? 'id'
|
|
81
84
|
const rawSortDir = queryParams.sortDir ?? queryParams.order ?? 'asc'
|
|
@@ -544,8 +547,7 @@ function handleError(err: unknown): Response {
|
|
|
544
547
|
|
|
545
548
|
const message = err instanceof Error ? err.message : undefined
|
|
546
549
|
const stack = err instanceof Error ? err.stack : undefined
|
|
547
|
-
|
|
548
|
-
console.error('[crud] unexpected error', { message, stack, err })
|
|
550
|
+
logger.error('Unexpected CRUD error', { message, stack, err })
|
|
549
551
|
const body: Record<string, unknown> = {
|
|
550
552
|
error: 'Internal server error',
|
|
551
553
|
message: 'Something went wrong. Please try again later.',
|
|
@@ -615,7 +617,7 @@ async function runGuardAfterSuccessCallbacks(
|
|
|
615
617
|
try {
|
|
616
618
|
await guard.afterSuccess!({ ...base, metadata: guardMeta })
|
|
617
619
|
} catch (error) {
|
|
618
|
-
|
|
620
|
+
logger.error('Mutation guard afterSuccess failed', { guardId: guard.id, err: error })
|
|
619
621
|
}
|
|
620
622
|
}
|
|
621
623
|
}
|
|
@@ -705,8 +707,7 @@ export async function flushPendingCrudAccessLogs(): Promise<void> {
|
|
|
705
707
|
|
|
706
708
|
function logForbidden(details: Record<string, unknown>) {
|
|
707
709
|
try {
|
|
708
|
-
|
|
709
|
-
console.warn('[crud] Forbidden request', details)
|
|
710
|
+
logger.warn('Forbidden request', details)
|
|
710
711
|
} catch {}
|
|
711
712
|
}
|
|
712
713
|
|
|
@@ -824,7 +825,7 @@ export async function logCrudAccess(options: LogCrudAccessOptions): Promise<LogC
|
|
|
824
825
|
payloads.map((payload) =>
|
|
825
826
|
Promise.resolve(service.log(payload)).catch((err) => {
|
|
826
827
|
try {
|
|
827
|
-
|
|
828
|
+
logger.error('Failed to record access log', { err, payload })
|
|
828
829
|
} catch {}
|
|
829
830
|
return undefined
|
|
830
831
|
}),
|
|
@@ -833,7 +834,7 @@ export async function logCrudAccess(options: LogCrudAccessOptions): Promise<LogC
|
|
|
833
834
|
}
|
|
834
835
|
} catch (err) {
|
|
835
836
|
try {
|
|
836
|
-
|
|
837
|
+
logger.error('Failed to record access logs (batch)', { err, count: payloads.length })
|
|
837
838
|
} catch {}
|
|
838
839
|
}
|
|
839
840
|
})()
|
|
@@ -1085,7 +1086,7 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1085
1086
|
})
|
|
1086
1087
|
return decoratedItems
|
|
1087
1088
|
} catch (err) {
|
|
1088
|
-
|
|
1089
|
+
logger.warn('Failed to decorate custom fields', { err })
|
|
1089
1090
|
endProfile({
|
|
1090
1091
|
result: 'error',
|
|
1091
1092
|
entityIds: entityIds.length,
|
|
@@ -1709,7 +1710,7 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1709
1710
|
}
|
|
1710
1711
|
}
|
|
1711
1712
|
} catch (err) {
|
|
1712
|
-
|
|
1713
|
+
logger.warn('Translation overlay failed', { err })
|
|
1713
1714
|
}
|
|
1714
1715
|
profiler.mark('translation_overlays_complete', { itemCount: transformedItems.length })
|
|
1715
1716
|
}
|
|
@@ -1940,7 +1941,7 @@ export function makeCrudRoute<TCreate = any, TUpdate = any, TList = any>(opts: C
|
|
|
1940
1941
|
}
|
|
1941
1942
|
}
|
|
1942
1943
|
} catch (err) {
|
|
1943
|
-
|
|
1944
|
+
logger.warn('Translation overlay (fallback) failed', { err })
|
|
1944
1945
|
}
|
|
1945
1946
|
profiler.mark('fallback_translation_overlays_complete', { itemCount: Array.isArray(list) ? list.length : 0 })
|
|
1946
1947
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ApiInterceptor, ApiInterceptorMethod, ApiInterceptorRegistryEntry } from './api-interceptor'
|
|
2
2
|
import { applyApiInterceptorOverridesToEntries } from '../../modules/overrides'
|
|
3
|
+
import { createLogger } from '../logger'
|
|
4
|
+
|
|
5
|
+
const logger = createLogger('shared').child({ component: 'umes' })
|
|
3
6
|
|
|
4
7
|
let _interceptorEntries: ApiInterceptorRegistryEntry[] | null = null
|
|
5
8
|
const GLOBAL_INTERCEPTOR_KEY = '__openMercatoApiInterceptors__'
|
|
@@ -80,9 +83,7 @@ export function getApiInterceptorsForRoute(routePath: string, method: ApiInterce
|
|
|
80
83
|
const warningKey = `${routePath}:${method}:${prev.interceptor.id}:${current.interceptor.id}:${currentPriority}`
|
|
81
84
|
if (collisionWarnings.has(warningKey)) continue
|
|
82
85
|
collisionWarnings.add(warningKey)
|
|
83
|
-
|
|
84
|
-
`[UMES] Interceptors "${prev.interceptor.id}" and "${current.interceptor.id}" have the same priority (${currentPriority}) for route "${routePath}". Execution order is based on module registration order.`
|
|
85
|
-
)
|
|
86
|
+
logger.warn('Interceptors share the same priority for route — execution order is based on module registration order', { firstInterceptorId: prev.interceptor.id, secondInterceptorId: current.interceptor.id, priority: currentPriority, routePath })
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import { createLogger } from '../logger'
|
|
3
|
+
|
|
4
|
+
const logger = createLogger('shared').child({ component: 'crud' })
|
|
2
5
|
|
|
3
6
|
export type CrudMutationGuardValidationSuccess = {
|
|
4
7
|
ok: true
|
|
@@ -94,12 +97,12 @@ export async function runCrudMutationGuardAfterSuccess(
|
|
|
94
97
|
try {
|
|
95
98
|
await service.afterMutationSuccess(input)
|
|
96
99
|
} catch (error) {
|
|
97
|
-
|
|
100
|
+
logger.error('Mutation guard after-success hook failed', {
|
|
98
101
|
resourceKind: input.resourceKind,
|
|
99
102
|
resourceId: input.resourceId,
|
|
100
103
|
operation: input.operation,
|
|
101
104
|
requestMethod: input.requestMethod,
|
|
102
|
-
|
|
105
|
+
err: error,
|
|
103
106
|
})
|
|
104
107
|
}
|
|
105
108
|
}
|
|
@@ -44,6 +44,9 @@ import {
|
|
|
44
44
|
type OptimisticLockConflictBody,
|
|
45
45
|
} from './optimistic-lock-headers'
|
|
46
46
|
import { getAllOptimisticLockReaders } from './optimistic-lock-store'
|
|
47
|
+
import { createLogger } from '../logger'
|
|
48
|
+
|
|
49
|
+
const logger = createLogger('shared').child({ component: 'optimistic-lock' })
|
|
47
50
|
|
|
48
51
|
export type OptimisticLockConfig =
|
|
49
52
|
| { mode: 'off' }
|
|
@@ -194,12 +197,7 @@ export function createGenericOptimisticLockReader(
|
|
|
194
197
|
// misconfig filtering on a column the table lacks), which silently disables
|
|
195
198
|
// locking for `resourceKind`. Log loudly so the misconfig is visible.
|
|
196
199
|
// Control flow stays fail-open (return null) to honor the no-500 contract.
|
|
197
|
-
|
|
198
|
-
console.error(
|
|
199
|
-
`[optimistic-lock] reader query failed for resourceKind="${resourceKind}" — optimistic locking is DISABLED for this entity until fixed. ` +
|
|
200
|
-
`Most likely a softDeleteField/tenantField/orgField filters on a column the table does not have.`,
|
|
201
|
-
err,
|
|
202
|
-
)
|
|
200
|
+
logger.error('Reader query failed — optimistic locking is DISABLED for this entity until fixed; most likely a softDeleteField/tenantField/orgField filters on a column the table does not have', { resourceKind, err })
|
|
203
201
|
return null
|
|
204
202
|
}
|
|
205
203
|
}
|
|
@@ -350,8 +348,7 @@ export function createOptimisticLockGuardService(
|
|
|
350
348
|
|
|
351
349
|
if (currentIso === expectedIso) {
|
|
352
350
|
if (debugEnabled) {
|
|
353
|
-
|
|
354
|
-
console.log('[optimistic-lock] match', {
|
|
351
|
+
logger.info('Optimistic lock match', {
|
|
355
352
|
resourceKind: input.resourceKind,
|
|
356
353
|
resourceId: input.resourceId,
|
|
357
354
|
operation: input.operation,
|
|
@@ -363,8 +360,7 @@ export function createOptimisticLockGuardService(
|
|
|
363
360
|
}
|
|
364
361
|
|
|
365
362
|
if (debugEnabled) {
|
|
366
|
-
|
|
367
|
-
console.log('[optimistic-lock] CONFLICT', {
|
|
363
|
+
logger.info('Optimistic lock conflict', {
|
|
368
364
|
resourceKind: input.resourceKind,
|
|
369
365
|
resourceId: input.resourceId,
|
|
370
366
|
operation: input.operation,
|
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
type MutationGuard,
|
|
6
6
|
} from './mutation-guard-registry'
|
|
7
7
|
import { getAllMutationGuardInstances } from './mutation-guard-store'
|
|
8
|
+
import { createLogger } from '../logger'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('shared').child({ component: 'crud' })
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Shared registry-based mutation-guard wrapper for custom write routes that do
|
|
@@ -173,7 +176,7 @@ export async function runRouteMutationGuards(params: {
|
|
|
173
176
|
metadata,
|
|
174
177
|
})
|
|
175
178
|
} catch (error) {
|
|
176
|
-
|
|
179
|
+
logger.error('Mutation guard afterSuccess failed', { guardId: guard.id, err: error })
|
|
177
180
|
}
|
|
178
181
|
}
|
|
179
182
|
},
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { SyncCrudEventPayload } from './sync-event-types'
|
|
2
2
|
import type { SyncSubscriberEntry } from './sync-subscriber-store'
|
|
3
3
|
import { matchWildcardPattern } from '@open-mercato/shared/lib/patterns/wildcard'
|
|
4
|
+
import { createLogger } from '../logger'
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('shared').child({ component: 'crud' })
|
|
4
7
|
|
|
5
8
|
// ---------------------------------------------------------------------------
|
|
6
9
|
// Event pattern matching
|
|
@@ -63,7 +66,7 @@ export async function runSyncAfterEvent(
|
|
|
63
66
|
try {
|
|
64
67
|
await subscriber.handler(payload, ctx)
|
|
65
68
|
} catch (error) {
|
|
66
|
-
|
|
69
|
+
logger.error('Sync-event after-subscriber failed', { subscriberId: subscriber.metadata.id, err: error })
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
}
|
|
@@ -2,6 +2,21 @@ import type { AwilixContainer } from 'awilix'
|
|
|
2
2
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
3
|
import { DefaultDataEngine, __resetUndeclaredEventWarningsForTests } from '../engine'
|
|
4
4
|
import { createModuleEvents, registerEventModuleConfigs } from '../../../modules/events'
|
|
5
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
6
|
+
|
|
7
|
+
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
8
|
+
const mocked = {
|
|
9
|
+
debug: jest.fn(),
|
|
10
|
+
info: jest.fn(),
|
|
11
|
+
warn: jest.fn(),
|
|
12
|
+
error: jest.fn(),
|
|
13
|
+
child: jest.fn(),
|
|
14
|
+
}
|
|
15
|
+
mocked.child.mockImplementation(() => mocked)
|
|
16
|
+
return { createLogger: jest.fn(() => mocked) }
|
|
17
|
+
})
|
|
18
|
+
const loggerWarn = createLogger('shared').warn as jest.Mock
|
|
19
|
+
|
|
5
20
|
|
|
6
21
|
const testEvents = [
|
|
7
22
|
{ id: 'issue1421_test.widget.created', label: 'Widget Created', entity: 'widget', category: 'crud' as const },
|
|
@@ -41,7 +56,7 @@ describe('DataEngine event contract validation (issue #1421)', () => {
|
|
|
41
56
|
})
|
|
42
57
|
|
|
43
58
|
it('emits declared events without warning', async () => {
|
|
44
|
-
|
|
59
|
+
loggerWarn.mockClear()
|
|
45
60
|
try {
|
|
46
61
|
const { engine, emitted } = makeFixture()
|
|
47
62
|
|
|
@@ -55,9 +70,8 @@ describe('DataEngine event contract validation (issue #1421)', () => {
|
|
|
55
70
|
expect(emitted).toEqual([
|
|
56
71
|
expect.objectContaining({ name: 'issue1421_test.widget.created' }),
|
|
57
72
|
])
|
|
58
|
-
expect(
|
|
73
|
+
expect(loggerWarn).not.toHaveBeenCalled()
|
|
59
74
|
} finally {
|
|
60
|
-
warnSpy.mockRestore()
|
|
61
75
|
}
|
|
62
76
|
})
|
|
63
77
|
|
|
@@ -78,7 +92,7 @@ describe('DataEngine event contract validation (issue #1421)', () => {
|
|
|
78
92
|
},
|
|
79
93
|
])
|
|
80
94
|
|
|
81
|
-
|
|
95
|
+
loggerWarn.mockClear()
|
|
82
96
|
try {
|
|
83
97
|
const { engine, emitted } = makeFixture()
|
|
84
98
|
|
|
@@ -92,14 +106,13 @@ describe('DataEngine event contract validation (issue #1421)', () => {
|
|
|
92
106
|
expect(emitted).toEqual([
|
|
93
107
|
expect.objectContaining({ name: 'issue1421_bootstrap.widget.deleted' }),
|
|
94
108
|
])
|
|
95
|
-
expect(
|
|
109
|
+
expect(loggerWarn).not.toHaveBeenCalled()
|
|
96
110
|
} finally {
|
|
97
|
-
warnSpy.mockRestore()
|
|
98
111
|
}
|
|
99
112
|
})
|
|
100
113
|
|
|
101
114
|
it('warns when emitting an event that is not registered', async () => {
|
|
102
|
-
|
|
115
|
+
loggerWarn.mockClear()
|
|
103
116
|
try {
|
|
104
117
|
const { engine, emitted } = makeFixture()
|
|
105
118
|
|
|
@@ -110,22 +123,21 @@ describe('DataEngine event contract validation (issue #1421)', () => {
|
|
|
110
123
|
events: { module: 'issue1421_unregistered', entity: 'ghost' },
|
|
111
124
|
})
|
|
112
125
|
|
|
113
|
-
expect(
|
|
114
|
-
const warningMessage =
|
|
115
|
-
expect(warningMessage).toContain('
|
|
116
|
-
expect(
|
|
126
|
+
expect(loggerWarn).toHaveBeenCalledTimes(1)
|
|
127
|
+
const [warningMessage, warningFields] = loggerWarn.mock.calls[0] ?? []
|
|
128
|
+
expect(String(warningMessage)).toContain('events.ts')
|
|
129
|
+
expect(warningFields).toEqual(expect.objectContaining({ eventName: 'issue1421_unregistered.ghost.created' }))
|
|
117
130
|
|
|
118
131
|
// Emission is still attempted (non-strict), matching the factory's default behavior
|
|
119
132
|
expect(emitted).toEqual([
|
|
120
133
|
expect.objectContaining({ name: 'issue1421_unregistered.ghost.created' }),
|
|
121
134
|
])
|
|
122
135
|
} finally {
|
|
123
|
-
warnSpy.mockRestore()
|
|
124
136
|
}
|
|
125
137
|
})
|
|
126
138
|
|
|
127
139
|
it('deduplicates repeated warnings for the same undeclared event', async () => {
|
|
128
|
-
|
|
140
|
+
loggerWarn.mockClear()
|
|
129
141
|
try {
|
|
130
142
|
const { engine } = makeFixture()
|
|
131
143
|
|
|
@@ -138,9 +150,8 @@ describe('DataEngine event contract validation (issue #1421)', () => {
|
|
|
138
150
|
})
|
|
139
151
|
}
|
|
140
152
|
|
|
141
|
-
expect(
|
|
153
|
+
expect(loggerWarn).toHaveBeenCalledTimes(1)
|
|
142
154
|
} finally {
|
|
143
|
-
warnSpy.mockRestore()
|
|
144
155
|
}
|
|
145
156
|
})
|
|
146
157
|
})
|
package/src/lib/data/engine.ts
CHANGED
|
@@ -18,6 +18,9 @@ import { getEntityIds } from '../encryption/entityIds'
|
|
|
18
18
|
import { normalizeCustomFieldValues } from '../custom-fields/normalize'
|
|
19
19
|
import { parseBooleanToken } from '../boolean'
|
|
20
20
|
import { isEventDeclared } from '../../modules/events'
|
|
21
|
+
import { createLogger } from '../logger'
|
|
22
|
+
|
|
23
|
+
const logger = createLogger('shared').child({ component: 'data-engine' })
|
|
21
24
|
|
|
22
25
|
const undeclaredEventWarned = new Set<string>()
|
|
23
26
|
|
|
@@ -25,10 +28,7 @@ function warnIfUndeclaredEvent(eventName: string, context: string): void {
|
|
|
25
28
|
if (isEventDeclared(eventName)) return
|
|
26
29
|
if (undeclaredEventWarned.has(eventName)) return
|
|
27
30
|
undeclaredEventWarned.add(eventName)
|
|
28
|
-
|
|
29
|
-
`[data-engine] ${context} is emitting undeclared event "${eventName}". ` +
|
|
30
|
-
`Declare it in the owning module's events.ts (createModuleEvents) so the event registry stays authoritative.`,
|
|
31
|
-
)
|
|
31
|
+
logger.warn('Emitting undeclared event — declare it in the owning module events.ts (createModuleEvents) so the event registry stays authoritative', { context, eventName })
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** Internal: clear the undeclared-event warning cache. Exposed for tests. */
|
|
@@ -618,7 +618,7 @@ export class DefaultDataEngine implements DataEngine {
|
|
|
618
618
|
// defers the coverage recompute + fulltext delete, so this stays bounded.
|
|
619
619
|
// Errors are logged, not thrown — index drift never fails the originating write.
|
|
620
620
|
await bus.emitEvent('query_index.delete_one', enrichedPayload).catch((err: unknown) => {
|
|
621
|
-
|
|
621
|
+
logger.error('query_index.delete_one emit failed', { err })
|
|
622
622
|
})
|
|
623
623
|
} else {
|
|
624
624
|
const payload = indexer.buildUpsertPayload
|
|
@@ -638,7 +638,7 @@ export class DefaultDataEngine implements DataEngine {
|
|
|
638
638
|
// and defers the heavy token-reindex pipeline (build doc + encrypt + decrypt +
|
|
639
639
|
// tokenize + DELETE + chunked INSERT) so write latency stays bounded.
|
|
640
640
|
await bus.emitEvent('query_index.upsert_one', enrichedPayload).catch((err: unknown) => {
|
|
641
|
-
|
|
641
|
+
logger.error('query_index.upsert_one emit failed', { err })
|
|
642
642
|
})
|
|
643
643
|
}
|
|
644
644
|
|
package/src/lib/db/mikro.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { MikroORM } from '@mikro-orm/core'
|
|
|
4
4
|
import { ReflectMetadataProvider } from '@mikro-orm/decorators/legacy'
|
|
5
5
|
import { PostgreSqlDriver, type EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'
|
|
6
6
|
import { getSslConfig } from './ssl'
|
|
7
|
+
import { createLogger } from '../logger'
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('shared').child({ component: 'orm' })
|
|
7
10
|
|
|
8
11
|
export type AppMikroORM = MikroORM<PostgreSqlDriver, PostgreSqlEntityManager<PostgreSqlDriver>>
|
|
9
12
|
|
|
@@ -22,7 +25,7 @@ function setRegisteredEntities(entities: any[]): void {
|
|
|
22
25
|
|
|
23
26
|
export function registerOrmEntities(entities: any[]) {
|
|
24
27
|
if (getRegisteredEntities() !== null && process.env.NODE_ENV === 'development') {
|
|
25
|
-
|
|
28
|
+
logger.debug('ORM entities re-registered (this may occur during HMR)')
|
|
26
29
|
}
|
|
27
30
|
setRegisteredEntities(entities)
|
|
28
31
|
}
|
|
@@ -106,7 +109,7 @@ export async function getOrm() {
|
|
|
106
109
|
const sslConfig = getSslConfig()
|
|
107
110
|
|
|
108
111
|
if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {
|
|
109
|
-
|
|
112
|
+
logger.info('Pool config', {
|
|
110
113
|
poolMin,
|
|
111
114
|
poolMax,
|
|
112
115
|
poolIdleTimeout,
|
|
@@ -150,7 +153,7 @@ export async function getOrm() {
|
|
|
150
153
|
ssl: sslConfig,
|
|
151
154
|
onPoolCreated: (pool: any) => {
|
|
152
155
|
if (process.env.OM_DB_POOL_DEBUG === '1' || process.env.OM_INTEGRATION_TEST === 'true') {
|
|
153
|
-
|
|
156
|
+
logger.info('pg pool created with options', {
|
|
154
157
|
max: pool.options?.max,
|
|
155
158
|
min: pool.options?.min,
|
|
156
159
|
idleTimeoutMillis: pool.options?.idleTimeoutMillis,
|
package/src/lib/di/container.ts
CHANGED
|
@@ -9,6 +9,9 @@ import { applyDiOverridesToContainer } from '@open-mercato/shared/modules/overri
|
|
|
9
9
|
import { createOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock'
|
|
10
10
|
import { getAllOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'
|
|
11
11
|
import { createCommandOptimisticLockGuardService } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
12
|
+
import { createLogger } from '../logger'
|
|
13
|
+
|
|
14
|
+
const logger = createLogger('shared').child({ component: 'di' })
|
|
12
15
|
|
|
13
16
|
type DynamicCradle = Record<string, any>
|
|
14
17
|
|
|
@@ -107,7 +110,7 @@ function setGlobalRegistrars(registrars: DiRegistrar[]): void {
|
|
|
107
110
|
export function registerDiRegistrars(registrars: DiRegistrar[]) {
|
|
108
111
|
const existing = getGlobalRegistrars()
|
|
109
112
|
if (existing !== null && process.env.NODE_ENV === 'development') {
|
|
110
|
-
|
|
113
|
+
logger.debug('DI registrars re-registered (this may occur during HMR)')
|
|
111
114
|
}
|
|
112
115
|
setGlobalRegistrars(registrars)
|
|
113
116
|
// Force re-bootstrap on HMR — module subscribers may have changed.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
2
|
import { isEncryptionDebugEnabled } from './toggles'
|
|
3
|
+
import { createLogger } from '../logger'
|
|
4
|
+
|
|
5
|
+
const logger = createLogger('shared').child({ component: 'encryption' })
|
|
3
6
|
|
|
4
7
|
export type EncryptionPayload = {
|
|
5
8
|
value: string | null
|
|
@@ -31,8 +34,7 @@ export function generateDek(): string {
|
|
|
31
34
|
function logDebug(event: string, payload: Record<string, unknown>) {
|
|
32
35
|
if (!isEncryptionDebugEnabled()) return
|
|
33
36
|
try {
|
|
34
|
-
|
|
35
|
-
console.debug('[encryption]', event, payload)
|
|
37
|
+
logger.debug(event, payload)
|
|
36
38
|
} catch {
|
|
37
39
|
// ignore
|
|
38
40
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '../logger'
|
|
2
|
+
|
|
3
|
+
const logger = createLogger('shared').child({ component: 'encryption' })
|
|
4
|
+
|
|
1
5
|
// Registration pattern for entity fields (for Turbopack compatibility)
|
|
2
6
|
export type EntityFieldsRegistry = Record<string, Record<string, string>>
|
|
3
7
|
|
|
@@ -5,7 +9,7 @@ let _entityFieldsRegistry: EntityFieldsRegistry | null = null
|
|
|
5
9
|
|
|
6
10
|
export function registerEntityFields(registry: EntityFieldsRegistry) {
|
|
7
11
|
if (_entityFieldsRegistry !== null && process.env.NODE_ENV === 'development') {
|
|
8
|
-
|
|
12
|
+
logger.debug('Entity fields re-registered (this may occur during HMR)')
|
|
9
13
|
}
|
|
10
14
|
_entityFieldsRegistry = registry
|
|
11
15
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { EntityMetadata } from '@mikro-orm/core'
|
|
2
|
+
import { createLogger } from '../logger'
|
|
3
|
+
|
|
4
|
+
const logger = createLogger('shared').child({ component: 'encryption' })
|
|
2
5
|
|
|
3
6
|
// Registration pattern for publishable packages
|
|
4
7
|
export type EntityIds = Record<string, Record<string, string>>
|
|
@@ -8,7 +11,7 @@ let _entityIdLookup: Map<string, string> | null = null
|
|
|
8
11
|
|
|
9
12
|
export function registerEntityIds(E: EntityIds) {
|
|
10
13
|
if (_entityIds !== null && process.env.NODE_ENV === 'development') {
|
|
11
|
-
|
|
14
|
+
logger.debug('Entity IDs re-registered (this may occur during HMR)')
|
|
12
15
|
}
|
|
13
16
|
_entityIds = E
|
|
14
17
|
_entityIdLookup = null // Reset cache on re-registration
|
|
@@ -2,8 +2,11 @@ import crypto from 'node:crypto'
|
|
|
2
2
|
import { generateDek, hashForLookup } from './aes'
|
|
3
3
|
import { isEncryptionDebugEnabled, isTenantDataEncryptionEnabled } from './toggles'
|
|
4
4
|
import { parseBooleanToken } from '../boolean'
|
|
5
|
+
import { createLogger } from '../logger'
|
|
5
6
|
import { fetchWithTimeout, resolveTimeoutMs } from '../http/fetchWithTimeout'
|
|
6
7
|
|
|
8
|
+
const logger = createLogger('shared').child({ component: 'kms' })
|
|
9
|
+
|
|
7
10
|
const DEFAULT_VAULT_REQUEST_TIMEOUT_MS = 1_000
|
|
8
11
|
const DEFAULT_VAULT_RECOVERY_COOLDOWN_MS = 30_000
|
|
9
12
|
|
|
@@ -54,9 +57,7 @@ class FallbackKmsService implements KmsService {
|
|
|
54
57
|
try {
|
|
55
58
|
return await op()
|
|
56
59
|
} catch (err) {
|
|
57
|
-
|
|
58
|
-
error: (err as Error)?.message || String(err),
|
|
59
|
-
})
|
|
60
|
+
logger.warn('Primary KMS failed, will try fallback', { err })
|
|
60
61
|
return null
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -199,13 +200,13 @@ export class HashicorpVaultKmsService implements KmsService {
|
|
|
199
200
|
this.healthy = false
|
|
200
201
|
this.misconfigured = true
|
|
201
202
|
if (this.debugEnabled) {
|
|
202
|
-
|
|
203
|
+
logger.warn('Vault misconfigured (missing VAULT_ADDR or VAULT_TOKEN)')
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
if (this.healthy && !HashicorpVaultKmsService.loggedInit && this.debugEnabled) {
|
|
206
207
|
HashicorpVaultKmsService.loggedInit = true
|
|
207
|
-
if(this.debugEnabled) {
|
|
208
|
-
|
|
208
|
+
if (this.debugEnabled) {
|
|
209
|
+
logger.info('Hashicorp Vault KMS enabled')
|
|
209
210
|
}
|
|
210
211
|
}
|
|
211
212
|
}
|
|
@@ -267,21 +268,17 @@ export class HashicorpVaultKmsService implements KmsService {
|
|
|
267
268
|
// not-yet-created tenant DEK is the normal read-before-write path.
|
|
268
269
|
if (res.status >= 500) this.markTransientFailure()
|
|
269
270
|
else this.markHealthy()
|
|
270
|
-
|
|
271
|
+
logger.warn('Vault read failed', { path, status: res.status })
|
|
271
272
|
return null
|
|
272
273
|
}
|
|
273
274
|
this.markHealthy()
|
|
274
275
|
if (this.debugEnabled) {
|
|
275
|
-
|
|
276
|
+
logger.info('Vault read ok', { path })
|
|
276
277
|
}
|
|
277
278
|
return (await res.json()) as VaultReadResponse
|
|
278
279
|
} catch (err) {
|
|
279
280
|
this.markTransientFailure()
|
|
280
|
-
|
|
281
|
-
path,
|
|
282
|
-
error: (err as Error)?.message || String(err),
|
|
283
|
-
timeoutMs: this.requestTimeoutMs,
|
|
284
|
-
})
|
|
281
|
+
logger.warn('Vault read error', { path, err, timeoutMs: this.requestTimeoutMs })
|
|
285
282
|
return null
|
|
286
283
|
}
|
|
287
284
|
}
|
|
@@ -313,19 +310,15 @@ export class HashicorpVaultKmsService implements KmsService {
|
|
|
313
310
|
// not an unhealthy Vault — Vault is reachable, so close the breaker.
|
|
314
311
|
if (typeof opts?.cas === 'number' && res.status === 400) {
|
|
315
312
|
this.markHealthy()
|
|
316
|
-
|
|
313
|
+
logger.warn('Vault write CAS conflict (concurrent DEK create)', { path, status: res.status })
|
|
317
314
|
return 'conflict'
|
|
318
315
|
}
|
|
319
316
|
this.markTransientFailure()
|
|
320
|
-
|
|
317
|
+
logger.warn('Vault write failed', { path, status: res.status })
|
|
321
318
|
return 'error'
|
|
322
319
|
} catch (err) {
|
|
323
320
|
this.markTransientFailure()
|
|
324
|
-
|
|
325
|
-
path,
|
|
326
|
-
error: (err as Error)?.message || String(err),
|
|
327
|
-
timeoutMs: this.requestTimeoutMs,
|
|
328
|
-
})
|
|
321
|
+
logger.warn('Vault write error', { path, err, timeoutMs: this.requestTimeoutMs })
|
|
329
322
|
return 'error'
|
|
330
323
|
}
|
|
331
324
|
}
|
|
@@ -348,7 +341,7 @@ export class HashicorpVaultKmsService implements KmsService {
|
|
|
348
341
|
const res = await this.readVault(path)
|
|
349
342
|
const key = res?.data?.data?.key
|
|
350
343
|
if (!key) {
|
|
351
|
-
|
|
344
|
+
logger.warn('No tenant DEK found in Vault', { tenantId, path })
|
|
352
345
|
return null
|
|
353
346
|
}
|
|
354
347
|
const dek: TenantDek = { tenantId, key, fetchedAt: this.now() }
|
|
@@ -371,7 +364,7 @@ export class HashicorpVaultKmsService implements KmsService {
|
|
|
371
364
|
const key = generateDek()
|
|
372
365
|
const outcome = await this.writeVault(path, key, { cas: 0 })
|
|
373
366
|
if (outcome === 'ok') {
|
|
374
|
-
|
|
367
|
+
logger.info('Stored tenant DEK in Vault', { tenantId, path })
|
|
375
368
|
return this.remember({ tenantId, key, fetchedAt: this.now() })
|
|
376
369
|
}
|
|
377
370
|
if (outcome === 'conflict') {
|
|
@@ -380,11 +373,11 @@ export class HashicorpVaultKmsService implements KmsService {
|
|
|
380
373
|
const winner = await this.readVault(path)
|
|
381
374
|
const winnerKey = winner?.data?.data?.key
|
|
382
375
|
if (winnerKey) {
|
|
383
|
-
|
|
376
|
+
logger.info('Adopted concurrently-created tenant DEK', { tenantId, path })
|
|
384
377
|
return this.remember({ tenantId, key: winnerKey, fetchedAt: this.now() })
|
|
385
378
|
}
|
|
386
379
|
}
|
|
387
|
-
|
|
380
|
+
logger.warn('Failed to store tenant DEK in Vault', { tenantId, path })
|
|
388
381
|
return null
|
|
389
382
|
}
|
|
390
383
|
|
|
@@ -419,12 +412,15 @@ function logDerivedKeyFallbackBanner(opts: DerivedSecret): void {
|
|
|
419
412
|
const width = 110
|
|
420
413
|
const border = `${redBg}${white}${'━'.repeat(width)}${reset}`
|
|
421
414
|
const body = buildDerivedKeyFallbackBannerLines(opts)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
415
|
+
const bannerLines = [
|
|
416
|
+
border,
|
|
417
|
+
...body.map((line) => `${redBg}${white} ${line.padEnd(width - 2, ' ')} ${reset}`),
|
|
418
|
+
border,
|
|
419
|
+
]
|
|
420
|
+
process.stderr.write(bannerLines.join('\n') + '\n')
|
|
421
|
+
logger.warn('Using derived tenant encryption keys (Vault unavailable / no DEK)', {
|
|
422
|
+
secretFingerprint: fingerprintSecret(opts.secret),
|
|
423
|
+
})
|
|
428
424
|
}
|
|
429
425
|
|
|
430
426
|
export function createKmsService(): KmsService {
|
|
@@ -444,9 +440,7 @@ export function createKmsService(): KmsService {
|
|
|
444
440
|
notifyFallback?.()
|
|
445
441
|
return fallback
|
|
446
442
|
}
|
|
447
|
-
|
|
448
|
-
'⚠️ [encryption][kms] Vault not healthy or misconfigured (missing VAULT_ADDR/VAULT_TOKEN) and no fallback secret provided; falling back to noop KMS',
|
|
449
|
-
)
|
|
443
|
+
logger.warn('Vault not healthy or misconfigured (missing VAULT_ADDR/VAULT_TOKEN) and no fallback secret provided; falling back to noop KMS')
|
|
450
444
|
return new NoopKmsService()
|
|
451
445
|
}
|
|
452
446
|
|
|
@@ -5,6 +5,9 @@ import { TenantDataEncryptionService, parseDecryptedFieldValue } from './tenantD
|
|
|
5
5
|
import { isTenantDataEncryptionEnabled } from './toggles'
|
|
6
6
|
import { isEncryptionDebugEnabled } from './toggles'
|
|
7
7
|
import { resolveTenantEncryptionService } from './customFieldValues'
|
|
8
|
+
import { createLogger } from '../logger'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('shared').child({ component: 'encryption' })
|
|
8
11
|
|
|
9
12
|
type Scoped = {
|
|
10
13
|
tenantId?: string | null
|
|
@@ -29,8 +32,7 @@ function resolveScope(entity: Scoped): Scope {
|
|
|
29
32
|
function debug(event: string, payload: Record<string, unknown>) {
|
|
30
33
|
if (!isEncryptionDebugEnabled()) return
|
|
31
34
|
try {
|
|
32
|
-
|
|
33
|
-
console.debug(event, payload)
|
|
35
|
+
logger.debug(event, payload)
|
|
34
36
|
} catch {
|
|
35
37
|
// ignore
|
|
36
38
|
}
|