@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
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { withAtomicFlush } from '../flush'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
|
+
|
|
4
|
+
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
5
|
+
const mocked = {
|
|
6
|
+
debug: jest.fn(),
|
|
7
|
+
info: jest.fn(),
|
|
8
|
+
warn: jest.fn(),
|
|
9
|
+
error: jest.fn(),
|
|
10
|
+
child: jest.fn(),
|
|
11
|
+
}
|
|
12
|
+
mocked.child.mockImplementation(() => mocked)
|
|
13
|
+
return { createLogger: jest.fn(() => mocked) }
|
|
14
|
+
})
|
|
15
|
+
const loggerWarn = createLogger('shared').warn as jest.Mock
|
|
16
|
+
|
|
2
17
|
|
|
3
18
|
type FakeEntityManager = {
|
|
4
19
|
flush: jest.Mock<Promise<void>, []>
|
|
@@ -258,11 +273,10 @@ describe('withAtomicFlush', () => {
|
|
|
258
273
|
// after its own flush). The guard must persist it instead of letting the
|
|
259
274
|
// transaction commit the work-in-progress silently.
|
|
260
275
|
const em = createUowEm([{ entity: 'lingering' }], { inTransaction: false })
|
|
261
|
-
|
|
276
|
+
loggerWarn.mockClear()
|
|
262
277
|
try {
|
|
263
278
|
await withAtomicFlush(em as any, [() => {}], { transaction: true, label: 'demo.command' })
|
|
264
279
|
} finally {
|
|
265
|
-
warn.mockRestore()
|
|
266
280
|
}
|
|
267
281
|
|
|
268
282
|
// 1 per-phase flush + 1 defensive guard flush, all inside the same transaction.
|
|
@@ -275,36 +289,31 @@ describe('withAtomicFlush', () => {
|
|
|
275
289
|
const em = createUowEm([{ a: 1 }, { b: 2 }])
|
|
276
290
|
const previousEnv = process.env.NODE_ENV
|
|
277
291
|
process.env.NODE_ENV = 'development'
|
|
278
|
-
|
|
279
|
-
let warnCallCount = 0
|
|
280
|
-
let warnMessage = ''
|
|
292
|
+
loggerWarn.mockClear()
|
|
281
293
|
try {
|
|
282
294
|
await withAtomicFlush(em as any, [() => {}], { label: 'sales.update_shipment' })
|
|
283
|
-
// Capture BEFORE mockRestore — restore() resets mock.calls.
|
|
284
|
-
warnCallCount = warn.mock.calls.length
|
|
285
|
-
warnMessage = String(warn.mock.calls[0]?.[0] ?? '')
|
|
286
295
|
} finally {
|
|
287
|
-
warn.mockRestore()
|
|
288
296
|
process.env.NODE_ENV = previousEnv
|
|
289
297
|
}
|
|
290
298
|
|
|
291
|
-
expect(
|
|
292
|
-
expect(
|
|
293
|
-
|
|
299
|
+
expect(loggerWarn).toHaveBeenCalledTimes(1)
|
|
300
|
+
expect(loggerWarn).toHaveBeenCalledWith(
|
|
301
|
+
expect.stringContaining('flushed defensively'),
|
|
302
|
+
expect.objectContaining({ label: 'sales.update_shipment', pendingCount: 2 }),
|
|
303
|
+
)
|
|
294
304
|
})
|
|
295
305
|
|
|
296
306
|
it('does NOT flush again when the UnitOfWork is clean at the boundary', async () => {
|
|
297
307
|
const em = createUowEm([])
|
|
298
|
-
|
|
308
|
+
loggerWarn.mockClear()
|
|
299
309
|
try {
|
|
300
310
|
await withAtomicFlush(em as any, [() => {}, () => {}])
|
|
301
311
|
} finally {
|
|
302
|
-
warn.mockRestore()
|
|
303
312
|
}
|
|
304
313
|
|
|
305
314
|
// 2 phases → 2 per-phase flushes; the clean guard adds nothing.
|
|
306
315
|
expect(em.flush).toHaveBeenCalledTimes(2)
|
|
307
|
-
expect(
|
|
316
|
+
expect(loggerWarn).not.toHaveBeenCalled()
|
|
308
317
|
})
|
|
309
318
|
|
|
310
319
|
it('never throws when the UnitOfWork probe itself fails', async () => {
|
|
@@ -3,8 +3,20 @@ import { normalizeAuthorUserId } from '@open-mercato/shared/lib/commands/helpers
|
|
|
3
3
|
describe('normalizeAuthorUserId', () => {
|
|
4
4
|
const validUuid = 'a1b2c3d4-e5f6-1a2b-9c3d-4e5f6a7b8c9d'
|
|
5
5
|
|
|
6
|
-
it('
|
|
7
|
-
|
|
6
|
+
it('ignores explicit authorUserId for normal authenticated callers', () => {
|
|
7
|
+
const spoofedUuid = 'b1b2c3d4-e5f6-1a2b-9c3d-4e5f6a7b8c9d'
|
|
8
|
+
|
|
9
|
+
expect(normalizeAuthorUserId(spoofedUuid, { sub: validUuid })).toBe(validUuid)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('honors explicit authorUserId for super admins', () => {
|
|
13
|
+
const delegatedUuid = 'b1b2c3d4-e5f6-1a2b-9c3d-4e5f6a7b8c9d'
|
|
14
|
+
|
|
15
|
+
expect(normalizeAuthorUserId(delegatedUuid, { sub: validUuid, isSuperAdmin: true })).toBe(delegatedUuid)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('rejects non-UUID explicit authorUserId values for super admins', () => {
|
|
19
|
+
expect(normalizeAuthorUserId('not-a-uuid', { sub: validUuid, isSuperAdmin: true })).toBe(validUuid)
|
|
8
20
|
})
|
|
9
21
|
|
|
10
22
|
it('returns null when no explicit ID and auth is null', () => {
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import { commandRegistry, registerCommand, registerCommandLoaders } from '@open-mercato/shared/lib/commands'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
|
+
|
|
4
|
+
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
5
|
+
const mocked = {
|
|
6
|
+
debug: jest.fn(),
|
|
7
|
+
info: jest.fn(),
|
|
8
|
+
warn: jest.fn(),
|
|
9
|
+
error: jest.fn(),
|
|
10
|
+
child: jest.fn(),
|
|
11
|
+
}
|
|
12
|
+
mocked.child.mockImplementation(() => mocked)
|
|
13
|
+
return { createLogger: jest.fn(() => mocked) }
|
|
14
|
+
})
|
|
15
|
+
const loggerDebug = createLogger('shared').debug as jest.Mock
|
|
16
|
+
|
|
2
17
|
|
|
3
18
|
describe('command registry registration', () => {
|
|
4
19
|
const originalNodeEnv = process.env.NODE_ENV
|
|
@@ -27,7 +42,7 @@ describe('command registry registration', () => {
|
|
|
27
42
|
|
|
28
43
|
it('overwrites duplicate command ids in development to tolerate HMR re-evaluation', () => {
|
|
29
44
|
process.env.NODE_ENV = 'development'
|
|
30
|
-
|
|
45
|
+
loggerDebug.mockClear()
|
|
31
46
|
const firstExecute = jest.fn(async () => 'first')
|
|
32
47
|
const secondExecute = jest.fn(async () => 'second')
|
|
33
48
|
|
|
@@ -42,7 +57,7 @@ describe('command registry registration', () => {
|
|
|
42
57
|
})
|
|
43
58
|
|
|
44
59
|
expect(commandRegistry.get('test.command.hmr')?.execute).toBe(secondExecute)
|
|
45
|
-
expect(
|
|
60
|
+
expect(loggerDebug).toHaveBeenCalledWith('Commands re-registered (this may occur during HMR)')
|
|
46
61
|
})
|
|
47
62
|
|
|
48
63
|
it('loads a command handler on demand from a registered loader', async () => {
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
|
|
2
2
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
3
3
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
4
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
5
|
+
|
|
6
|
+
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
7
|
+
const mocked = {
|
|
8
|
+
debug: jest.fn(),
|
|
9
|
+
info: jest.fn(),
|
|
10
|
+
warn: jest.fn(),
|
|
11
|
+
error: jest.fn(),
|
|
12
|
+
child: jest.fn(),
|
|
13
|
+
}
|
|
14
|
+
mocked.child.mockImplementation(() => mocked)
|
|
15
|
+
return { createLogger: jest.fn(() => mocked) }
|
|
16
|
+
})
|
|
17
|
+
const loggerWarn = createLogger('shared').warn as jest.Mock
|
|
18
|
+
|
|
4
19
|
|
|
5
20
|
type ScopeShape = NonNullable<CommandRuntimeContext['organizationScope']>
|
|
6
21
|
|
|
@@ -154,17 +169,14 @@ describe('ensureOrganizationScope', () => {
|
|
|
154
169
|
describe('fail-open-by-omission hardening (#2441)', () => {
|
|
155
170
|
const originalNodeEnv = process.env.NODE_ENV
|
|
156
171
|
const originalStrict = process.env.OM_ENFORCE_ORG_SCOPE_STRICT
|
|
157
|
-
let warnSpy: jest.SpyInstance
|
|
158
|
-
|
|
159
172
|
beforeEach(() => {
|
|
160
173
|
// The scope loggers suppress output under NODE_ENV==='test'; flip it so the
|
|
161
174
|
// observability signal becomes assertable, then restore in afterEach.
|
|
162
175
|
process.env.NODE_ENV = 'development'
|
|
163
|
-
|
|
176
|
+
loggerWarn.mockClear()
|
|
164
177
|
})
|
|
165
178
|
|
|
166
179
|
afterEach(() => {
|
|
167
|
-
warnSpy.mockRestore()
|
|
168
180
|
if (originalNodeEnv === undefined) delete process.env.NODE_ENV
|
|
169
181
|
else process.env.NODE_ENV = originalNodeEnv
|
|
170
182
|
if (originalStrict === undefined) delete process.env.OM_ENFORCE_ORG_SCOPE_STRICT
|
|
@@ -175,8 +187,8 @@ describe('ensureOrganizationScope', () => {
|
|
|
175
187
|
delete process.env.OM_ENFORCE_ORG_SCOPE_STRICT
|
|
176
188
|
const ctx = buildCtx({ orgId: null, selectedOrganizationId: null, organizationScope: null })
|
|
177
189
|
expect(() => ensureOrganizationScope(ctx, 'org-b')).not.toThrow()
|
|
178
|
-
expect(
|
|
179
|
-
'
|
|
190
|
+
expect(loggerWarn).toHaveBeenCalledWith(
|
|
191
|
+
'Unscoped organization command executed without organization context',
|
|
180
192
|
expect.objectContaining({ targetOrganizationId: 'org-b', strictEnforcement: false })
|
|
181
193
|
)
|
|
182
194
|
})
|
|
@@ -185,8 +197,8 @@ describe('ensureOrganizationScope', () => {
|
|
|
185
197
|
process.env.OM_ENFORCE_ORG_SCOPE_STRICT = 'true'
|
|
186
198
|
const ctx = buildCtx({ orgId: null, selectedOrganizationId: null, organizationScope: null })
|
|
187
199
|
expect(() => ensureOrganizationScope(ctx, 'org-b')).toThrow(CrudHttpError)
|
|
188
|
-
expect(
|
|
189
|
-
'
|
|
200
|
+
expect(loggerWarn).toHaveBeenCalledWith(
|
|
201
|
+
'Unscoped organization command executed without organization context',
|
|
190
202
|
expect.objectContaining({ targetOrganizationId: 'org-b', strictEnforcement: true })
|
|
191
203
|
)
|
|
192
204
|
})
|
|
@@ -195,7 +207,7 @@ describe('ensureOrganizationScope', () => {
|
|
|
195
207
|
delete process.env.OM_ENFORCE_ORG_SCOPE_STRICT
|
|
196
208
|
const ctx = buildCtx({ orgId: null, selectedOrganizationId: null, organizationScope: null })
|
|
197
209
|
expect(() => ensureOrganizationScope(ctx, '')).not.toThrow()
|
|
198
|
-
expect(
|
|
210
|
+
expect(loggerWarn).not.toHaveBeenCalled()
|
|
199
211
|
})
|
|
200
212
|
})
|
|
201
213
|
})
|
|
@@ -30,6 +30,9 @@ import {
|
|
|
30
30
|
} from './command-interceptor-runner'
|
|
31
31
|
import type { CommandInterceptorContext } from './command-interceptor'
|
|
32
32
|
import { CommandInterceptorError } from './errors'
|
|
33
|
+
import { createLogger } from '../logger'
|
|
34
|
+
|
|
35
|
+
const logger = createLogger('shared').child({ component: 'commands' })
|
|
33
36
|
|
|
34
37
|
const SKIPPED_ACTION_LOG_RESOURCE_KINDS = new Set<string>([
|
|
35
38
|
'audit_logs.access',
|
|
@@ -624,7 +627,7 @@ export class CommandBus {
|
|
|
624
627
|
} catch (err) {
|
|
625
628
|
if (isCrudCacheDebugEnabled()) {
|
|
626
629
|
try {
|
|
627
|
-
|
|
630
|
+
logger.debug('Cache execute-invalidation failed', { commandId, err })
|
|
628
631
|
} catch {}
|
|
629
632
|
}
|
|
630
633
|
}
|
|
@@ -656,7 +659,7 @@ export class CommandBus {
|
|
|
656
659
|
} catch (err) {
|
|
657
660
|
if (isCrudCacheDebugEnabled()) {
|
|
658
661
|
try {
|
|
659
|
-
|
|
662
|
+
logger.debug('Cache undo-invalidation failed', { commandId: log.commandId, err })
|
|
660
663
|
} catch {}
|
|
661
664
|
}
|
|
662
665
|
}
|
|
@@ -4,6 +4,9 @@ import type {
|
|
|
4
4
|
CommandInterceptorUndoContext,
|
|
5
5
|
} from './command-interceptor'
|
|
6
6
|
import { hasAllFeatures } from '../../security/features'
|
|
7
|
+
import { createLogger } from '../logger'
|
|
8
|
+
|
|
9
|
+
const logger = createLogger('shared').child({ component: 'commands' })
|
|
7
10
|
|
|
8
11
|
// ---------------------------------------------------------------------------
|
|
9
12
|
// Command pattern matching
|
|
@@ -116,7 +119,7 @@ export async function runCommandInterceptorsAfter(
|
|
|
116
119
|
currentResult = { ...(currentResult as Record<string, unknown>), ...afterResult.modifiedResult }
|
|
117
120
|
}
|
|
118
121
|
} catch (error) {
|
|
119
|
-
|
|
122
|
+
logger.error('Command interceptor afterExecute failed', { interceptorId: interceptor.id, err: error })
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
125
|
|
|
@@ -185,7 +188,7 @@ export async function runCommandInterceptorsAfterUndo(
|
|
|
185
188
|
{ ...context, commandId, metadata: metadataByInterceptor.get(interceptor.id) },
|
|
186
189
|
)
|
|
187
190
|
} catch (error) {
|
|
188
|
-
|
|
191
|
+
logger.error('Command interceptor afterUndo failed', { interceptorId: interceptor.id, err: error })
|
|
189
192
|
}
|
|
190
193
|
}
|
|
191
194
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import type { IsolationLevel } from '@mikro-orm/core'
|
|
3
|
+
import { createLogger } from '../logger'
|
|
4
|
+
|
|
5
|
+
const logger = createLogger('shared').child({ component: 'commands' })
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Options controlling how {@link withAtomicFlush} executes its phases.
|
|
@@ -72,11 +75,7 @@ async function flushPendingChangesGuard(
|
|
|
72
75
|
if (pendingCount > 0) {
|
|
73
76
|
await em.flush()
|
|
74
77
|
if (process.env.NODE_ENV !== 'production') {
|
|
75
|
-
|
|
76
|
-
console.warn(
|
|
77
|
-
`[withAtomicFlush]${where}: ${pendingCount} pending change-set(s) remained at the commit boundary and were flushed defensively. ` +
|
|
78
|
-
'A phase mutated a managed entity after its flush boundary — split the mutation and any dependent read/sync into separate phases so the change is never at risk of being dropped.',
|
|
79
|
-
)
|
|
78
|
+
logger.warn('withAtomicFlush: pending change-sets remained at the commit boundary and were flushed defensively — a phase mutated a managed entity after its flush boundary; split the mutation and any dependent read/sync into separate phases', { label: label ?? null, pendingCount })
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
81
|
}
|
|
@@ -170,9 +170,16 @@ const AUTHOR_UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[8
|
|
|
170
170
|
|
|
171
171
|
export function normalizeAuthorUserId(
|
|
172
172
|
explicitAuthorUserId: string | undefined | null,
|
|
173
|
-
auth: { isApiKey?: boolean; sub?: string | null } | undefined | null
|
|
173
|
+
auth: { isApiKey?: boolean; isSuperAdmin?: boolean; sub?: string | null } | undefined | null
|
|
174
174
|
): string | null {
|
|
175
|
-
if (
|
|
175
|
+
if (
|
|
176
|
+
explicitAuthorUserId &&
|
|
177
|
+
auth?.isSuperAdmin === true &&
|
|
178
|
+
auth.isApiKey !== true &&
|
|
179
|
+
AUTHOR_UUID_REGEX.test(explicitAuthorUserId)
|
|
180
|
+
) {
|
|
181
|
+
return explicitAuthorUserId
|
|
182
|
+
}
|
|
176
183
|
const authSub = auth?.isApiKey ? null : auth?.sub ?? null
|
|
177
184
|
if (!authSub) return null
|
|
178
185
|
return AUTHOR_UUID_REGEX.test(authSub) ? authSub : null
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { CommandHandler } from './types'
|
|
2
|
+
import { createLogger } from '../logger'
|
|
3
|
+
|
|
4
|
+
const logger = createLogger('shared').child({ component: 'commands' })
|
|
2
5
|
|
|
3
6
|
export type CommandLoader = {
|
|
4
7
|
id?: string | null
|
|
@@ -21,7 +24,7 @@ class CommandRegistry {
|
|
|
21
24
|
if (this.handlers.has(handler.id)) {
|
|
22
25
|
if (process.env.NODE_ENV === 'development') {
|
|
23
26
|
if (!this.didWarnAboutDevelopmentReregistration) {
|
|
24
|
-
|
|
27
|
+
logger.debug('Commands re-registered (this may occur during HMR)')
|
|
25
28
|
this.didWarnAboutDevelopmentReregistration = true
|
|
26
29
|
}
|
|
27
30
|
this.handlers.set(handler.id, handler)
|
|
@@ -42,7 +45,7 @@ class CommandRegistry {
|
|
|
42
45
|
throw new Error(`Duplicate command loader registration for id ${loader.id}`)
|
|
43
46
|
}
|
|
44
47
|
if (this.loadersById.has(loader.id) && process.env.NODE_ENV === 'development' && !this.didWarnAboutDevelopmentLoaderReregistration) {
|
|
45
|
-
|
|
48
|
+
logger.debug('Command loaders re-registered (this may occur during HMR)')
|
|
46
49
|
this.didWarnAboutDevelopmentLoaderReregistration = true
|
|
47
50
|
}
|
|
48
51
|
this.loadersById.set(loader.id, loader)
|
|
@@ -55,7 +58,7 @@ class CommandRegistry {
|
|
|
55
58
|
throw new Error(`Duplicate command loader registration for key ${key}`)
|
|
56
59
|
}
|
|
57
60
|
if (existing.has(key) && process.env.NODE_ENV === 'development' && !this.didWarnAboutDevelopmentLoaderReregistration) {
|
|
58
|
-
|
|
61
|
+
logger.debug('Command loaders re-registered (this may occur during HMR)')
|
|
59
62
|
this.didWarnAboutDevelopmentLoaderReregistration = true
|
|
60
63
|
}
|
|
61
64
|
existing.set(key, loader)
|
|
@@ -3,6 +3,9 @@ import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
|
3
3
|
import { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'
|
|
4
4
|
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
5
5
|
import { env } from 'process'
|
|
6
|
+
import { createLogger } from '../logger'
|
|
7
|
+
|
|
8
|
+
const logger = createLogger('shared').child({ component: 'scope' })
|
|
6
9
|
|
|
7
10
|
function buildScopeLogContext(ctx: CommandRuntimeContext) {
|
|
8
11
|
const requestInfo =
|
|
@@ -46,7 +49,7 @@ function logScopeViolation(
|
|
|
46
49
|
): void {
|
|
47
50
|
try {
|
|
48
51
|
if (env.NODE_ENV !== 'test') {
|
|
49
|
-
|
|
52
|
+
logger.warn('Forbidden organization scope mismatch detected', {
|
|
50
53
|
expectedId: expected,
|
|
51
54
|
actualId: actual,
|
|
52
55
|
...buildScopeLogContext(ctx),
|
|
@@ -60,7 +63,7 @@ function logScopeViolation(
|
|
|
60
63
|
function logUnscopedOrganizationAccess(ctx: CommandRuntimeContext, organizationId: string): void {
|
|
61
64
|
try {
|
|
62
65
|
if (env.NODE_ENV !== 'test') {
|
|
63
|
-
|
|
66
|
+
logger.warn('Unscoped organization command executed without organization context', {
|
|
64
67
|
targetOrganizationId: organizationId,
|
|
65
68
|
strictEnforcement: isStrictOrganizationScopeEnforced(),
|
|
66
69
|
...buildScopeLogContext(ctx),
|
|
@@ -78,7 +81,7 @@ function logTenantScopeViolation(
|
|
|
78
81
|
): void {
|
|
79
82
|
try {
|
|
80
83
|
if (env.NODE_ENV !== 'test') {
|
|
81
|
-
|
|
84
|
+
logger.warn('Forbidden tenant scope mismatch detected', {
|
|
82
85
|
expectedTenantId,
|
|
83
86
|
actualTenantId,
|
|
84
87
|
...buildScopeLogContext(ctx),
|
|
@@ -10,6 +10,21 @@ import {
|
|
|
10
10
|
OPTIMISTIC_LOCK_HEADER_NAME,
|
|
11
11
|
} from '../optimistic-lock-headers'
|
|
12
12
|
import type { CrudMutationGuardValidateInput } from '../mutation-guard'
|
|
13
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
14
|
+
|
|
15
|
+
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
16
|
+
const mocked = {
|
|
17
|
+
debug: jest.fn(),
|
|
18
|
+
info: jest.fn(),
|
|
19
|
+
warn: jest.fn(),
|
|
20
|
+
error: jest.fn(),
|
|
21
|
+
child: jest.fn(),
|
|
22
|
+
}
|
|
23
|
+
mocked.child.mockImplementation(() => mocked)
|
|
24
|
+
return { createLogger: jest.fn(() => mocked) }
|
|
25
|
+
})
|
|
26
|
+
const loggerError = createLogger('shared').error as jest.Mock
|
|
27
|
+
|
|
13
28
|
|
|
14
29
|
describe('parseOptimisticLockEnv', () => {
|
|
15
30
|
it('returns mode=all when unset (default ON)', () => {
|
|
@@ -476,7 +491,7 @@ describe('createGenericOptimisticLockReader', () => {
|
|
|
476
491
|
throw new Error('column "deleted_at" does not exist')
|
|
477
492
|
},
|
|
478
493
|
} as never
|
|
479
|
-
|
|
494
|
+
loggerError.mockClear()
|
|
480
495
|
try {
|
|
481
496
|
const result = await reader(em, {
|
|
482
497
|
resourceKind: 'customers.tag',
|
|
@@ -487,12 +502,12 @@ describe('createGenericOptimisticLockReader', () => {
|
|
|
487
502
|
// Fail-open control flow preserved: a query error must never 500 the mutation.
|
|
488
503
|
expect(result).toBeNull()
|
|
489
504
|
// ...but a misconfig must be visible, naming the affected resourceKind.
|
|
490
|
-
expect(
|
|
491
|
-
const [message] =
|
|
492
|
-
expect(String(message)).toContain('
|
|
493
|
-
expect(
|
|
505
|
+
expect(loggerError).toHaveBeenCalledTimes(1)
|
|
506
|
+
const [message, fields] = loggerError.mock.calls[0]
|
|
507
|
+
expect(String(message)).toContain('optimistic locking is DISABLED')
|
|
508
|
+
expect(fields).toEqual(expect.objectContaining({ resourceKind: 'customers.tag' }))
|
|
494
509
|
} finally {
|
|
495
|
-
|
|
510
|
+
loggerError.mockClear()
|
|
496
511
|
}
|
|
497
512
|
})
|
|
498
513
|
|
|
@@ -2,6 +2,21 @@ import type { AwilixContainer } from 'awilix'
|
|
|
2
2
|
import { registerMutationGuards } from '../mutation-guard-store'
|
|
3
3
|
import type { MutationGuard } from '../mutation-guard-registry'
|
|
4
4
|
import { runRouteMutationGuards, toRegistryMutationOperation } from '../route-mutation-guard'
|
|
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 loggerError = createLogger('shared').error as jest.Mock
|
|
19
|
+
|
|
5
20
|
|
|
6
21
|
type Registrations = Record<string, unknown>
|
|
7
22
|
|
|
@@ -146,7 +161,7 @@ describe('runRouteMutationGuards', () => {
|
|
|
146
161
|
})
|
|
147
162
|
|
|
148
163
|
it('swallows afterSuccess callback errors so a committed write still succeeds', async () => {
|
|
149
|
-
|
|
164
|
+
loggerError.mockClear()
|
|
150
165
|
const guard = makeGuard({
|
|
151
166
|
id: 'throwing-after-guard',
|
|
152
167
|
validate: jest.fn().mockResolvedValue({ ok: true, shouldRunAfterSuccess: true }),
|
|
@@ -164,7 +179,7 @@ describe('runRouteMutationGuards', () => {
|
|
|
164
179
|
expect(result.ok).toBe(true)
|
|
165
180
|
if (!result.ok) throw new Error('expected passed result')
|
|
166
181
|
await expect(result.runAfterSuccess()).resolves.toBeUndefined()
|
|
167
|
-
expect(
|
|
182
|
+
expect(loggerError).toHaveBeenCalledWith('Mutation guard afterSuccess failed', expect.objectContaining({ guardId: 'throwing-after-guard' }))
|
|
168
183
|
})
|
|
169
184
|
|
|
170
185
|
it('skips feature-gated guards when the caller lacks the feature', async () => {
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { matchesEventPattern, collectSyncSubscribers, runSyncBeforeEvent, runSyncAfterEvent } from '../sync-event-runner'
|
|
2
2
|
import type { SyncSubscriberEntry } from '../sync-subscriber-store'
|
|
3
3
|
import type { SyncCrudEventPayload } from '../sync-event-types'
|
|
4
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
5
|
+
|
|
6
|
+
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
7
|
+
const mocked = {
|
|
8
|
+
debug: jest.fn(),
|
|
9
|
+
info: jest.fn(),
|
|
10
|
+
warn: jest.fn(),
|
|
11
|
+
error: jest.fn(),
|
|
12
|
+
child: jest.fn(),
|
|
13
|
+
}
|
|
14
|
+
mocked.child.mockImplementation(() => mocked)
|
|
15
|
+
return { createLogger: jest.fn(() => mocked) }
|
|
16
|
+
})
|
|
17
|
+
const loggerError = createLogger('shared').error as jest.Mock
|
|
18
|
+
|
|
4
19
|
|
|
5
20
|
describe('matchesEventPattern', () => {
|
|
6
21
|
it('matches exact event ID', () => {
|
|
@@ -158,10 +173,9 @@ describe('runSyncAfterEvent', () => {
|
|
|
158
173
|
handler: jest.fn().mockResolvedValue(undefined),
|
|
159
174
|
}
|
|
160
175
|
|
|
161
|
-
|
|
176
|
+
loggerError.mockClear()
|
|
162
177
|
await runSyncAfterEvent([s1, s2], basePayload, ctx)
|
|
163
178
|
expect(s2.handler).toHaveBeenCalled()
|
|
164
|
-
expect(
|
|
165
|
-
consoleSpy.mockRestore()
|
|
179
|
+
expect(loggerError).toHaveBeenCalled()
|
|
166
180
|
})
|
|
167
181
|
})
|
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
convertAdvancedFilterToWhere,
|
|
6
6
|
} from '../query/advanced-filter'
|
|
7
7
|
import { compileTreeToWhere, type AdvancedFilterTree } from '../query/advanced-filter-tree'
|
|
8
|
+
import { createLogger } from '../logger'
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('shared').child({ component: 'crud' })
|
|
8
11
|
|
|
9
12
|
function splitOrClauses(filters: Record<string, unknown>): {
|
|
10
13
|
directFilters: Record<string, unknown>
|
|
@@ -142,7 +145,7 @@ export function consumeAdvancedFilterState(query: Record<string, unknown>): Adva
|
|
|
142
145
|
if (!g[sentinel]) {
|
|
143
146
|
g[sentinel] = true
|
|
144
147
|
// eslint-disable-next-line no-console
|
|
145
|
-
|
|
148
|
+
logger.warn('Legacy v1 advanced-filter URL detected; auto-upgraded to v2 tree')
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
}
|
package/src/lib/crud/cache.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { AwilixContainer } from 'awilix'
|
|
2
2
|
import { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
|
|
3
3
|
import { parseBooleanToken } from '../boolean'
|
|
4
|
+
import { createLogger } from '../logger'
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('shared').child({ component: 'crud-cache' })
|
|
4
7
|
|
|
5
8
|
export type CrudCacheIdentifiers = {
|
|
6
9
|
id?: string | null
|
|
@@ -25,7 +28,7 @@ export function isCrudCacheDebugEnabled(): boolean {
|
|
|
25
28
|
export function debugCrudCache(event: string, context: Record<string, unknown>) {
|
|
26
29
|
if (!isCrudCacheDebugEnabled()) return
|
|
27
30
|
try {
|
|
28
|
-
|
|
31
|
+
logger.debug(event, context)
|
|
29
32
|
} catch {}
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -6,6 +6,7 @@ import type { TenantDataEncryptionService } from '../encryption/tenantDataEncryp
|
|
|
6
6
|
import { decryptCustomFieldValue, resolveTenantEncryptionService } from '../encryption/customFieldValues'
|
|
7
7
|
import { parseBooleanToken } from '../boolean'
|
|
8
8
|
import { extractCustomFieldEntries } from './custom-fields-client'
|
|
9
|
+
import { createLogger } from '../logger'
|
|
9
10
|
import {
|
|
10
11
|
buildCustomFieldDefinitionIndexFromRows,
|
|
11
12
|
normalizeDefinitionKey,
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
type CustomFieldDefinitionSummary,
|
|
17
18
|
} from './custom-field-definition-index'
|
|
18
19
|
|
|
20
|
+
const logger = createLogger('shared').child({ component: 'crud' })
|
|
21
|
+
|
|
19
22
|
export type { CustomFieldDefinitionSummary, CustomFieldDefinitionIndex } from './custom-field-definition-index'
|
|
20
23
|
|
|
21
24
|
export type CustomFieldSelectors = {
|
|
@@ -416,7 +419,7 @@ export async function loadCustomFieldDefinitionIndex(opts: LoadCustomFieldDefini
|
|
|
416
419
|
return restored
|
|
417
420
|
}
|
|
418
421
|
} catch (err) {
|
|
419
|
-
|
|
422
|
+
logger.warn('Custom-field definition cache read failed', { err })
|
|
420
423
|
}
|
|
421
424
|
}
|
|
422
425
|
|
|
@@ -433,7 +436,7 @@ export async function loadCustomFieldDefinitionIndex(opts: LoadCustomFieldDefini
|
|
|
433
436
|
tags: buildCfDefIndexCacheTags({ tenantId, entityIds }),
|
|
434
437
|
})
|
|
435
438
|
} catch (err) {
|
|
436
|
-
|
|
439
|
+
logger.warn('Custom-field definition cache write failed', { err })
|
|
437
440
|
}
|
|
438
441
|
}
|
|
439
442
|
if (requestBucket) requestBucket.set(cacheKey, index)
|
|
@@ -14,6 +14,9 @@ import type {
|
|
|
14
14
|
} from './response-enricher'
|
|
15
15
|
import { getEnrichersForEntity } from './enricher-registry'
|
|
16
16
|
import { logEnricherTiming } from '../umes/enricher-timing'
|
|
17
|
+
import { createLogger } from '../logger'
|
|
18
|
+
|
|
19
|
+
const logger = createLogger('shared').child({ component: 'umes' })
|
|
17
20
|
|
|
18
21
|
const DEFAULT_TIMEOUT = 2000
|
|
19
22
|
const SLOW_WARN_MS = 100
|
|
@@ -263,13 +266,9 @@ export async function applyResponseEnrichers<T extends Record<string, unknown>>(
|
|
|
263
266
|
|
|
264
267
|
const elapsedMs = Date.now() - startTime
|
|
265
268
|
if (elapsedMs > SLOW_ERROR_MS) {
|
|
266
|
-
|
|
267
|
-
`[UMES] Enricher ${enricher.id} took ${elapsedMs}ms (threshold: ${SLOW_ERROR_MS}ms)`,
|
|
268
|
-
)
|
|
269
|
+
logger.error('Enricher exceeded slow threshold', { enricherId: enricher.id, elapsedMs, thresholdMs: SLOW_ERROR_MS })
|
|
269
270
|
} else if (elapsedMs > SLOW_WARN_MS) {
|
|
270
|
-
|
|
271
|
-
`[UMES] Enricher ${enricher.id} took ${elapsedMs}ms (threshold: ${SLOW_WARN_MS}ms)`,
|
|
272
|
-
)
|
|
271
|
+
logger.warn('Enricher exceeded slow threshold', { enricherId: enricher.id, elapsedMs, thresholdMs: SLOW_WARN_MS })
|
|
273
272
|
}
|
|
274
273
|
logEnricherTiming(enricher.id, entry.moduleId, targetEntity, elapsedMs)
|
|
275
274
|
|
|
@@ -289,8 +288,7 @@ export async function applyResponseEnrichers<T extends Record<string, unknown>>(
|
|
|
289
288
|
throw err
|
|
290
289
|
}
|
|
291
290
|
|
|
292
|
-
|
|
293
|
-
console.warn(`[UMES] Enricher ${enricher.id} failed: ${message}`)
|
|
291
|
+
logger.warn('Enricher failed', { enricherId: enricher.id, err })
|
|
294
292
|
enricherErrors.push(enricher.id)
|
|
295
293
|
|
|
296
294
|
if (enricher.fallback) {
|
|
@@ -376,8 +374,7 @@ export async function applyResponseEnricherToRecord<T extends Record<string, unk
|
|
|
376
374
|
throw err
|
|
377
375
|
}
|
|
378
376
|
|
|
379
|
-
|
|
380
|
-
console.warn(`[UMES] Enricher ${enricher.id} failed: ${message}`)
|
|
377
|
+
logger.warn('Enricher failed', { enricherId: enricher.id, err })
|
|
381
378
|
enricherErrors.push(enricher.id)
|
|
382
379
|
|
|
383
380
|
if (enricher.fallback) {
|