@open-mercato/shared 0.6.7-develop.6860.1.f796b22e80 → 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 -192
- 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,211 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment node
|
|
3
|
-
*
|
|
4
|
-
* Regression guard: the worker/CLI/scheduler bootstrap path (bootstrapFromAppRoot) must register
|
|
5
|
-
* the app-level DI hook (`src/di.ts`) the same way the Next.js runtime does through its own
|
|
6
|
-
* `src/bootstrap.ts`. Without it, `getAppDiRegistrar()` stays null in those processes, so the app's
|
|
7
|
-
* registrations silently never run there and every request container falls back to a doomed
|
|
8
|
-
* `import('@/di')` — the `@/` alias does not exist in a plain Node ESM process, so it throws
|
|
9
|
-
* ERR_MODULE_NOT_FOUND once per created container (once per queued job).
|
|
10
|
-
*
|
|
11
|
-
* Like the #4327 guard next to it, this test authors both the `.ts` sources and fresh compiled
|
|
12
|
-
* `.mjs` siblings so compileAndImport takes its cache path and never invokes esbuild. The `.mjs`
|
|
13
|
-
* stubs use module.exports because Jest's CJS runtime handles the dynamic import().
|
|
14
|
-
*
|
|
15
|
-
* `bootstrapFromAppRoot` reaches the factory through `import('./factory.js')`, a specifier with no
|
|
16
|
-
* on-disk counterpart under Jest's CJS resolver, so it is mocked virtually.
|
|
17
|
-
*/
|
|
18
|
-
jest.mock('../../logger', () => {
|
|
19
|
-
const logger = {
|
|
20
|
-
debug: jest.fn(),
|
|
21
|
-
info: jest.fn(),
|
|
22
|
-
warn: jest.fn(),
|
|
23
|
-
error: jest.fn(),
|
|
24
|
-
child: () => logger,
|
|
25
|
-
}
|
|
26
|
-
return { createLogger: () => logger }
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const createBootstrapMock = jest.fn(() => () => {})
|
|
30
|
-
|
|
31
|
-
jest.mock(
|
|
32
|
-
'../factory.js',
|
|
33
|
-
() => ({
|
|
34
|
-
createBootstrap: (...args: unknown[]) => createBootstrapMock(...(args as [])),
|
|
35
|
-
waitForAsyncRegistration: async () => {},
|
|
36
|
-
}),
|
|
37
|
-
{ virtual: true },
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
import fs from 'node:fs'
|
|
41
|
-
import os from 'node:os'
|
|
42
|
-
import path from 'node:path'
|
|
43
|
-
import crypto from 'node:crypto'
|
|
44
|
-
import { createLogger } from '../../logger'
|
|
45
|
-
import { bootstrapFromAppRoot } from '../dynamicLoader'
|
|
46
|
-
|
|
47
|
-
const mockedLogger = createLogger('shared') as unknown as {
|
|
48
|
-
debug: jest.Mock
|
|
49
|
-
error: jest.Mock
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const APP_DI_CALL_COUNTER_KEY = '__omTestAppDiRegisterCalls__'
|
|
53
|
-
|
|
54
|
-
const GENERATED_MODULES: Record<string, { ts: string; compiled: string }> = {
|
|
55
|
-
'entities.ids.generated': { ts: 'export const E = {}', compiled: 'module.exports = { E: {} }' },
|
|
56
|
-
'modules.cli.generated': { ts: 'export const modules = []', compiled: 'module.exports = { modules: [] }' },
|
|
57
|
-
'entities.generated': { ts: 'export const entities = []', compiled: 'module.exports = { entities: [] }' },
|
|
58
|
-
'di.generated': { ts: 'export const diRegistrars = []', compiled: 'module.exports = { diRegistrars: [] }' },
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const APP_TSCONFIG = JSON.stringify({
|
|
62
|
-
compilerOptions: {
|
|
63
|
-
experimentalDecorators: true,
|
|
64
|
-
emitDecoratorMetadata: true,
|
|
65
|
-
useDefineForClassFields: false,
|
|
66
|
-
target: 'ES2022',
|
|
67
|
-
},
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const APP_DI_TS = 'export function register() {}'
|
|
71
|
-
|
|
72
|
-
const APP_DI_COMPILED = [
|
|
73
|
-
'module.exports = {',
|
|
74
|
-
' register: () => {',
|
|
75
|
-
` globalThis['${APP_DI_CALL_COUNTER_KEY}'] = (globalThis['${APP_DI_CALL_COUNTER_KEY}'] ?? 0) + 1`,
|
|
76
|
-
' },',
|
|
77
|
-
'}',
|
|
78
|
-
].join('\n')
|
|
79
|
-
|
|
80
|
-
function hash(content: string): string {
|
|
81
|
-
return crypto.createHash('sha256').update(content).digest('hex')
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function writeCompiledPair(
|
|
85
|
-
appRoot: string,
|
|
86
|
-
sourcePath: string,
|
|
87
|
-
compiledPath: string,
|
|
88
|
-
source: { ts: string; compiled: string },
|
|
89
|
-
) {
|
|
90
|
-
fs.writeFileSync(sourcePath, source.ts)
|
|
91
|
-
fs.writeFileSync(compiledPath, source.compiled)
|
|
92
|
-
const sourceRelativePath = path.relative(appRoot, sourcePath).split(path.sep).join('/')
|
|
93
|
-
fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
|
|
94
|
-
version: 4,
|
|
95
|
-
inputHash: hash(JSON.stringify({
|
|
96
|
-
version: 4,
|
|
97
|
-
sourceHash: hash(source.ts),
|
|
98
|
-
tsconfigHashes: {
|
|
99
|
-
'tsconfig.json': hash(APP_TSCONFIG),
|
|
100
|
-
},
|
|
101
|
-
})),
|
|
102
|
-
outputHash: hash(source.compiled),
|
|
103
|
-
dependencies: {
|
|
104
|
-
[sourceRelativePath]: hash(source.ts),
|
|
105
|
-
},
|
|
106
|
-
}))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const createdAppRoots: string[] = []
|
|
110
|
-
|
|
111
|
-
function createAppRoot(appDi: { ts: string; compiled: string } | null): string {
|
|
112
|
-
const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-bootstrap-app-di-'))
|
|
113
|
-
const generatedDir = path.join(appRoot, '.mercato', 'generated')
|
|
114
|
-
fs.mkdirSync(generatedDir, { recursive: true })
|
|
115
|
-
fs.writeFileSync(path.join(appRoot, 'tsconfig.json'), APP_TSCONFIG)
|
|
116
|
-
|
|
117
|
-
for (const [baseName, source] of Object.entries(GENERATED_MODULES)) {
|
|
118
|
-
writeCompiledPair(
|
|
119
|
-
appRoot,
|
|
120
|
-
path.join(generatedDir, `${baseName}.ts`),
|
|
121
|
-
path.join(generatedDir, `${baseName}.mjs`),
|
|
122
|
-
source,
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (appDi) {
|
|
127
|
-
fs.mkdirSync(path.join(appRoot, 'src'), { recursive: true })
|
|
128
|
-
writeCompiledPair(
|
|
129
|
-
appRoot,
|
|
130
|
-
path.join(appRoot, 'src', 'di.ts'),
|
|
131
|
-
path.join(generatedDir, 'app-di.compiled.mjs'),
|
|
132
|
-
appDi,
|
|
133
|
-
)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
createdAppRoots.push(appRoot)
|
|
137
|
-
return appRoot
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function appDiRegistrarArgument(): unknown {
|
|
141
|
-
const [, options] = createBootstrapMock.mock.calls[0] as unknown as [unknown, Record<string, unknown>]
|
|
142
|
-
return options?.appDiRegistrar
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
describe('bootstrapFromAppRoot — app-level DI reaches worker/CLI processes', () => {
|
|
146
|
-
afterAll(() => {
|
|
147
|
-
for (const root of createdAppRoots) {
|
|
148
|
-
fs.rmSync(root, { recursive: true, force: true })
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
beforeEach(() => {
|
|
153
|
-
createBootstrapMock.mockClear()
|
|
154
|
-
mockedLogger.debug.mockClear()
|
|
155
|
-
mockedLogger.error.mockClear()
|
|
156
|
-
delete (globalThis as Record<string, unknown>)[APP_DI_CALL_COUNTER_KEY]
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('passes the app DI register() from src/di.ts to createBootstrap', async () => {
|
|
160
|
-
const appRoot = createAppRoot({ ts: APP_DI_TS, compiled: APP_DI_COMPILED })
|
|
161
|
-
|
|
162
|
-
await bootstrapFromAppRoot(appRoot)
|
|
163
|
-
|
|
164
|
-
const registrar = appDiRegistrarArgument()
|
|
165
|
-
expect(typeof registrar).toBe('function')
|
|
166
|
-
|
|
167
|
-
await (registrar as (container: unknown) => unknown)({})
|
|
168
|
-
expect((globalThis as Record<string, unknown>)[APP_DI_CALL_COUNTER_KEY]).toBe(1)
|
|
169
|
-
expect(mockedLogger.error).not.toHaveBeenCalled()
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('bootstraps without an app DI registrar when src/di.ts is absent', async () => {
|
|
173
|
-
const appRoot = createAppRoot(null)
|
|
174
|
-
|
|
175
|
-
await bootstrapFromAppRoot(appRoot)
|
|
176
|
-
|
|
177
|
-
expect(createBootstrapMock).toHaveBeenCalledTimes(1)
|
|
178
|
-
expect(appDiRegistrarArgument()).toBeUndefined()
|
|
179
|
-
expect(mockedLogger.error).not.toHaveBeenCalled()
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('reports an error and keeps bootstrapping when src/di.ts fails to load', async () => {
|
|
183
|
-
const appRoot = createAppRoot({
|
|
184
|
-
ts: APP_DI_TS,
|
|
185
|
-
compiled: "throw new Error('src/di.ts is broken')",
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
await bootstrapFromAppRoot(appRoot)
|
|
189
|
-
|
|
190
|
-
expect(createBootstrapMock).toHaveBeenCalledTimes(1)
|
|
191
|
-
expect(appDiRegistrarArgument()).toBeUndefined()
|
|
192
|
-
expect(mockedLogger.error).toHaveBeenCalledTimes(1)
|
|
193
|
-
const [message, fields] = mockedLogger.error.mock.calls[0] as [string, Record<string, unknown>]
|
|
194
|
-
expect(message).toContain('Failed to load the app-level DI module')
|
|
195
|
-
expect((fields.err as Error).message).toContain('src/di.ts is broken')
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('reports an error and keeps bootstrapping when src/di.ts exports no register()', async () => {
|
|
199
|
-
const appRoot = createAppRoot({
|
|
200
|
-
ts: APP_DI_TS,
|
|
201
|
-
compiled: 'module.exports = {}',
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
await bootstrapFromAppRoot(appRoot)
|
|
205
|
-
|
|
206
|
-
expect(createBootstrapMock).toHaveBeenCalledTimes(1)
|
|
207
|
-
expect(appDiRegistrarArgument()).toBeUndefined()
|
|
208
|
-
expect(mockedLogger.error).toHaveBeenCalledTimes(1)
|
|
209
|
-
expect(mockedLogger.error.mock.calls[0][0]).toContain('exports no register()')
|
|
210
|
-
})
|
|
211
|
-
})
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment node
|
|
3
|
-
*
|
|
4
|
-
* Integration guard for #4526/#4693: compileAndImport must await its dynamic
|
|
5
|
-
* import so that an import-time rejection settles inside the try block.
|
|
6
|
-
* Returning the promise unawaited let the rejection escape the surrounding try,
|
|
7
|
-
* which made the MikroORM v7 generated-cache recovery in the catch dead code for
|
|
8
|
-
* exactly the failure it exists to repair. The one-line fix shipped in #4540;
|
|
9
|
-
* this file is the end-to-end coverage for it.
|
|
10
|
-
*
|
|
11
|
-
* Two sibling suites cover this area and neither subsumes the other:
|
|
12
|
-
*
|
|
13
|
-
* - dynamicLoader.cacheRecovery.test.ts mocks ../generatedCacheRecovery in full
|
|
14
|
-
* and asserts how the loader *reacts* to a rejecting import (recovery is
|
|
15
|
-
* invoked once, the retry is capped at one, the error propagates when no
|
|
16
|
-
* recovery applies).
|
|
17
|
-
* - generatedCacheRecovery.test.ts exercises the recovery module directly,
|
|
18
|
-
* without going through the loader.
|
|
19
|
-
* - This file runs the REAL recovery module from the loader's catch, so the
|
|
20
|
-
* loader ↔ recovery seam is covered end to end: the on-disk marker, its
|
|
21
|
-
* `runtime-import-error` reason and the deleted-file list are asserted against
|
|
22
|
-
* what the production module actually writes. Nothing else in the repository
|
|
23
|
-
* reaches that reason.
|
|
24
|
-
*
|
|
25
|
-
* The fixture reproduces the window the catch exists for: a stale compiled file
|
|
26
|
-
* that the startup scan (ensureMikroOrmV7GeneratedCacheCompatibility) cannot see
|
|
27
|
-
* because it appears only once loading is already under way. The staged .mjs
|
|
28
|
-
* therefore carries no decorator import at scan time — it writes a stale sibling
|
|
29
|
-
* and then rejects with the error Node raises for a v6 decorator import, which
|
|
30
|
-
* is what the import-time recovery is meant to repair.
|
|
31
|
-
*
|
|
32
|
-
* The assertions stop at the recovery boundary on purpose. Jest's module
|
|
33
|
-
* registry keeps serving a file it has already evaluated, so the guarded retry
|
|
34
|
-
* re-runs the rejecting module from memory no matter what recovery wrote to
|
|
35
|
-
* disk — the outcome of the retry is a property of the test runner, not of the
|
|
36
|
-
* loader. What #4526 broke, and what this guards, is that the catch runs at
|
|
37
|
-
* all: before the fix the rejection escaped the try, so no import-time recovery
|
|
38
|
-
* ever happened.
|
|
39
|
-
*/
|
|
40
|
-
import crypto from 'node:crypto'
|
|
41
|
-
import fs from 'node:fs'
|
|
42
|
-
import os from 'node:os'
|
|
43
|
-
import path from 'node:path'
|
|
44
|
-
import { loadBootstrapData } from '../dynamicLoader'
|
|
45
|
-
|
|
46
|
-
const STALE_SIBLING_BASENAME = 'stale-entity.generated.mjs'
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* The decorator import is assembled at runtime rather than written literally:
|
|
50
|
-
* a literal would make this fixture itself match the startup scan's staleness
|
|
51
|
-
* pattern, so the scan would delete the cache before the import ever runs and
|
|
52
|
-
* the import-time path under test would never be exercised.
|
|
53
|
-
*/
|
|
54
|
-
const REJECTING_COMPILED_SOURCE = [
|
|
55
|
-
"const nodeFs = require('node:fs')",
|
|
56
|
-
"const nodePath = require('node:path')",
|
|
57
|
-
"const quote = String.fromCharCode(39)",
|
|
58
|
-
"const staleImport = 'import { Entity, PrimaryKey } from ' + quote + '@mikro-orm/core' + quote",
|
|
59
|
-
`nodeFs.writeFileSync(nodePath.join(__dirname, ${JSON.stringify(STALE_SIBLING_BASENAME)}), staleImport + '\\nexport const stale = true\\n')`,
|
|
60
|
-
`throw new Error('The requested module ' + quote + '@mikro-orm/core' + quote + ' does not provide an export named ' + quote + 'Entity' + quote)`,
|
|
61
|
-
].join('\n')
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Jest resolves the loader's dynamic import through its own CommonJS registry,
|
|
65
|
-
* so a staged .mjs has to be CommonJS to evaluate at all — esbuild's real ESM
|
|
66
|
-
* output fails under the runner with "Unexpected token 'export'". The .ts
|
|
67
|
-
* sources stay honest ESM: they are what the loader recompiles from once
|
|
68
|
-
* recovery has deleted the cache.
|
|
69
|
-
*/
|
|
70
|
-
const GENERATED_MODULES: Record<string, { ts: string; compiled: string }> = {
|
|
71
|
-
'entities.ids.generated': { ts: 'export const E = {}', compiled: 'module.exports = { E: {} }' },
|
|
72
|
-
'modules.cli.generated': { ts: 'export const modules = []', compiled: 'module.exports = { modules: [] }' },
|
|
73
|
-
'entities.generated': { ts: 'export const entities = []', compiled: 'module.exports = { entities: [] }' },
|
|
74
|
-
'di.generated': { ts: 'export const diRegistrars = []', compiled: 'module.exports = { diRegistrars: [] }' },
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function contentHash(content: Buffer | string): string {
|
|
78
|
-
return crypto.createHash('sha256').update(content).digest('hex')
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Replace a compiled cache entry with staged content the runner can evaluate,
|
|
83
|
-
* keeping the loader's own cache metadata authoritative.
|
|
84
|
-
*
|
|
85
|
-
* The loader validates a cache entry by hashing (#4724): it recompiles unless
|
|
86
|
-
* the sibling `.cache.json` matches both the source and the compiled output.
|
|
87
|
-
* Only `outputHash` is rewritten here — `version`, `inputHash` and
|
|
88
|
-
* `dependencies` stay exactly as the loader wrote them, so the fixture models a
|
|
89
|
-
* cache that is internally consistent and simply stale, and it cannot drift out
|
|
90
|
-
* of step with the cache format the way a hand-built metadata file would.
|
|
91
|
-
*/
|
|
92
|
-
function stageCompiledCache(generatedDir: string, baseName: string, compiled: string) {
|
|
93
|
-
const jsPath = path.join(generatedDir, `${baseName}.mjs`)
|
|
94
|
-
const metadataPath = `${jsPath}.cache.json`
|
|
95
|
-
|
|
96
|
-
fs.writeFileSync(jsPath, compiled)
|
|
97
|
-
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'))
|
|
98
|
-
metadata.outputHash = contentHash(fs.readFileSync(jsPath))
|
|
99
|
-
fs.writeFileSync(metadataPath, JSON.stringify(metadata))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function hasCacheMetadata(generatedDir: string, baseName: string): boolean {
|
|
103
|
-
return fs.existsSync(path.join(generatedDir, `${baseName}.mjs.cache.json`))
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
describe('compileAndImport — generated-cache recovery is reachable (#4526)', () => {
|
|
107
|
-
let appRoot: string
|
|
108
|
-
let generatedDir: string
|
|
109
|
-
let warnSpy: jest.SpyInstance
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Let the loader compile the generated sources so it writes real cache
|
|
113
|
-
* metadata for each of them, replacing each compiled output with the
|
|
114
|
-
* runner-evaluable staged content as it appears.
|
|
115
|
-
*
|
|
116
|
-
* The loader writes a module's metadata before importing it, so an import
|
|
117
|
-
* failing here (esbuild's ESM output under Jest's CommonJS registry) is
|
|
118
|
-
* expected and irrelevant — the metadata is the only thing this step is
|
|
119
|
-
* after. That failure does abort the rest of the load, though, so one pass
|
|
120
|
-
* only reaches the modules the loader got to: entities.ids first and alone,
|
|
121
|
-
* then the remainder together. Repeating the pass lets each newly staged
|
|
122
|
-
* module unblock the next batch, and the loop ends on the first pass that
|
|
123
|
-
* loads cleanly, which is precisely when every module is staged.
|
|
124
|
-
*
|
|
125
|
-
* jest.resetModules() between passes is load-bearing. Jest keys its registry
|
|
126
|
-
* on the resolved path and ignores the loader's `?cache=` query, so without
|
|
127
|
-
* the reset a module would keep being served from its first (ESM, failing)
|
|
128
|
-
* evaluation and no later pass could make progress.
|
|
129
|
-
*
|
|
130
|
-
* None of these failures carries a decorator-export error, so no recovery
|
|
131
|
-
* runs and no marker is written during setup; both tests below would fail
|
|
132
|
-
* loudly if one were.
|
|
133
|
-
*/
|
|
134
|
-
async function primeAndStageCompiledCaches() {
|
|
135
|
-
const baseNames = Object.keys(GENERATED_MODULES)
|
|
136
|
-
for (let pass = 0; pass <= baseNames.length; pass += 1) {
|
|
137
|
-
let loadedCleanly = true
|
|
138
|
-
await loadBootstrapData(appRoot).catch(() => { loadedCleanly = false })
|
|
139
|
-
jest.resetModules()
|
|
140
|
-
if (loadedCleanly) return
|
|
141
|
-
for (const baseName of baseNames) {
|
|
142
|
-
if (!hasCacheMetadata(generatedDir, baseName)) continue
|
|
143
|
-
stageCompiledCache(generatedDir, baseName, GENERATED_MODULES[baseName].compiled)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
throw new Error('[internal] loader never reached a fully staged generated cache')
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
beforeEach(async () => {
|
|
150
|
-
appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-bootstrap-4526-'))
|
|
151
|
-
generatedDir = path.join(appRoot, '.mercato', 'generated')
|
|
152
|
-
fs.mkdirSync(generatedDir, { recursive: true })
|
|
153
|
-
fs.writeFileSync(
|
|
154
|
-
path.join(appRoot, 'tsconfig.json'),
|
|
155
|
-
JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'ESNext' } }),
|
|
156
|
-
)
|
|
157
|
-
for (const [baseName, source] of Object.entries(GENERATED_MODULES)) {
|
|
158
|
-
fs.writeFileSync(path.join(generatedDir, `${baseName}.ts`), source.ts)
|
|
159
|
-
}
|
|
160
|
-
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
161
|
-
|
|
162
|
-
await primeAndStageCompiledCaches()
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
afterEach(() => {
|
|
166
|
-
warnSpy.mockRestore()
|
|
167
|
-
fs.rmSync(appRoot, { recursive: true, force: true })
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('runs cache recovery when the compiled cache rejects on import', async () => {
|
|
171
|
-
const rejectingPath = path.join(generatedDir, 'entities.ids.generated.mjs')
|
|
172
|
-
stageCompiledCache(generatedDir, 'entities.ids.generated', REJECTING_COMPILED_SOURCE)
|
|
173
|
-
|
|
174
|
-
await loadBootstrapData(appRoot).catch(() => undefined)
|
|
175
|
-
|
|
176
|
-
const markerPath = path.join(generatedDir, '.mikro-orm-v7-cache-recovery.json')
|
|
177
|
-
expect(fs.existsSync(markerPath)).toBe(true)
|
|
178
|
-
|
|
179
|
-
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'))
|
|
180
|
-
expect(marker.reason).toBe('runtime-import-error')
|
|
181
|
-
expect(marker.deletedFiles).toContain(rejectingPath)
|
|
182
|
-
expect(marker.deletedFiles).toContain(path.join(generatedDir, STALE_SIBLING_BASENAME))
|
|
183
|
-
|
|
184
|
-
expect(fs.readFileSync(rejectingPath, 'utf8')).not.toContain('does not provide an export named')
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
it('does not run recovery when the compiled cache imports cleanly', async () => {
|
|
188
|
-
await loadBootstrapData(appRoot)
|
|
189
|
-
|
|
190
|
-
expect(fs.existsSync(path.join(generatedDir, '.mikro-orm-v7-cache-recovery.json'))).toBe(false)
|
|
191
|
-
})
|
|
192
|
-
})
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client-only modules (`*.client.tsx`) hold browser components and must never enter the
|
|
3
|
-
* CLI bundle graph. App modules are bundled from source by `compileAndImport`, and esbuild
|
|
4
|
-
* inlines dynamic imports into the single output file, hoisting the client file's static
|
|
5
|
-
* imports to the top of the bundle. A wrapper such as `@open-mercato/ui/backend/charts`
|
|
6
|
-
* would then execute on every CLI start and fail on bare Next.js specifiers that Node's
|
|
7
|
-
* ESM resolver cannot resolve.
|
|
8
|
-
*
|
|
9
|
-
* Resolving these files to an inert stub keeps the owning `widget.ts` importable (the CLI
|
|
10
|
-
* reads its metadata when seeding dashboards) while cutting the browser-only subgraph.
|
|
11
|
-
*
|
|
12
|
-
* Only `import()` expressions are stubbed. That is the documented way a server-side
|
|
13
|
-
* `widget.ts` reaches its browser component (`lazyDashboardWidget(() => import('./widget.client'))`),
|
|
14
|
-
* and its result is consumed as a namespace object, so a default-only stub is sufficient.
|
|
15
|
-
* Static imports are left to the bundler: they may request named bindings the stub cannot
|
|
16
|
-
* provide, which esbuild rejects with `No matching export`, failing the whole CLI bundle —
|
|
17
|
-
* the exact class of breakage this module exists to prevent. Restricting the rewrite to
|
|
18
|
-
* dynamic imports also keeps server-side helpers that merely follow a `*.client.ts` naming
|
|
19
|
-
* convention working untouched.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
export const CLIENT_ONLY_STUB_NAMESPACE = 'om-client-only-stub'
|
|
23
|
-
|
|
24
|
-
const LOCAL_IMPORT_PATTERN = /^(\.{1,2}\/|@\/)/
|
|
25
|
-
const CLIENT_ONLY_SUFFIX_PATTERN = /(^|[\\/])[^\\/]+\.client(\.[cm]?[jt]sx?)?$/
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Local (relative or `@/` aliased) imports of a `*.client` module. Bare package specifiers
|
|
29
|
-
* are left alone because the bundler already marks them external, so they never get inlined.
|
|
30
|
-
*/
|
|
31
|
-
export function isClientOnlyModulePath(importPath: string): boolean {
|
|
32
|
-
if (!LOCAL_IMPORT_PATTERN.test(importPath)) return false
|
|
33
|
-
return CLIENT_ONLY_SUFFIX_PATTERN.test(importPath)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Render a value as a JavaScript string literal safe to embed in generated source.
|
|
38
|
-
* Everything outside printable ASCII is escaped to a `\uXXXX` sequence, so no character
|
|
39
|
-
* of the input can terminate the literal or be re-interpreted as code — `JSON.stringify`
|
|
40
|
-
* alone is a JSON encoder, not a JavaScript-source escaper.
|
|
41
|
-
*/
|
|
42
|
-
export function encodeJsStringLiteral(value: string): string {
|
|
43
|
-
let encoded = ''
|
|
44
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
45
|
-
const char = value[index]
|
|
46
|
-
if (char === '"' || char === '\\') {
|
|
47
|
-
encoded += `\\${char}`
|
|
48
|
-
continue
|
|
49
|
-
}
|
|
50
|
-
const code = value.charCodeAt(index)
|
|
51
|
-
if (code < 0x20 || code > 0x7e) {
|
|
52
|
-
encoded += `\\u${code.toString(16).padStart(4, '0')}`
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
55
|
-
encoded += char
|
|
56
|
-
}
|
|
57
|
-
return `"${encoded}"`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function renderClientOnlyModuleStub(importPath: string): string {
|
|
61
|
-
const message =
|
|
62
|
-
`[internal] Client-only module ${importPath} is not available in the CLI runtime. ` +
|
|
63
|
-
'It is excluded from the CLI bundle because it renders browser components.'
|
|
64
|
-
return [
|
|
65
|
-
`function clientOnlyModuleUnavailable() { throw new Error(${encodeJsStringLiteral(message)}) }`,
|
|
66
|
-
'export default clientOnlyModuleUnavailable',
|
|
67
|
-
].join('\n')
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function createClientOnlyStubPlugin(): import('esbuild').Plugin {
|
|
71
|
-
return {
|
|
72
|
-
name: 'client-only-stub',
|
|
73
|
-
setup(build) {
|
|
74
|
-
build.onResolve({ filter: CLIENT_ONLY_SUFFIX_PATTERN }, (args) => {
|
|
75
|
-
if (args.kind !== 'dynamic-import') return null
|
|
76
|
-
if (!isClientOnlyModulePath(args.path)) return null
|
|
77
|
-
return { path: args.path, namespace: CLIENT_ONLY_STUB_NAMESPACE }
|
|
78
|
-
})
|
|
79
|
-
build.onLoad({ filter: /.*/, namespace: CLIENT_ONLY_STUB_NAMESPACE }, (args) => ({
|
|
80
|
-
contents: renderClientOnlyModuleStub(args.path),
|
|
81
|
-
loader: 'js',
|
|
82
|
-
}))
|
|
83
|
-
},
|
|
84
|
-
}
|
|
85
|
-
}
|
|
@@ -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
|
-
})
|