@open-mercato/shared 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7
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 +6 -57
- package/build.mjs +4 -2
- package/dist/lib/auth/server.js +2 -15
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/dynamicLoader.js +43 -117
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +1 -5
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +2 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +11 -2
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +4 -63
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-runner.js +2 -2
- package/dist/lib/crud/interceptor-runner.js.map +2 -2
- package/dist/lib/crud/mutation-guard-registry.js +2 -2
- package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
- package/dist/lib/crud/types.js +0 -4
- package/dist/lib/crud/types.js.map +3 -3
- package/dist/lib/data/engine.js +11 -37
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/mikro.js +12 -3
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/db/pg-errors.js +0 -42
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/di/container.js +6 -48
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/email/send.js +0 -56
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +10 -2
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +17 -52
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/i18n/config.js +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/index.js +3 -47
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +1 -5
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/query/engine.js +40 -54
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/redis/connection.js +2 -7
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/search/config.js +2 -61
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenize.js +11 -23
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/index.js +1 -13
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/lib/webhooks/verify.js +7 -12
- package/dist/lib/webhooks/verify.js.map +2 -2
- package/dist/modules/analytics.js.map +2 -2
- package/dist/modules/overrides.js +1 -50
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +1 -8
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js +0 -1
- package/dist/modules/registry.js.map +2 -2
- package/dist/security/enabledModulesRegistry.js +11 -56
- package/dist/security/enabledModulesRegistry.js.map +2 -2
- package/jest.config.cjs +1 -2
- package/package.json +9 -19
- package/src/lib/ai/llm-provider.ts +0 -33
- package/src/lib/auth/__tests__/server.test.ts +0 -23
- package/src/lib/auth/server.ts +2 -23
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +8 -63
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +1 -21
- package/src/lib/bootstrap/dynamicLoader.ts +65 -213
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +0 -28
- package/src/lib/commands/command-bus.ts +1 -5
- package/src/lib/commands/command-interceptor-runner.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +0 -298
- package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +0 -26
- package/src/lib/crud/enricher-runner.ts +11 -2
- package/src/lib/crud/factory.ts +1 -69
- package/src/lib/crud/interceptor-runner.ts +2 -2
- package/src/lib/crud/mutation-guard-registry.ts +2 -2
- package/src/lib/crud/types.ts +0 -3
- package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +3 -18
- package/src/lib/data/engine.ts +16 -50
- package/src/lib/db/__tests__/mikro.test.ts +25 -0
- package/src/lib/db/mikro.ts +38 -13
- package/src/lib/db/pg-errors.ts +0 -58
- package/src/lib/di/container.ts +8 -72
- package/src/lib/email/__tests__/send.test.ts +1 -37
- package/src/lib/email/send.ts +0 -79
- package/src/lib/encryption/__tests__/subscriber.test.ts +2 -54
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -58
- package/src/lib/encryption/subscriber.ts +14 -2
- package/src/lib/encryption/tenantDataEncryptionService.ts +18 -64
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +2 -6
- package/src/lib/logger/__tests__/logger.test.ts +0 -77
- package/src/lib/logger/index.ts +1 -59
- package/src/lib/modules/registry.ts +0 -10
- package/src/lib/query/__tests__/engine.test.ts +13 -65
- package/src/lib/query/engine.ts +58 -75
- package/src/lib/query/types.ts +5 -4
- package/src/lib/redis/__tests__/connection.test.ts +0 -16
- package/src/lib/redis/connection.ts +7 -10
- package/src/lib/search/__tests__/config.test.ts +0 -177
- package/src/lib/search/config.ts +8 -106
- package/src/lib/search/tokenize.ts +11 -33
- package/src/lib/webhooks/__tests__/verify.test.ts +1 -20
- package/src/lib/webhooks/index.ts +1 -19
- package/src/lib/webhooks/verify.ts +9 -17
- package/src/modules/__tests__/registry.test.ts +2 -10
- package/src/modules/analytics.ts +0 -7
- package/src/modules/customer-auth.ts +0 -1
- package/src/modules/encryption.ts +0 -3
- package/src/modules/navigation/backendChrome.ts +0 -15
- package/src/modules/overrides.ts +0 -103
- package/src/modules/payment_gateways/types.ts +0 -41
- package/src/modules/registry.ts +0 -3
- package/src/security/enabledModulesRegistry.ts +12 -64
- package/dist/lib/ai/safety-identifier.js +0 -32
- package/dist/lib/ai/safety-identifier.js.map +0 -7
- package/dist/lib/auth/organizationScope.js +0 -34
- package/dist/lib/auth/organizationScope.js.map +0 -7
- package/dist/lib/bootstrap/clientOnlyModules.js +0 -55
- package/dist/lib/bootstrap/clientOnlyModules.js.map +0 -7
- package/dist/lib/data/consistency.js +0 -19
- package/dist/lib/data/consistency.js.map +0 -7
- package/dist/lib/db/entityMetadata.js +0 -26
- package/dist/lib/db/entityMetadata.js.map +0 -7
- package/dist/lib/logger/extension.js +0 -29
- package/dist/lib/logger/extension.js.map +0 -7
- package/dist/lib/modules/surfaceFingerprint.js +0 -47
- package/dist/lib/modules/surfaceFingerprint.js.map +0 -7
- package/dist/lib/query/ciphertext-search-warning.js +0 -45
- package/dist/lib/query/ciphertext-search-warning.js.map +0 -7
- package/dist/lib/search/auto-indexing.js +0 -14
- package/dist/lib/search/auto-indexing.js.map +0 -7
- package/dist/lib/search/availability.js +0 -107
- package/dist/lib/search/availability.js.map +0 -7
- package/dist/lib/search/tokenLookup.js +0 -46
- package/dist/lib/search/tokenLookup.js.map +0 -7
- package/dist/lib/telemetry/runtime.js +0 -35
- package/dist/lib/telemetry/runtime.js.map +0 -7
- package/dist/lib/webhooks/body.js +0 -57
- package/dist/lib/webhooks/body.js.map +0 -7
- package/dist/lib/webhooks/inbound-types.js +0 -1
- package/dist/lib/webhooks/inbound-types.js.map +0 -7
- package/dist/modules/widgets/extension-points.js +0 -96
- package/dist/modules/widgets/extension-points.js.map +0 -7
- package/dist/security/featurePolicy.js +0 -62
- package/dist/security/featurePolicy.js.map +0 -7
- package/scripts/versionSource.cjs +0 -65
- package/src/lib/__tests__/versionSource.test.ts +0 -46
- package/src/lib/ai/__tests__/llm-provider-contract.test.ts +0 -59
- package/src/lib/ai/__tests__/safety-identifier.test.ts +0 -71
- package/src/lib/ai/safety-identifier.ts +0 -73
- package/src/lib/auth/__tests__/organizationScope.test.ts +0 -107
- package/src/lib/auth/organizationScope.ts +0 -65
- package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +0 -189
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +0 -98
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +0 -211
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +0 -256
- package/src/lib/bootstrap/clientOnlyModules.ts +0 -85
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +0 -171
- package/src/lib/data/__tests__/consistency.test.ts +0 -38
- package/src/lib/data/consistency.ts +0 -17
- package/src/lib/db/__tests__/entityMetadata.test.ts +0 -38
- package/src/lib/db/__tests__/pg-errors.test.ts +0 -48
- package/src/lib/db/entityMetadata.ts +0 -43
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +0 -155
- package/src/lib/di/__tests__/container-app-di.test.ts +0 -173
- package/src/lib/di/__tests__/registrar-error-log.test.ts +0 -154
- package/src/lib/i18n/__tests__/config.test.ts +0 -8
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +0 -30
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +0 -29
- package/src/lib/logger/extension.ts +0 -59
- package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +0 -122
- package/src/lib/modules/surfaceFingerprint.ts +0 -87
- package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +0 -178
- package/src/lib/query/ciphertext-search-warning.ts +0 -95
- package/src/lib/search/__tests__/availability.test.ts +0 -211
- package/src/lib/search/__tests__/tokenLookup.test.ts +0 -206
- package/src/lib/search/__tests__/tokenize.test.ts +0 -49
- package/src/lib/search/auto-indexing.ts +0 -22
- package/src/lib/search/availability.ts +0 -232
- package/src/lib/search/tokenLookup.ts +0 -133
- package/src/lib/telemetry/runtime.ts +0 -72
- package/src/lib/webhooks/__tests__/body.test.ts +0 -95
- package/src/lib/webhooks/body.ts +0 -70
- package/src/lib/webhooks/inbound-types.ts +0 -125
- package/src/modules/__tests__/nav-group-order-override.test.ts +0 -183
- package/src/modules/payment_gateways/__tests__/types.test.ts +0 -37
- package/src/modules/widgets/__tests__/extension-points.test.ts +0 -101
- package/src/modules/widgets/extension-points.ts +0 -524
- package/src/security/__tests__/featurePolicy.test.ts +0 -166
- package/src/security/featurePolicy.ts +0 -89
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// A module DI registrar that throws is skipped fail-open (one broken module
|
|
2
|
-
// must not take down every request container), but the failure MUST be logged —
|
|
3
|
-
// a silent catch leaves the module's services missing with no trace until an
|
|
4
|
-
// unrelated Awilix resolution error surfaces much later.
|
|
5
|
-
|
|
6
|
-
import { asValue } from 'awilix'
|
|
7
|
-
import type { AwilixContainer } from 'awilix'
|
|
8
|
-
|
|
9
|
-
const mockLoggerError = jest.fn()
|
|
10
|
-
|
|
11
|
-
jest.mock('@open-mercato/shared/lib/logger', () => {
|
|
12
|
-
const makeLogger = (): Record<string, unknown> => ({
|
|
13
|
-
error: (...args: unknown[]) => mockLoggerError(...args),
|
|
14
|
-
warn: jest.fn(),
|
|
15
|
-
info: jest.fn(),
|
|
16
|
-
debug: jest.fn(),
|
|
17
|
-
trace: jest.fn(),
|
|
18
|
-
child: () => makeLogger(),
|
|
19
|
-
})
|
|
20
|
-
return {
|
|
21
|
-
__esModule: true,
|
|
22
|
-
createLogger: () => makeLogger(),
|
|
23
|
-
getLogLevel: () => 'info',
|
|
24
|
-
isLevelEnabled: () => false,
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
jest.mock(
|
|
29
|
-
'@open-mercato/shared/lib/db/mikro',
|
|
30
|
-
() => {
|
|
31
|
-
const baseEm: any = {
|
|
32
|
-
fork: () => baseEm,
|
|
33
|
-
getRepository: () => ({ find: async () => [], findOne: async () => null }),
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
__esModule: true,
|
|
37
|
-
getOrm: async () => ({ em: baseEm }),
|
|
38
|
-
getOrmEntities: () => [],
|
|
39
|
-
registerOrmEntities: () => {},
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
{ virtual: false },
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
jest.mock(
|
|
46
|
-
'@mikro-orm/core',
|
|
47
|
-
() => ({
|
|
48
|
-
__esModule: true,
|
|
49
|
-
RequestContext: { getEntityManager: () => null },
|
|
50
|
-
}),
|
|
51
|
-
{ virtual: true },
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
jest.mock(
|
|
55
|
-
'@open-mercato/shared/lib/query/engine',
|
|
56
|
-
() => ({ __esModule: true, BasicQueryEngine: class {} }),
|
|
57
|
-
{ virtual: false },
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
jest.mock(
|
|
61
|
-
'@open-mercato/shared/lib/data/engine',
|
|
62
|
-
() => ({ __esModule: true, DefaultDataEngine: class {} }),
|
|
63
|
-
{ virtual: false },
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
jest.mock(
|
|
67
|
-
'@open-mercato/shared/lib/commands',
|
|
68
|
-
() => ({
|
|
69
|
-
__esModule: true,
|
|
70
|
-
commandRegistry: {},
|
|
71
|
-
CommandBus: class { constructor() {} },
|
|
72
|
-
}),
|
|
73
|
-
{ virtual: false },
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
jest.mock(
|
|
77
|
-
'@open-mercato/shared/modules/overrides',
|
|
78
|
-
() => ({
|
|
79
|
-
__esModule: true,
|
|
80
|
-
applyDiOverridesToContainer: () => {},
|
|
81
|
-
applyModuleOverridesToModules: (modules: unknown[]) => modules,
|
|
82
|
-
applyComponentOverridesToEntries: (entries: unknown[]) => entries,
|
|
83
|
-
}),
|
|
84
|
-
{ virtual: false },
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
jest.mock(
|
|
88
|
-
'@open-mercato/core/bootstrap',
|
|
89
|
-
() => ({
|
|
90
|
-
__esModule: true,
|
|
91
|
-
bootstrap: async () => {},
|
|
92
|
-
}),
|
|
93
|
-
{ virtual: true },
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
jest.mock(
|
|
97
|
-
'@open-mercato/shared/lib/encryption/subscriber',
|
|
98
|
-
() => ({
|
|
99
|
-
__esModule: true,
|
|
100
|
-
registerTenantEncryptionSubscriber: () => {},
|
|
101
|
-
}),
|
|
102
|
-
{ virtual: true },
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
const {
|
|
106
|
-
createRequestContainer,
|
|
107
|
-
registerAppDiRegistrar,
|
|
108
|
-
registerDiRegistrars,
|
|
109
|
-
resetBootstrapCache,
|
|
110
|
-
} = require('@open-mercato/shared/lib/di/container')
|
|
111
|
-
|
|
112
|
-
describe('createRequestContainer — module DI registrar failures', () => {
|
|
113
|
-
beforeEach(() => {
|
|
114
|
-
resetBootstrapCache()
|
|
115
|
-
registerAppDiRegistrar(null)
|
|
116
|
-
mockLoggerError.mockClear()
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
afterAll(() => {
|
|
120
|
-
registerDiRegistrars([])
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('logs the error and continues with the remaining registrars', async () => {
|
|
124
|
-
const boom = new Error('di exploded')
|
|
125
|
-
const workingRegistrar = jest.fn((container: AwilixContainer) => {
|
|
126
|
-
container.register({ registrarProbeService: asValue('registered') })
|
|
127
|
-
})
|
|
128
|
-
registerDiRegistrars([
|
|
129
|
-
() => { throw boom },
|
|
130
|
-
workingRegistrar,
|
|
131
|
-
])
|
|
132
|
-
|
|
133
|
-
const container = await createRequestContainer()
|
|
134
|
-
|
|
135
|
-
expect(workingRegistrar).toHaveBeenCalledTimes(1)
|
|
136
|
-
expect(container.resolve('registrarProbeService')).toBe('registered')
|
|
137
|
-
expect(mockLoggerError).toHaveBeenCalledWith('Module DI registrar failed', {
|
|
138
|
-
registrarIndex: 0,
|
|
139
|
-
err: boom,
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('does not log when every registrar succeeds', async () => {
|
|
144
|
-
registerDiRegistrars([
|
|
145
|
-
(container: AwilixContainer) => {
|
|
146
|
-
container.register({ registrarProbeService: asValue('ok') })
|
|
147
|
-
},
|
|
148
|
-
])
|
|
149
|
-
|
|
150
|
-
await createRequestContainer()
|
|
151
|
-
|
|
152
|
-
expect(mockLoggerError).not.toHaveBeenCalled()
|
|
153
|
-
})
|
|
154
|
-
})
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { defaultLocale, locales, type Locale } from '../config'
|
|
2
|
-
|
|
3
|
-
describe('supported application locales', () => {
|
|
4
|
-
it('registers Korean without changing the default locale', () => {
|
|
5
|
-
expect(locales).toEqual<Locale[]>(['en', 'pl', 'es', 'de', 'ko'])
|
|
6
|
-
expect(defaultLocale).toBe('en')
|
|
7
|
-
})
|
|
8
|
-
})
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { loadDictionary, registerModules } from '../server'
|
|
2
|
-
import type { Module } from '../../../modules/registry'
|
|
3
|
-
|
|
4
|
-
describe('loadDictionary memoization', () => {
|
|
5
|
-
it('returns a cached dictionary for repeated calls with the same locale', async () => {
|
|
6
|
-
registerModules([
|
|
7
|
-
{ id: 'demo', translations: { en: { 'demo.hello': 'Hello' } } },
|
|
8
|
-
] satisfies Module[])
|
|
9
|
-
|
|
10
|
-
const first = await loadDictionary('en')
|
|
11
|
-
const second = await loadDictionary('en')
|
|
12
|
-
|
|
13
|
-
expect(second).toBe(first)
|
|
14
|
-
expect(first['demo.hello']).toBe('Hello')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('busts the cache when the registered module set changes', async () => {
|
|
18
|
-
registerModules([{ id: 'a', translations: { en: { 'a.k': 'A' } } }] satisfies Module[])
|
|
19
|
-
const before = await loadDictionary('en')
|
|
20
|
-
|
|
21
|
-
registerModules([
|
|
22
|
-
{ id: 'a', translations: { en: { 'a.k': 'A' } } },
|
|
23
|
-
{ id: 'b', translations: { en: { 'b.k': 'B' } } },
|
|
24
|
-
] satisfies Module[])
|
|
25
|
-
const after = await loadDictionary('en')
|
|
26
|
-
|
|
27
|
-
expect(after).not.toBe(before)
|
|
28
|
-
expect(after['b.k']).toBe('B')
|
|
29
|
-
})
|
|
30
|
-
})
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { invalidateDictionaryCache, loadDictionary, registerModules, resolveTranslations } from '../server'
|
|
2
|
-
import type { Module } from '../../../modules/registry'
|
|
3
|
-
|
|
4
|
-
const REGISTRY_GLOBAL_KEY = '__openMercatoModulesRegistry__'
|
|
5
|
-
|
|
6
|
-
describe('loadDictionary without a bootstrapped module registry', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
delete (globalThis as Record<string, unknown>)[REGISTRY_GLOBAL_KEY]
|
|
9
|
-
invalidateDictionaryCache()
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('resolves the app dictionary instead of throwing a bootstrap error', async () => {
|
|
13
|
-
await expect(loadDictionary('en')).resolves.toEqual(expect.any(Object))
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('lets a route handler translate its response before bootstrap runs', async () => {
|
|
17
|
-
const { translate } = await resolveTranslations()
|
|
18
|
-
|
|
19
|
-
expect(translate('storage_s3.upload.errors.unsupported', 'Unsupported file type')).toBe('Unsupported file type')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('picks up module translations once bootstrap registers them', async () => {
|
|
23
|
-
await loadDictionary('en')
|
|
24
|
-
|
|
25
|
-
registerModules([{ id: 'demo', translations: { en: { 'demo.hello': 'Hello' } } }] satisfies Module[])
|
|
26
|
-
|
|
27
|
-
await expect(loadDictionary('en')).resolves.toMatchObject({ 'demo.hello': 'Hello' })
|
|
28
|
-
})
|
|
29
|
-
})
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { LogLevel } from './level'
|
|
2
|
-
|
|
3
|
-
export type LoggerExtensionRecord = {
|
|
4
|
-
level: LogLevel
|
|
5
|
-
namespace: string
|
|
6
|
-
message: string
|
|
7
|
-
fields: Record<string, unknown>
|
|
8
|
-
time: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type LoggerExtension = {
|
|
12
|
-
/**
|
|
13
|
-
* Add process-local context (for example trace/span ids) to both the local
|
|
14
|
-
* line and the optional remote sink.
|
|
15
|
-
*/
|
|
16
|
-
enrich?(): Record<string, unknown> | undefined
|
|
17
|
-
/**
|
|
18
|
-
* Observe a record after the local transport writes it. Implementations must
|
|
19
|
-
* never throw into application code.
|
|
20
|
-
*/
|
|
21
|
-
emit?(record: LoggerExtensionRecord): void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const GLOBAL_KEY = Symbol.for('@open-mercato/shared.loggerExtension')
|
|
25
|
-
|
|
26
|
-
type LoggerExtensionStore = {
|
|
27
|
-
active?: LoggerExtension
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function store(): LoggerExtensionStore {
|
|
31
|
-
const globalStore = globalThis as unknown as Record<symbol, LoggerExtensionStore | undefined>
|
|
32
|
-
let current = globalStore[GLOBAL_KEY]
|
|
33
|
-
if (!current) {
|
|
34
|
-
current = {}
|
|
35
|
-
globalStore[GLOBAL_KEY] = current
|
|
36
|
-
}
|
|
37
|
-
return current
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Register the single process-wide logger extension. The returned disposer only
|
|
42
|
-
* clears the extension when it is still the active registration.
|
|
43
|
-
*/
|
|
44
|
-
export function registerLoggerExtension(extension: LoggerExtension): () => void {
|
|
45
|
-
store().active = extension
|
|
46
|
-
return () => {
|
|
47
|
-
const current = store()
|
|
48
|
-
if (current.active === extension) current.active = undefined
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function getLoggerExtension(): LoggerExtension | undefined {
|
|
53
|
-
return store().active
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Test-only: clear the process-wide extension. */
|
|
57
|
-
export function resetLoggerExtension(): void {
|
|
58
|
-
store().active = undefined
|
|
59
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import type { Module, BackendRouteManifestEntry } from '../../../modules/registry'
|
|
2
|
-
import { getBackendRouteManifests } from '../../../modules/registry'
|
|
3
|
-
import { getModules } from '../registry'
|
|
4
|
-
import { getModuleSurfaceFingerprint } from '../surfaceFingerprint'
|
|
5
|
-
|
|
6
|
-
jest.mock('../registry', () => ({
|
|
7
|
-
getModules: jest.fn(),
|
|
8
|
-
}))
|
|
9
|
-
|
|
10
|
-
jest.mock('../../../modules/registry', () => ({
|
|
11
|
-
getBackendRouteManifests: jest.fn(),
|
|
12
|
-
}))
|
|
13
|
-
|
|
14
|
-
const mockGetModules = jest.mocked(getModules)
|
|
15
|
-
const mockGetBackendRouteManifests = jest.mocked(getBackendRouteManifests)
|
|
16
|
-
|
|
17
|
-
function route(pattern: string, overrides: Partial<BackendRouteManifestEntry> = {}): BackendRouteManifestEntry {
|
|
18
|
-
return {
|
|
19
|
-
moduleId: 'auth',
|
|
20
|
-
pattern,
|
|
21
|
-
title: 'Page',
|
|
22
|
-
load: async () => null,
|
|
23
|
-
...overrides,
|
|
24
|
-
} as BackendRouteManifestEntry
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type ModuleSurface = string | { id: string; features: Array<{ id: string; module?: string }> }
|
|
28
|
-
|
|
29
|
-
function setSurface(modules: ModuleSurface[], routes: BackendRouteManifestEntry[]): void {
|
|
30
|
-
mockGetModules.mockReturnValue(modules.map((mod) => (typeof mod === 'string' ? { id: mod } : mod) as Module))
|
|
31
|
-
mockGetBackendRouteManifests.mockReturnValue(routes)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('getModuleSurfaceFingerprint', () => {
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
jest.resetAllMocks()
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('is stable for the same enabled modules and route manifest', () => {
|
|
40
|
-
setSurface(['auth', 'search'], [route('/backend/dashboard')])
|
|
41
|
-
const first = getModuleSurfaceFingerprint()
|
|
42
|
-
|
|
43
|
-
setSurface(['auth', 'search'], [route('/backend/dashboard')])
|
|
44
|
-
expect(getModuleSurfaceFingerprint()).toBe(first)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('changes when a module is enabled — the deploy that used to serve a stale nav', () => {
|
|
48
|
-
setSurface(['auth'], [route('/backend/dashboard')])
|
|
49
|
-
const before = getModuleSurfaceFingerprint()
|
|
50
|
-
|
|
51
|
-
setSurface(['auth', 'search'], [route('/backend/dashboard')])
|
|
52
|
-
expect(getModuleSurfaceFingerprint()).not.toBe(before)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('changes when a backend route is added', () => {
|
|
56
|
-
setSurface(['auth'], [route('/backend/dashboard')])
|
|
57
|
-
const before = getModuleSurfaceFingerprint()
|
|
58
|
-
|
|
59
|
-
setSurface(['auth'], [route('/backend/dashboard'), route('/backend/search')])
|
|
60
|
-
expect(getModuleSurfaceFingerprint()).not.toBe(before)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('changes when only a route metadata field changes', () => {
|
|
64
|
-
setSurface(['auth'], [route('/backend/dashboard', { title: 'Dashboard' })])
|
|
65
|
-
const before = getModuleSurfaceFingerprint()
|
|
66
|
-
|
|
67
|
-
setSurface(['auth'], [route('/backend/dashboard', { title: 'Overview' })])
|
|
68
|
-
expect(getModuleSurfaceFingerprint()).not.toBe(before)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('changes when a module declares a new feature and no route moves', () => {
|
|
72
|
-
const routes = [route('/backend/dashboard')]
|
|
73
|
-
setSurface([{ id: 'auth', features: [{ id: 'auth.view' }] }], routes)
|
|
74
|
-
const before = getModuleSurfaceFingerprint()
|
|
75
|
-
|
|
76
|
-
setSurface([{ id: 'auth', features: [{ id: 'auth.view' }, { id: 'auth.manage' }] }], routes)
|
|
77
|
-
expect(getModuleSurfaceFingerprint()).not.toBe(before)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('changes when a feature declares a different owning module', () => {
|
|
81
|
-
const routes = [route('/backend/dashboard')]
|
|
82
|
-
setSurface([{ id: 'auth', features: [{ id: 'analytics.view' }] }], routes)
|
|
83
|
-
const before = getModuleSurfaceFingerprint()
|
|
84
|
-
|
|
85
|
-
setSurface([{ id: 'auth', features: [{ id: 'analytics.view', module: 'reporting' }] }], routes)
|
|
86
|
-
expect(getModuleSurfaceFingerprint()).not.toBe(before)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('ignores feature ordering within a module', () => {
|
|
90
|
-
const routes = [route('/backend/dashboard')]
|
|
91
|
-
setSurface([{ id: 'auth', features: [{ id: 'auth.view' }, { id: 'auth.manage' }] }], routes)
|
|
92
|
-
const forward = getModuleSurfaceFingerprint()
|
|
93
|
-
|
|
94
|
-
setSurface([{ id: 'auth', features: [{ id: 'auth.manage' }, { id: 'auth.view' }] }], routes)
|
|
95
|
-
expect(getModuleSurfaceFingerprint()).toBe(forward)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('ignores route manifest ordering', () => {
|
|
99
|
-
setSurface(['auth'], [route('/backend/a'), route('/backend/b')])
|
|
100
|
-
const forward = getModuleSurfaceFingerprint()
|
|
101
|
-
|
|
102
|
-
setSurface(['auth'], [route('/backend/b'), route('/backend/a')])
|
|
103
|
-
expect(getModuleSurfaceFingerprint()).toBe(forward)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('falls back to the route manifest alone when the module registry is not populated', () => {
|
|
107
|
-
mockGetModules.mockImplementation(() => {
|
|
108
|
-
throw new Error('[Bootstrap] Modules not registered.')
|
|
109
|
-
})
|
|
110
|
-
mockGetBackendRouteManifests.mockReturnValue([route('/backend/dashboard')])
|
|
111
|
-
|
|
112
|
-
expect(getModuleSurfaceFingerprint()).toMatch(/^[0-9a-f]{12}$/)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('degrades to a constant rather than throwing when route metadata cannot be serialized', () => {
|
|
116
|
-
const circular = route('/backend/dashboard') as BackendRouteManifestEntry & { self?: unknown }
|
|
117
|
-
circular.self = circular
|
|
118
|
-
setSurface(['auth'], [circular])
|
|
119
|
-
|
|
120
|
-
expect(getModuleSurfaceFingerprint()).toBe('unknown')
|
|
121
|
-
})
|
|
122
|
-
})
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deploy-time fingerprint of the registered module surface.
|
|
3
|
-
*
|
|
4
|
-
* Some cached payloads are derived from state that only changes when the app
|
|
5
|
-
* is redeployed — the enabled module set and the backend route manifest. That
|
|
6
|
-
* state has no database write to hang a tag invalidation off, so a cache key
|
|
7
|
-
* that omits it keeps serving the pre-deploy payload to anyone with a warm
|
|
8
|
-
* entry (see `/api/auth/admin/nav`, whose payload embeds
|
|
9
|
-
* `filterGrantsByEnabledModules(...)` computed at write time).
|
|
10
|
-
*
|
|
11
|
-
* Mixing this fingerprint into such keys makes new processes write to new
|
|
12
|
-
* keys, so old and new pods coexist during a rolling deploy without a purge
|
|
13
|
-
* step and without any ordering constraint between purge and rollout.
|
|
14
|
-
*
|
|
15
|
-
* Covered inputs: the enabled module ids, each module's declared `features`
|
|
16
|
-
* (which drive `filterGrantsByEnabledModules`, including the `*` superadmin
|
|
17
|
-
* expansion and off-convention prefixes), and every JSON-serializable field
|
|
18
|
-
* of the backend route manifest.
|
|
19
|
-
*
|
|
20
|
-
* NOT covered: a route's `icon`. It is a React element whose `type` is a
|
|
21
|
-
* function, which `JSON.stringify` drops — two different icon components
|
|
22
|
-
* serialize identically. Swapping an icon heals on the caller's TTL, not on
|
|
23
|
-
* the fingerprint. Callers MUST therefore still pass a `ttl`.
|
|
24
|
-
*/
|
|
25
|
-
import { createHash } from 'node:crypto'
|
|
26
|
-
import type { Module } from '../../modules/registry'
|
|
27
|
-
import { getBackendRouteManifests } from '../../modules/registry'
|
|
28
|
-
import { getModules } from './registry'
|
|
29
|
-
|
|
30
|
-
const UNKNOWN_FINGERPRINT = 'unknown'
|
|
31
|
-
|
|
32
|
-
let cachedFingerprint: string | null = null
|
|
33
|
-
let cachedManifestsRef: unknown = null
|
|
34
|
-
let cachedModulesRef: unknown = null
|
|
35
|
-
|
|
36
|
-
function readModulesRef(): readonly Module[] | null {
|
|
37
|
-
try {
|
|
38
|
-
return getModules()
|
|
39
|
-
} catch {
|
|
40
|
-
return null
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function byCodeUnit(a: string, b: string): number {
|
|
45
|
-
return a < b ? -1 : a > b ? 1 : 0
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function describeModules(modules: readonly Module[] | null): string[] {
|
|
49
|
-
if (!modules) return []
|
|
50
|
-
return modules
|
|
51
|
-
.map((mod) => {
|
|
52
|
-
const features = (Array.isArray(mod.features) ? mod.features : [])
|
|
53
|
-
.filter((feature) => feature && typeof feature.id === 'string' && feature.id.length > 0)
|
|
54
|
-
.map((feature) => `${feature.id}:${feature.module || mod.id}`)
|
|
55
|
-
.sort(byCodeUnit)
|
|
56
|
-
return JSON.stringify({ id: mod.id, features })
|
|
57
|
-
})
|
|
58
|
-
.sort(byCodeUnit)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function getModuleSurfaceFingerprint(): string {
|
|
62
|
-
const manifests = getBackendRouteManifests()
|
|
63
|
-
const modulesRef = readModulesRef()
|
|
64
|
-
if (cachedFingerprint !== null && cachedManifestsRef === manifests && cachedModulesRef === modulesRef) {
|
|
65
|
-
return cachedFingerprint
|
|
66
|
-
}
|
|
67
|
-
// The manifest carries third-party route metadata and module-scope React
|
|
68
|
-
// elements, so serialization is not fully under this repo's control. Every
|
|
69
|
-
// other fallible step in the nav handler degrades rather than throws, and a
|
|
70
|
-
// throw here would blank the entire admin chrome — fall back to a constant,
|
|
71
|
-
// which is exactly the pre-fingerprint behavior.
|
|
72
|
-
let fingerprint: string
|
|
73
|
-
try {
|
|
74
|
-
const modules = describeModules(modulesRef)
|
|
75
|
-
const routes = manifests.map((route) => JSON.stringify(route)).sort(byCodeUnit)
|
|
76
|
-
fingerprint = createHash('sha1')
|
|
77
|
-
.update(JSON.stringify({ modules, routes }))
|
|
78
|
-
.digest('hex')
|
|
79
|
-
.slice(0, 12)
|
|
80
|
-
} catch {
|
|
81
|
-
fingerprint = UNKNOWN_FINGERPRINT
|
|
82
|
-
}
|
|
83
|
-
cachedManifestsRef = manifests
|
|
84
|
-
cachedModulesRef = modulesRef
|
|
85
|
-
cachedFingerprint = fingerprint
|
|
86
|
-
return fingerprint
|
|
87
|
-
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
normalizeColumnName,
|
|
3
|
-
resetCiphertextLikeWarnCache,
|
|
4
|
-
warnOnCiphertextLikeFallback,
|
|
5
|
-
} from '../ciphertext-search-warning'
|
|
6
|
-
import { createLogger } from '../../logger'
|
|
7
|
-
|
|
8
|
-
jest.mock('../../logger', () => {
|
|
9
|
-
const warn = jest.fn()
|
|
10
|
-
const debug = jest.fn()
|
|
11
|
-
const child = jest.fn(() => ({ warn, debug }))
|
|
12
|
-
return { createLogger: jest.fn(() => ({ child })), __warn: warn, __debug: debug }
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
const loggerModule = jest.requireMock('../../logger') as { __warn: jest.Mock; __debug: jest.Mock }
|
|
16
|
-
|
|
17
|
-
function createService(encryptedFields: string[], overrides: Record<string, unknown> = {}) {
|
|
18
|
-
return {
|
|
19
|
-
isEnabled: () => true,
|
|
20
|
-
getEncryptedFieldNames: jest.fn(async () => encryptedFields),
|
|
21
|
-
...overrides,
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('warnOnCiphertextLikeFallback', () => {
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
resetCiphertextLikeWarnCache()
|
|
28
|
-
loggerModule.__warn.mockClear()
|
|
29
|
-
loggerModule.__debug.mockClear()
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('warns when the filtered column is covered by an encryption map', async () => {
|
|
33
|
-
const service = createService(['email', 'display_name'])
|
|
34
|
-
await warnOnCiphertextLikeFallback({
|
|
35
|
-
entity: 'customers:customer_entity',
|
|
36
|
-
fields: ['display_name'],
|
|
37
|
-
tenantId: 'tenant-1',
|
|
38
|
-
reason: 'no-search-tokens',
|
|
39
|
-
service,
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
expect(loggerModule.__warn).toHaveBeenCalledTimes(1)
|
|
43
|
-
const [message, payload] = loggerModule.__warn.mock.calls[0]
|
|
44
|
-
expect(message).toBe('Text search filter cannot match an encrypted column')
|
|
45
|
-
expect(payload).toMatchObject({
|
|
46
|
-
entity: 'customers:customer_entity',
|
|
47
|
-
field: 'display_name',
|
|
48
|
-
reason: 'no-search-tokens',
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('stays quiet for columns outside the encryption map', async () => {
|
|
53
|
-
await warnOnCiphertextLikeFallback({
|
|
54
|
-
entity: 'currencies:currency',
|
|
55
|
-
fields: ['code', 'symbol'],
|
|
56
|
-
tenantId: 'tenant-1',
|
|
57
|
-
reason: 'no-search-tokens',
|
|
58
|
-
service: createService(['unrelated']),
|
|
59
|
-
})
|
|
60
|
-
expect(loggerModule.__warn).not.toHaveBeenCalled()
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('does not cache a non-encrypted lookup as already warned', async () => {
|
|
64
|
-
await warnOnCiphertextLikeFallback({
|
|
65
|
-
entity: 'auth:user', fields: ['email'], tenantId: 'tenant-1', reason: 'no-search-tokens', service: createService([]),
|
|
66
|
-
})
|
|
67
|
-
await warnOnCiphertextLikeFallback({
|
|
68
|
-
entity: 'auth:user', fields: ['email'], tenantId: 'tenant-1', reason: 'no-search-tokens', service: createService(['email']),
|
|
69
|
-
})
|
|
70
|
-
expect(loggerModule.__warn).toHaveBeenCalledTimes(1)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('matches camelCase filter fields against snake_case map entries', async () => {
|
|
74
|
-
await warnOnCiphertextLikeFallback({
|
|
75
|
-
entity: 'checkout:checkout_transaction',
|
|
76
|
-
fields: ['firstName'],
|
|
77
|
-
tenantId: null,
|
|
78
|
-
reason: 'no-search-tokens',
|
|
79
|
-
service: createService(['first_name']),
|
|
80
|
-
})
|
|
81
|
-
expect(loggerModule.__warn).toHaveBeenCalledTimes(1)
|
|
82
|
-
expect(loggerModule.__warn.mock.calls[0][1]).toMatchObject({ field: 'first_name' })
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('warns once per entity, tenant and field', async () => {
|
|
86
|
-
const service = createService(['email'])
|
|
87
|
-
const params = {
|
|
88
|
-
entity: 'auth:user',
|
|
89
|
-
fields: ['email'],
|
|
90
|
-
tenantId: 'tenant-1',
|
|
91
|
-
reason: 'no-search-tokens' as const,
|
|
92
|
-
service,
|
|
93
|
-
}
|
|
94
|
-
await warnOnCiphertextLikeFallback(params)
|
|
95
|
-
await warnOnCiphertextLikeFallback(params)
|
|
96
|
-
await warnOnCiphertextLikeFallback(params)
|
|
97
|
-
|
|
98
|
-
expect(loggerModule.__warn).toHaveBeenCalledTimes(1)
|
|
99
|
-
expect(service.getEncryptedFieldNames).toHaveBeenCalledTimes(1)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('warns separately for a different tenant', async () => {
|
|
103
|
-
const service = createService(['email'])
|
|
104
|
-
await warnOnCiphertextLikeFallback({
|
|
105
|
-
entity: 'auth:user', fields: ['email'], tenantId: 'tenant-1', reason: 'no-search-tokens', service,
|
|
106
|
-
})
|
|
107
|
-
await warnOnCiphertextLikeFallback({
|
|
108
|
-
entity: 'auth:user', fields: ['email'], tenantId: 'tenant-2', reason: 'no-search-tokens', service,
|
|
109
|
-
})
|
|
110
|
-
expect(loggerModule.__warn).toHaveBeenCalledTimes(2)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('does nothing when encryption is unavailable or disabled', async () => {
|
|
114
|
-
await warnOnCiphertextLikeFallback({
|
|
115
|
-
entity: 'auth:user', fields: ['email'], tenantId: null, reason: 'no-search-tokens', service: null,
|
|
116
|
-
})
|
|
117
|
-
await warnOnCiphertextLikeFallback({
|
|
118
|
-
entity: 'auth:user',
|
|
119
|
-
fields: ['email'],
|
|
120
|
-
tenantId: null,
|
|
121
|
-
reason: 'no-search-tokens',
|
|
122
|
-
service: createService(['email'], { isEnabled: () => false }),
|
|
123
|
-
})
|
|
124
|
-
expect(loggerModule.__warn).not.toHaveBeenCalled()
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('ignores custom-field filters', async () => {
|
|
128
|
-
const service = createService(['cf:secret_note'])
|
|
129
|
-
await warnOnCiphertextLikeFallback({
|
|
130
|
-
entity: 'example:todo', fields: ['cf:secret_note'], tenantId: null, reason: 'no-search-tokens', service,
|
|
131
|
-
})
|
|
132
|
-
expect(service.getEncryptedFieldNames).not.toHaveBeenCalled()
|
|
133
|
-
expect(loggerModule.__warn).not.toHaveBeenCalled()
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('reports the disabled-search reason with its own hint', async () => {
|
|
137
|
-
await warnOnCiphertextLikeFallback({
|
|
138
|
-
entity: 'auth:user', fields: ['email'], tenantId: null, reason: 'search-disabled', service: createService(['email']),
|
|
139
|
-
})
|
|
140
|
-
expect(loggerModule.__warn.mock.calls[0][1]).toMatchObject({ reason: 'search-disabled' })
|
|
141
|
-
expect(loggerModule.__warn.mock.calls[0][1].hint).toContain('OM_SEARCH_ENABLED')
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('reports values that are too short to tokenize', async () => {
|
|
145
|
-
await warnOnCiphertextLikeFallback({
|
|
146
|
-
entity: 'auth:user', fields: ['email'], tenantId: null, reason: 'no-indexable-tokens', service: createService(['email']),
|
|
147
|
-
})
|
|
148
|
-
expect(loggerModule.__warn.mock.calls[0][1]).toMatchObject({ reason: 'no-indexable-tokens' })
|
|
149
|
-
expect(loggerModule.__warn.mock.calls[0][1].hint).toContain('OM_SEARCH_MIN_LEN')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('never rethrows when the encryption lookup fails', async () => {
|
|
153
|
-
const service = {
|
|
154
|
-
isEnabled: () => true,
|
|
155
|
-
getEncryptedFieldNames: jest.fn(async () => { throw new Error('kms down') }),
|
|
156
|
-
}
|
|
157
|
-
await expect(warnOnCiphertextLikeFallback({
|
|
158
|
-
entity: 'auth:user', fields: ['email'], tenantId: null, reason: 'no-search-tokens', service,
|
|
159
|
-
})).resolves.toBeUndefined()
|
|
160
|
-
expect(loggerModule.__warn).not.toHaveBeenCalled()
|
|
161
|
-
expect(loggerModule.__debug).toHaveBeenCalled()
|
|
162
|
-
})
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
describe('normalizeColumnName', () => {
|
|
166
|
-
it('converts camelCase to snake_case and lowercases', () => {
|
|
167
|
-
expect(normalizeColumnName('firstName')).toBe('first_name')
|
|
168
|
-
expect(normalizeColumnName('primaryEmail')).toBe('primary_email')
|
|
169
|
-
expect(normalizeColumnName('display_name')).toBe('display_name')
|
|
170
|
-
expect(normalizeColumnName('Email')).toBe('email')
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
describe('logger wiring', () => {
|
|
175
|
-
it('uses the shared query logger namespace', () => {
|
|
176
|
-
expect(createLogger).toHaveBeenCalledWith('shared')
|
|
177
|
-
})
|
|
178
|
-
})
|