@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,171 +0,0 @@
|
|
|
1
|
-
// Regression coverage for the CRUD list cache key missing a caller-identity
|
|
2
|
-
// segment: two callers in the same tenant/org scope requesting the identical
|
|
3
|
-
// URL must NOT share cache entries, because list payloads can vary per caller —
|
|
4
|
-
// buildFilters may narrow by ctx.auth (e.g. ?mine=true on staff time-projects,
|
|
5
|
-
// the isSuperAdmin branch on scheduler jobs), before-interceptor query rewrites
|
|
6
|
-
// are feature-gated per user, and afterList/after-interceptor output is
|
|
7
|
-
// embedded in the stored payload.
|
|
8
|
-
|
|
9
|
-
jest.mock('@open-mercato/cache', () => ({
|
|
10
|
-
runWithCacheTenant: async (_tenantId: string | null, fn: () => Promise<unknown>) => fn(),
|
|
11
|
-
}), { virtual: true })
|
|
12
|
-
|
|
13
|
-
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
14
|
-
import { registerApiInterceptors } from '@open-mercato/shared/lib/crud/interceptor-registry'
|
|
15
|
-
import { z } from 'zod'
|
|
16
|
-
|
|
17
|
-
const defaultOrganizationId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
|
|
18
|
-
const defaultTenantId = '123e4567-e89b-12d3-a456-426614174000'
|
|
19
|
-
|
|
20
|
-
let mockAuthSub = 'user-a'
|
|
21
|
-
let mockAuthKeyId: string | null = null
|
|
22
|
-
|
|
23
|
-
const em = {}
|
|
24
|
-
|
|
25
|
-
// Returns rows derived from the per-caller filter buildFilters produced, so a
|
|
26
|
-
// cross-user cache hit is observable as one user's rows in another's response.
|
|
27
|
-
const queryEngine = {
|
|
28
|
-
query: jest.fn(async (_entity: string, opts: Record<string, any>) => {
|
|
29
|
-
const owner = opts?.filters?.owner_user_id?.$eq ?? 'unfiltered'
|
|
30
|
-
return {
|
|
31
|
-
items: [{ id: `todo-of-${owner}`, title: `Owned by ${owner}`, organization_id: defaultOrganizationId, tenant_id: defaultTenantId }],
|
|
32
|
-
total: 1,
|
|
33
|
-
}
|
|
34
|
-
}),
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const store = new Map<string, unknown>()
|
|
38
|
-
const cache = {
|
|
39
|
-
get: jest.fn(async (key: string) => (store.has(key) ? store.get(key) : null)),
|
|
40
|
-
set: jest.fn(async (key: string, value: unknown) => { store.set(key, value) }),
|
|
41
|
-
delete: jest.fn(async (key: string) => { store.delete(key) }),
|
|
42
|
-
deleteByTags: jest.fn(async () => 0),
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const accessLogService = { log: jest.fn(async () => {}) }
|
|
46
|
-
|
|
47
|
-
jest.mock('@open-mercato/shared/lib/di/container', () => ({
|
|
48
|
-
createRequestContainer: async () => ({
|
|
49
|
-
resolve: (name: string) => ({
|
|
50
|
-
em,
|
|
51
|
-
queryEngine,
|
|
52
|
-
cache,
|
|
53
|
-
accessLogService,
|
|
54
|
-
} as any)[name],
|
|
55
|
-
}),
|
|
56
|
-
}))
|
|
57
|
-
|
|
58
|
-
jest.mock('@open-mercato/shared/lib/auth/server', () => {
|
|
59
|
-
const buildAuth = () => ({
|
|
60
|
-
sub: mockAuthSub,
|
|
61
|
-
keyId: mockAuthKeyId ?? undefined,
|
|
62
|
-
orgId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
|
|
63
|
-
tenantId: '123e4567-e89b-12d3-a456-426614174000',
|
|
64
|
-
roles: ['admin'],
|
|
65
|
-
})
|
|
66
|
-
return {
|
|
67
|
-
getAuthFromCookies: async () => buildAuth(),
|
|
68
|
-
getAuthFromRequest: async () => buildAuth(),
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
jest.mock('@open-mercato/core/modules/directory/utils/organizationScope', () => ({
|
|
73
|
-
resolveOrganizationScopeForRequest: jest.fn(async () => ({
|
|
74
|
-
selectedId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
|
|
75
|
-
filterIds: ['aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'],
|
|
76
|
-
allowedIds: ['aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'],
|
|
77
|
-
tenantId: '123e4567-e89b-12d3-a456-426614174000',
|
|
78
|
-
})),
|
|
79
|
-
}))
|
|
80
|
-
|
|
81
|
-
jest.mock('@open-mercato/core/modules/entities/lib/helpers', () => ({
|
|
82
|
-
setRecordCustomFields: jest.fn(async () => {}),
|
|
83
|
-
}))
|
|
84
|
-
|
|
85
|
-
class Todo {}
|
|
86
|
-
|
|
87
|
-
const querySchema = z.object({
|
|
88
|
-
page: z.coerce.number().default(1),
|
|
89
|
-
pageSize: z.coerce.number().default(50),
|
|
90
|
-
sortField: z.string().default('id'),
|
|
91
|
-
sortDir: z.enum(['asc', 'desc']).default('asc'),
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
const route = makeCrudRoute({
|
|
95
|
-
metadata: { GET: { requireAuth: true } },
|
|
96
|
-
orm: { entity: Todo, idField: 'id', orgField: 'organizationId', tenantField: 'tenantId', softDeleteField: 'deletedAt' },
|
|
97
|
-
indexer: { entityType: 'example.todo' },
|
|
98
|
-
list: {
|
|
99
|
-
schema: querySchema,
|
|
100
|
-
entityId: 'example.todo',
|
|
101
|
-
fields: ['id', 'title'],
|
|
102
|
-
sortFieldMap: { id: 'id', title: 'title' },
|
|
103
|
-
buildFilters: (_query: any, ctx: any) => ({ owner_user_id: { $eq: ctx.auth?.sub ?? null } } as any),
|
|
104
|
-
transformItem: (i: any) => ({ id: i.id, title: i.title }),
|
|
105
|
-
},
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
const url = 'http://x/api/example/todos?page=1&pageSize=10&sortField=id&sortDir=asc'
|
|
109
|
-
|
|
110
|
-
describe('CRUD Factory — list cache is partitioned per caller identity', () => {
|
|
111
|
-
const previousCacheFlag = process.env.ENABLE_CRUD_API_CACHE
|
|
112
|
-
|
|
113
|
-
beforeAll(() => {
|
|
114
|
-
process.env.ENABLE_CRUD_API_CACHE = 'true'
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
afterAll(() => {
|
|
118
|
-
if (previousCacheFlag === undefined) delete process.env.ENABLE_CRUD_API_CACHE
|
|
119
|
-
else process.env.ENABLE_CRUD_API_CACHE = previousCacheFlag
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
beforeEach(() => {
|
|
123
|
-
jest.clearAllMocks()
|
|
124
|
-
store.clear()
|
|
125
|
-
mockAuthSub = 'user-a'
|
|
126
|
-
mockAuthKeyId = null
|
|
127
|
-
registerApiInterceptors([])
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('caches per user and still serves same-user repeat requests from cache', async () => {
|
|
131
|
-
const first = await route.GET(new Request(url))
|
|
132
|
-
expect(first.status).toBe(200)
|
|
133
|
-
expect(first.headers.get('x-om-cache')).toBe('miss')
|
|
134
|
-
expect((await first.json()).items[0].id).toBe('todo-of-user-a')
|
|
135
|
-
|
|
136
|
-
const second = await route.GET(new Request(url))
|
|
137
|
-
expect(second.headers.get('x-om-cache')).toBe('hit')
|
|
138
|
-
expect((await second.json()).items[0].id).toBe('todo-of-user-a')
|
|
139
|
-
expect(queryEngine.query).toHaveBeenCalledTimes(1)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('does not serve one user\'s cached rows to another user on the identical URL', async () => {
|
|
143
|
-
mockAuthSub = 'user-a'
|
|
144
|
-
const aRes = await route.GET(new Request(url))
|
|
145
|
-
expect((await aRes.json()).items[0].id).toBe('todo-of-user-a')
|
|
146
|
-
expect(store.size).toBe(1)
|
|
147
|
-
|
|
148
|
-
mockAuthSub = 'user-b'
|
|
149
|
-
const bRes = await route.GET(new Request(url))
|
|
150
|
-
expect(bRes.headers.get('x-om-cache')).toBe('miss')
|
|
151
|
-
expect((await bRes.json()).items[0].id).toBe('todo-of-user-b')
|
|
152
|
-
expect(store.size).toBe(2)
|
|
153
|
-
|
|
154
|
-
mockAuthSub = 'user-a'
|
|
155
|
-
const aAgain = await route.GET(new Request(url))
|
|
156
|
-
expect(aAgain.headers.get('x-om-cache')).toBe('hit')
|
|
157
|
-
expect((await aAgain.json()).items[0].id).toBe('todo-of-user-a')
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('keys every entry with an identity segment, preferring the API key id over the user id', async () => {
|
|
161
|
-
await route.GET(new Request(url))
|
|
162
|
-
const userKey = Array.from(store.keys())[0] as string
|
|
163
|
-
expect(userKey).toContain('user:user-a')
|
|
164
|
-
|
|
165
|
-
store.clear()
|
|
166
|
-
mockAuthKeyId = 'api-key-9'
|
|
167
|
-
await route.GET(new Request(url))
|
|
168
|
-
const apiKeyKey = Array.from(store.keys())[0] as string
|
|
169
|
-
expect(apiKeyKey).toContain('user:api-key-9')
|
|
170
|
-
})
|
|
171
|
-
})
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
__resetAlwaysConsistentCacheForTests,
|
|
3
|
-
isReadProjectionAlwaysConsistent,
|
|
4
|
-
parseAlwaysConsistentEnv,
|
|
5
|
-
} from '../consistency'
|
|
6
|
-
|
|
7
|
-
describe('read projection consistency flag', () => {
|
|
8
|
-
const originalEnv = process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
if (originalEnv === undefined) {
|
|
12
|
-
delete process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT
|
|
13
|
-
} else {
|
|
14
|
-
process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT = originalEnv
|
|
15
|
-
}
|
|
16
|
-
__resetAlwaysConsistentCacheForTests()
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it.each([undefined, null, '', ' ', 'off', 'false', '0', 'no', 'disabled', 'none', 'unexpected'])(
|
|
20
|
-
'parses %p as OFF',
|
|
21
|
-
(raw) => {
|
|
22
|
-
expect(parseAlwaysConsistentEnv(raw)).toBe(false)
|
|
23
|
-
},
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
it.each(['on', 'true', '1', 'yes', 'enabled'])('parses %p as ON', (raw) => {
|
|
27
|
-
expect(parseAlwaysConsistentEnv(raw)).toBe(true)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('memoizes the env value until reset for tests', () => {
|
|
31
|
-
process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT = 'on'
|
|
32
|
-
expect(isReadProjectionAlwaysConsistent()).toBe(true)
|
|
33
|
-
process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT = 'off'
|
|
34
|
-
expect(isReadProjectionAlwaysConsistent()).toBe(true)
|
|
35
|
-
__resetAlwaysConsistentCacheForTests()
|
|
36
|
-
expect(isReadProjectionAlwaysConsistent()).toBe(false)
|
|
37
|
-
})
|
|
38
|
-
})
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { parseBooleanWithDefault } from '../boolean'
|
|
2
|
-
|
|
3
|
-
let alwaysConsistentFlag: boolean | null = null
|
|
4
|
-
|
|
5
|
-
export function parseAlwaysConsistentEnv(raw: string | undefined | null): boolean {
|
|
6
|
-
return parseBooleanWithDefault(raw, false)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function isReadProjectionAlwaysConsistent(): boolean {
|
|
10
|
-
if (alwaysConsistentFlag !== null) return alwaysConsistentFlag
|
|
11
|
-
alwaysConsistentFlag = parseAlwaysConsistentEnv(process.env.OM_CACHE_SAFETY_ALWAYS_CONSISTENT)
|
|
12
|
-
return alwaysConsistentFlag
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function __resetAlwaysConsistentCacheForTests(): void {
|
|
16
|
-
alwaysConsistentFlag = null
|
|
17
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { listEntityMetadata, listEntityMetadataFromRegistry } from '../entityMetadata'
|
|
2
|
-
|
|
3
|
-
const personMeta = { className: 'Person', tableName: 'people' }
|
|
4
|
-
const orderMeta = { className: 'Order', tableName: 'orders' }
|
|
5
|
-
|
|
6
|
-
describe('listEntityMetadataFromRegistry', () => {
|
|
7
|
-
it('reads the MikroORM v7 Map returned by getAll()', () => {
|
|
8
|
-
const registry = { getAll: () => new Map([[Object, personMeta], [Array, orderMeta]]) }
|
|
9
|
-
expect(listEntityMetadataFromRegistry(registry)).toEqual([personMeta, orderMeta])
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('reads the MikroORM v6 dictionary returned by getAll()', () => {
|
|
13
|
-
const registry = { getAll: () => ({ Person: personMeta, Order: orderMeta }) }
|
|
14
|
-
expect(listEntityMetadataFromRegistry(registry)).toEqual([personMeta, orderMeta])
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('falls back to the metadata property when getAll() yields nothing', () => {
|
|
18
|
-
const registry = { getAll: () => new Map(), metadata: { Person: personMeta } }
|
|
19
|
-
expect(listEntityMetadataFromRegistry(registry)).toEqual([personMeta])
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('returns an empty array for a missing registry', () => {
|
|
23
|
-
expect(listEntityMetadataFromRegistry(null)).toEqual([])
|
|
24
|
-
expect(listEntityMetadataFromRegistry({})).toEqual([])
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
describe('listEntityMetadata', () => {
|
|
29
|
-
it('resolves the registry through getMetadata()', () => {
|
|
30
|
-
const em = { getMetadata: () => ({ getAll: () => new Map([[Object, personMeta]]) }) }
|
|
31
|
-
expect(listEntityMetadata(em)).toEqual([personMeta])
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('returns an empty array when the source exposes no registry', () => {
|
|
35
|
-
expect(listEntityMetadata({})).toEqual([])
|
|
36
|
-
expect(listEntityMetadata(undefined)).toEqual([])
|
|
37
|
-
})
|
|
38
|
-
})
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { isTransientDbError, isUniqueViolation } from '../pg-errors'
|
|
2
|
-
|
|
3
|
-
describe('isTransientDbError', () => {
|
|
4
|
-
it('is true for the max_connections SQLSTATE', () => {
|
|
5
|
-
expect(isTransientDbError({ code: '53300' })).toBe(true)
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
it('is true for connection-family SQLSTATEs', () => {
|
|
9
|
-
for (const code of ['53400', '57P01', '57P02', '57P03', '08000', '08001', '08003', '08006']) {
|
|
10
|
-
expect(isTransientDbError({ code })).toBe(true)
|
|
11
|
-
}
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('is true for driver/ORM-wrapped connection messages that drop the SQLSTATE', () => {
|
|
15
|
-
expect(isTransientDbError(new Error('sorry, too many clients already'))).toBe(true)
|
|
16
|
-
expect(isTransientDbError(new Error('Knex: Timeout acquiring a connection. The pool is probably full.'))).toBe(true)
|
|
17
|
-
expect(isTransientDbError(new Error('Unable to acquire a connection'))).toBe(true)
|
|
18
|
-
expect(isTransientDbError(new Error('Connection terminated unexpectedly'))).toBe(true)
|
|
19
|
-
expect(isTransientDbError(new Error('the database system is starting up'))).toBe(true)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('is false for bare socket errors with no Postgres signal (avoids false positives)', () => {
|
|
23
|
-
// Generic socket failures can come from any outbound connection (HTTP, cache,
|
|
24
|
-
// queue), so they must NOT be attributed to the database.
|
|
25
|
-
expect(isTransientDbError({ code: 'ECONNREFUSED' })).toBe(false)
|
|
26
|
-
expect(isTransientDbError({ code: 'ETIMEDOUT' })).toBe(false)
|
|
27
|
-
expect(isTransientDbError(Object.assign(new Error('read ECONNRESET'), { code: 'ECONNRESET' }))).toBe(false)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('is false for a unique violation and other query-level errors', () => {
|
|
31
|
-
expect(isTransientDbError({ code: '23505' })).toBe(false)
|
|
32
|
-
expect(isTransientDbError({ code: '40P01' })).toBe(false) // deadlock — not a connection failure
|
|
33
|
-
expect(isTransientDbError({ code: '40001' })).toBe(false) // serialization — not a connection failure
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('is false for non-DB and empty errors', () => {
|
|
37
|
-
expect(isTransientDbError(new Error('something unrelated broke'))).toBe(false)
|
|
38
|
-
expect(isTransientDbError(null)).toBe(false)
|
|
39
|
-
expect(isTransientDbError(undefined)).toBe(false)
|
|
40
|
-
expect(isTransientDbError('a string')).toBe(false)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('does not overlap with isUniqueViolation', () => {
|
|
44
|
-
const uniqueErr = { code: '23505', message: 'duplicate key value violates unique constraint' }
|
|
45
|
-
expect(isUniqueViolation(uniqueErr)).toBe(true)
|
|
46
|
-
expect(isTransientDbError(uniqueErr)).toBe(false)
|
|
47
|
-
})
|
|
48
|
-
})
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { EntityMetadata } from '@mikro-orm/core'
|
|
2
|
-
|
|
3
|
-
type MetadataRegistryLike = {
|
|
4
|
-
getAll?: () => unknown
|
|
5
|
-
metadata?: unknown
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
type MetadataHolder = {
|
|
9
|
-
getMetadata?: () => unknown
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* MikroORM v7 returns a `Map` from `MetadataStorage.getAll()`; v6 returned a plain
|
|
14
|
-
* dictionary. `Object.values(someMap)` evaluates to `[]`, so a v6-era read of the
|
|
15
|
-
* registry silently reports zero entities instead of failing loudly.
|
|
16
|
-
*/
|
|
17
|
-
function toMetadataList(value: unknown): EntityMetadata[] {
|
|
18
|
-
if (!value) return []
|
|
19
|
-
if (value instanceof Map) return Array.from(value.values()) as EntityMetadata[]
|
|
20
|
-
if (Array.isArray(value)) return value as EntityMetadata[]
|
|
21
|
-
if (typeof value === 'object') return Object.values(value as Record<string, EntityMetadata>)
|
|
22
|
-
return []
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function listEntityMetadataFromRegistry(registry: unknown): EntityMetadata[] {
|
|
26
|
-
const source = registry as MetadataRegistryLike | null | undefined
|
|
27
|
-
if (!source) return []
|
|
28
|
-
if (typeof source.getAll === 'function') {
|
|
29
|
-
const entries = toMetadataList(source.getAll())
|
|
30
|
-
if (entries.length) return entries
|
|
31
|
-
}
|
|
32
|
-
return toMetadataList(source.metadata)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* List every discovered entity metadata from an `EntityManager` or `MikroORM` instance.
|
|
37
|
-
* Returns an empty array when the source exposes no metadata registry.
|
|
38
|
-
*/
|
|
39
|
-
export function listEntityMetadata(source: unknown): EntityMetadata[] {
|
|
40
|
-
const holder = source as MetadataHolder | null | undefined
|
|
41
|
-
if (!holder || typeof holder.getMetadata !== 'function') return []
|
|
42
|
-
return listEntityMetadataFromRegistry(holder.getMetadata())
|
|
43
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
// The app-level DI override module (`@/di`) is OPTIONAL: most apps ship no
|
|
2
|
-
// src/di.ts, and packages contexts cannot resolve the alias at all. Its
|
|
3
|
-
// absence must not produce warnings — only real load/register failures do
|
|
4
|
-
// (covered in container-app-di.test.ts). This file deliberately does NOT
|
|
5
|
-
// mock '@/di', so the import fails with module-not-found, exercising the
|
|
6
|
-
// quiet branch.
|
|
7
|
-
|
|
8
|
-
jest.mock(
|
|
9
|
-
'@open-mercato/shared/lib/db/mikro',
|
|
10
|
-
() => {
|
|
11
|
-
const baseEm: any = {
|
|
12
|
-
fork: () => baseEm,
|
|
13
|
-
getRepository: () => ({ find: async () => [], findOne: async () => null }),
|
|
14
|
-
}
|
|
15
|
-
return {
|
|
16
|
-
__esModule: true,
|
|
17
|
-
getOrm: async () => ({ em: baseEm }),
|
|
18
|
-
getOrmEntities: () => [],
|
|
19
|
-
registerOrmEntities: () => {},
|
|
20
|
-
}
|
|
21
|
-
},
|
|
22
|
-
{ virtual: false },
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
jest.mock(
|
|
26
|
-
'@mikro-orm/core',
|
|
27
|
-
() => ({
|
|
28
|
-
__esModule: true,
|
|
29
|
-
RequestContext: { getEntityManager: () => null },
|
|
30
|
-
}),
|
|
31
|
-
{ virtual: true },
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
jest.mock(
|
|
35
|
-
'@open-mercato/shared/lib/query/engine',
|
|
36
|
-
() => ({ __esModule: true, BasicQueryEngine: class {} }),
|
|
37
|
-
{ virtual: false },
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
jest.mock(
|
|
41
|
-
'@open-mercato/shared/lib/data/engine',
|
|
42
|
-
() => ({ __esModule: true, DefaultDataEngine: class {} }),
|
|
43
|
-
{ virtual: false },
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
jest.mock(
|
|
47
|
-
'@open-mercato/shared/lib/commands',
|
|
48
|
-
() => ({
|
|
49
|
-
__esModule: true,
|
|
50
|
-
commandRegistry: {},
|
|
51
|
-
CommandBus: class { constructor() {} },
|
|
52
|
-
}),
|
|
53
|
-
{ virtual: false },
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
jest.mock(
|
|
57
|
-
'@open-mercato/shared/modules/overrides',
|
|
58
|
-
() => ({
|
|
59
|
-
__esModule: true,
|
|
60
|
-
applyDiOverridesToContainer: () => {},
|
|
61
|
-
}),
|
|
62
|
-
{ virtual: false },
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
// Keep this suite hermetic: without this mock the REAL core bootstrap module
|
|
66
|
-
// loads (and runs its module-level side effects) whenever generated files
|
|
67
|
-
// exist on disk, polluting globalThis state shared with sibling test files.
|
|
68
|
-
jest.mock(
|
|
69
|
-
'@open-mercato/core/bootstrap',
|
|
70
|
-
() => ({
|
|
71
|
-
__esModule: true,
|
|
72
|
-
bootstrap: async () => {},
|
|
73
|
-
}),
|
|
74
|
-
{ virtual: true },
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
jest.mock(
|
|
78
|
-
'@open-mercato/shared/lib/encryption/subscriber',
|
|
79
|
-
() => ({
|
|
80
|
-
__esModule: true,
|
|
81
|
-
registerTenantEncryptionSubscriber: () => {},
|
|
82
|
-
}),
|
|
83
|
-
{ virtual: true },
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
const mockWarn = jest.fn()
|
|
87
|
-
const mockDebug = jest.fn()
|
|
88
|
-
jest.mock('../../logger', () => ({
|
|
89
|
-
createLogger: () => ({
|
|
90
|
-
debug: (...args: unknown[]) => mockDebug(...args),
|
|
91
|
-
info: jest.fn(),
|
|
92
|
-
warn: (...args: unknown[]) => mockWarn(...args),
|
|
93
|
-
error: jest.fn(),
|
|
94
|
-
child() { return this },
|
|
95
|
-
}),
|
|
96
|
-
}))
|
|
97
|
-
|
|
98
|
-
const {
|
|
99
|
-
createRequestContainer,
|
|
100
|
-
registerAppDiRegistrar,
|
|
101
|
-
registerDiRegistrars,
|
|
102
|
-
resetBootstrapCache,
|
|
103
|
-
} = require('@open-mercato/shared/lib/di/container')
|
|
104
|
-
|
|
105
|
-
describe('app-level DI override hook when @/di is absent', () => {
|
|
106
|
-
beforeEach(() => {
|
|
107
|
-
resetBootstrapCache()
|
|
108
|
-
registerAppDiRegistrar(null)
|
|
109
|
-
registerDiRegistrars([])
|
|
110
|
-
mockWarn.mockClear()
|
|
111
|
-
mockDebug.mockClear()
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('creates the container without warning', async () => {
|
|
115
|
-
const firstContainer = await createRequestContainer()
|
|
116
|
-
const secondContainer = await createRequestContainer()
|
|
117
|
-
expect(firstContainer).toBeDefined()
|
|
118
|
-
expect(secondContainer).toBeDefined()
|
|
119
|
-
expect(mockWarn).not.toHaveBeenCalled()
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
// Worker and CLI processes create one request container per job against built package
|
|
123
|
-
// output, where the app's `@/` alias does not exist. Retrying the doomed import per
|
|
124
|
-
// container repeats a failed module resolution — and its log line — once per job.
|
|
125
|
-
it('stops retrying the unresolvable @/di import after the first container', async () => {
|
|
126
|
-
await createRequestContainer()
|
|
127
|
-
await createRequestContainer()
|
|
128
|
-
await createRequestContainer()
|
|
129
|
-
|
|
130
|
-
const unresolvableLogs = mockDebug.mock.calls.filter(
|
|
131
|
-
([message]) => message === 'App-level DI override module (@/di) not resolvable; skipping',
|
|
132
|
-
)
|
|
133
|
-
expect(unresolvableLogs).toHaveLength(1)
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('warns once when @/di fails because a nested app alias is missing', async () => {
|
|
137
|
-
const nestedAliasError = Object.assign(new Error("Cannot find module '@/di/helpers'"), {
|
|
138
|
-
code: 'MODULE_NOT_FOUND',
|
|
139
|
-
})
|
|
140
|
-
jest.doMock('@/di', () => {
|
|
141
|
-
throw nestedAliasError
|
|
142
|
-
}, { virtual: true })
|
|
143
|
-
|
|
144
|
-
const firstContainer = await createRequestContainer()
|
|
145
|
-
const secondContainer = await createRequestContainer()
|
|
146
|
-
|
|
147
|
-
expect(firstContainer).toBeDefined()
|
|
148
|
-
expect(secondContainer).toBeDefined()
|
|
149
|
-
expect(mockWarn).toHaveBeenCalledTimes(1)
|
|
150
|
-
expect(mockWarn).toHaveBeenCalledWith(
|
|
151
|
-
'App-level DI override module (@/di) failed to load; its registrations are skipped',
|
|
152
|
-
{ err: nestedAliasError },
|
|
153
|
-
)
|
|
154
|
-
})
|
|
155
|
-
})
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// Tests for the app-level DI override hook (`src/di.ts` → `@/di`) in
|
|
2
|
-
// `createRequestContainer()`. The hook used to swallow EVERY failure with
|
|
3
|
-
// bare `catch {}` blocks, so a src/di.ts whose register() threw — or a @/di
|
|
4
|
-
// module that failed to load for any reason other than being absent — was
|
|
5
|
-
// skipped without a trace and the documented override point silently did
|
|
6
|
-
// nothing. Absence of the optional module must stay quiet; real failures
|
|
7
|
-
// must be logged.
|
|
8
|
-
|
|
9
|
-
import { asValue } from 'awilix'
|
|
10
|
-
|
|
11
|
-
jest.mock(
|
|
12
|
-
'@open-mercato/shared/lib/db/mikro',
|
|
13
|
-
() => {
|
|
14
|
-
const baseEm: any = {
|
|
15
|
-
fork: () => baseEm,
|
|
16
|
-
getRepository: () => ({ find: async () => [], findOne: async () => null }),
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
__esModule: true,
|
|
20
|
-
getOrm: async () => ({ em: baseEm }),
|
|
21
|
-
getOrmEntities: () => [],
|
|
22
|
-
registerOrmEntities: () => {},
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
{ virtual: false },
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
jest.mock(
|
|
29
|
-
'@mikro-orm/core',
|
|
30
|
-
() => ({
|
|
31
|
-
__esModule: true,
|
|
32
|
-
RequestContext: { getEntityManager: () => null },
|
|
33
|
-
}),
|
|
34
|
-
{ virtual: true },
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
jest.mock(
|
|
38
|
-
'@open-mercato/shared/lib/query/engine',
|
|
39
|
-
() => ({ __esModule: true, BasicQueryEngine: class {} }),
|
|
40
|
-
{ virtual: false },
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
jest.mock(
|
|
44
|
-
'@open-mercato/shared/lib/data/engine',
|
|
45
|
-
() => ({ __esModule: true, DefaultDataEngine: class {} }),
|
|
46
|
-
{ virtual: false },
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
jest.mock(
|
|
50
|
-
'@open-mercato/shared/lib/commands',
|
|
51
|
-
() => ({
|
|
52
|
-
__esModule: true,
|
|
53
|
-
commandRegistry: {},
|
|
54
|
-
CommandBus: class { constructor() {} },
|
|
55
|
-
}),
|
|
56
|
-
{ virtual: false },
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
jest.mock(
|
|
60
|
-
'@open-mercato/shared/modules/overrides',
|
|
61
|
-
() => ({
|
|
62
|
-
__esModule: true,
|
|
63
|
-
applyDiOverridesToContainer: () => {},
|
|
64
|
-
}),
|
|
65
|
-
{ virtual: false },
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
// Keep this suite hermetic: without this mock the REAL core bootstrap module
|
|
69
|
-
// loads (and runs its module-level side effects) whenever generated files
|
|
70
|
-
// exist on disk, polluting globalThis state shared with sibling test files.
|
|
71
|
-
jest.mock(
|
|
72
|
-
'@open-mercato/core/bootstrap',
|
|
73
|
-
() => ({
|
|
74
|
-
__esModule: true,
|
|
75
|
-
bootstrap: async () => {},
|
|
76
|
-
}),
|
|
77
|
-
{ virtual: true },
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
jest.mock(
|
|
81
|
-
'@open-mercato/shared/lib/encryption/subscriber',
|
|
82
|
-
() => ({
|
|
83
|
-
__esModule: true,
|
|
84
|
-
registerTenantEncryptionSubscriber: () => {},
|
|
85
|
-
}),
|
|
86
|
-
{ virtual: true },
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
const mockWarn = jest.fn()
|
|
90
|
-
jest.mock('../../logger', () => ({
|
|
91
|
-
createLogger: () => ({
|
|
92
|
-
debug: jest.fn(),
|
|
93
|
-
info: jest.fn(),
|
|
94
|
-
warn: (...args: unknown[]) => mockWarn(...args),
|
|
95
|
-
error: jest.fn(),
|
|
96
|
-
child() { return this },
|
|
97
|
-
}),
|
|
98
|
-
}))
|
|
99
|
-
|
|
100
|
-
const mockAppDiRegister = jest.fn()
|
|
101
|
-
jest.mock(
|
|
102
|
-
'@/di',
|
|
103
|
-
() => ({
|
|
104
|
-
__esModule: true,
|
|
105
|
-
register: (...args: unknown[]) => mockAppDiRegister(...args),
|
|
106
|
-
}),
|
|
107
|
-
{ virtual: true },
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
const {
|
|
111
|
-
createRequestContainer,
|
|
112
|
-
registerAppDiRegistrar,
|
|
113
|
-
registerDiRegistrars,
|
|
114
|
-
resetBootstrapCache,
|
|
115
|
-
} = require('@open-mercato/shared/lib/di/container')
|
|
116
|
-
|
|
117
|
-
describe('app-level DI override hook (@/di)', () => {
|
|
118
|
-
beforeEach(() => {
|
|
119
|
-
resetBootstrapCache()
|
|
120
|
-
registerAppDiRegistrar(null)
|
|
121
|
-
registerDiRegistrars([])
|
|
122
|
-
mockWarn.mockClear()
|
|
123
|
-
mockAppDiRegister.mockReset()
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('calls register() with the request container and applies its registrations', async () => {
|
|
127
|
-
mockAppDiRegister.mockImplementation((container: any) => {
|
|
128
|
-
container.register({ appOverrideProbe: asValue('from-app-di') })
|
|
129
|
-
})
|
|
130
|
-
const container = await createRequestContainer()
|
|
131
|
-
expect(mockAppDiRegister).toHaveBeenCalledTimes(1)
|
|
132
|
-
expect(mockAppDiRegister).toHaveBeenCalledWith(container)
|
|
133
|
-
expect(container.resolve('appOverrideProbe')).toBe('from-app-di')
|
|
134
|
-
expect(mockWarn).not.toHaveBeenCalled()
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('awaits an async register()', async () => {
|
|
138
|
-
mockAppDiRegister.mockImplementation(async (container: any) => {
|
|
139
|
-
await Promise.resolve()
|
|
140
|
-
container.register({ appOverrideProbe: asValue('async-app-di') })
|
|
141
|
-
})
|
|
142
|
-
const container = await createRequestContainer()
|
|
143
|
-
expect(container.resolve('appOverrideProbe')).toBe('async-app-di')
|
|
144
|
-
expect(mockWarn).not.toHaveBeenCalled()
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('warns instead of silently swallowing when register() throws', async () => {
|
|
148
|
-
mockAppDiRegister.mockImplementation(() => {
|
|
149
|
-
throw new Error('boom from app di')
|
|
150
|
-
})
|
|
151
|
-
const firstContainer = await createRequestContainer()
|
|
152
|
-
const secondContainer = await createRequestContainer()
|
|
153
|
-
expect(firstContainer).toBeDefined()
|
|
154
|
-
expect(secondContainer).toBeDefined()
|
|
155
|
-
expect(mockAppDiRegister).toHaveBeenCalledTimes(2)
|
|
156
|
-
expect(mockWarn).toHaveBeenCalledTimes(1)
|
|
157
|
-
const [message, fields] = mockWarn.mock.calls[0]
|
|
158
|
-
expect(String(message)).toContain('register()')
|
|
159
|
-
expect((fields as { err: Error }).err.message).toBe('boom from app di')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('warns when an async register() rejects', async () => {
|
|
163
|
-
mockAppDiRegister.mockImplementation(async () => {
|
|
164
|
-
throw new Error('async boom from app di')
|
|
165
|
-
})
|
|
166
|
-
const firstContainer = await createRequestContainer()
|
|
167
|
-
const secondContainer = await createRequestContainer()
|
|
168
|
-
expect(firstContainer).toBeDefined()
|
|
169
|
-
expect(secondContainer).toBeDefined()
|
|
170
|
-
expect(mockAppDiRegister).toHaveBeenCalledTimes(2)
|
|
171
|
-
expect(mockWarn).toHaveBeenCalledTimes(1)
|
|
172
|
-
})
|
|
173
|
-
})
|