@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.
Files changed (199) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +6 -57
  3. package/build.mjs +4 -2
  4. package/dist/lib/auth/server.js +2 -15
  5. package/dist/lib/auth/server.js.map +2 -2
  6. package/dist/lib/bootstrap/dynamicLoader.js +43 -117
  7. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  8. package/dist/lib/commands/command-bus.js +1 -5
  9. package/dist/lib/commands/command-bus.js.map +2 -2
  10. package/dist/lib/commands/command-interceptor-runner.js +2 -2
  11. package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
  12. package/dist/lib/crud/enricher-runner.js +11 -2
  13. package/dist/lib/crud/enricher-runner.js.map +2 -2
  14. package/dist/lib/crud/factory.js +4 -63
  15. package/dist/lib/crud/factory.js.map +2 -2
  16. package/dist/lib/crud/interceptor-runner.js +2 -2
  17. package/dist/lib/crud/interceptor-runner.js.map +2 -2
  18. package/dist/lib/crud/mutation-guard-registry.js +2 -2
  19. package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
  20. package/dist/lib/crud/types.js +0 -4
  21. package/dist/lib/crud/types.js.map +3 -3
  22. package/dist/lib/data/engine.js +11 -37
  23. package/dist/lib/data/engine.js.map +2 -2
  24. package/dist/lib/db/mikro.js +12 -3
  25. package/dist/lib/db/mikro.js.map +2 -2
  26. package/dist/lib/db/pg-errors.js +0 -42
  27. package/dist/lib/db/pg-errors.js.map +2 -2
  28. package/dist/lib/di/container.js +6 -48
  29. package/dist/lib/di/container.js.map +2 -2
  30. package/dist/lib/email/send.js +0 -56
  31. package/dist/lib/email/send.js.map +2 -2
  32. package/dist/lib/encryption/subscriber.js +10 -2
  33. package/dist/lib/encryption/subscriber.js.map +2 -2
  34. package/dist/lib/encryption/tenantDataEncryptionService.js +17 -52
  35. package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
  36. package/dist/lib/i18n/config.js +1 -1
  37. package/dist/lib/i18n/config.js.map +2 -2
  38. package/dist/lib/i18n/server.js +4 -4
  39. package/dist/lib/i18n/server.js.map +3 -3
  40. package/dist/lib/logger/index.js +3 -47
  41. package/dist/lib/logger/index.js.map +3 -3
  42. package/dist/lib/modules/registry.js +1 -5
  43. package/dist/lib/modules/registry.js.map +2 -2
  44. package/dist/lib/query/engine.js +40 -54
  45. package/dist/lib/query/engine.js.map +2 -2
  46. package/dist/lib/query/types.js.map +1 -1
  47. package/dist/lib/redis/connection.js +2 -7
  48. package/dist/lib/redis/connection.js.map +2 -2
  49. package/dist/lib/search/config.js +2 -61
  50. package/dist/lib/search/config.js.map +2 -2
  51. package/dist/lib/search/tokenize.js +11 -23
  52. package/dist/lib/search/tokenize.js.map +2 -2
  53. package/dist/lib/version.js +1 -1
  54. package/dist/lib/version.js.map +1 -1
  55. package/dist/lib/webhooks/index.js +1 -13
  56. package/dist/lib/webhooks/index.js.map +2 -2
  57. package/dist/lib/webhooks/verify.js +7 -12
  58. package/dist/lib/webhooks/verify.js.map +2 -2
  59. package/dist/modules/analytics.js.map +2 -2
  60. package/dist/modules/overrides.js +1 -50
  61. package/dist/modules/overrides.js.map +2 -2
  62. package/dist/modules/payment_gateways/types.js +1 -8
  63. package/dist/modules/payment_gateways/types.js.map +2 -2
  64. package/dist/modules/registry.js +0 -1
  65. package/dist/modules/registry.js.map +2 -2
  66. package/dist/security/enabledModulesRegistry.js +11 -56
  67. package/dist/security/enabledModulesRegistry.js.map +2 -2
  68. package/jest.config.cjs +1 -2
  69. package/package.json +9 -19
  70. package/src/lib/ai/llm-provider.ts +0 -33
  71. package/src/lib/auth/__tests__/server.test.ts +0 -23
  72. package/src/lib/auth/server.ts +2 -23
  73. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +8 -63
  74. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +1 -21
  75. package/src/lib/bootstrap/dynamicLoader.ts +65 -213
  76. package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +0 -28
  77. package/src/lib/commands/command-bus.ts +1 -5
  78. package/src/lib/commands/command-interceptor-runner.ts +2 -2
  79. package/src/lib/crud/__tests__/crud-factory.test.ts +0 -298
  80. package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +0 -26
  81. package/src/lib/crud/enricher-runner.ts +11 -2
  82. package/src/lib/crud/factory.ts +1 -69
  83. package/src/lib/crud/interceptor-runner.ts +2 -2
  84. package/src/lib/crud/mutation-guard-registry.ts +2 -2
  85. package/src/lib/crud/types.ts +0 -3
  86. package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +3 -18
  87. package/src/lib/data/engine.ts +16 -50
  88. package/src/lib/db/__tests__/mikro.test.ts +25 -0
  89. package/src/lib/db/mikro.ts +38 -13
  90. package/src/lib/db/pg-errors.ts +0 -58
  91. package/src/lib/di/container.ts +8 -72
  92. package/src/lib/email/__tests__/send.test.ts +1 -37
  93. package/src/lib/email/send.ts +0 -79
  94. package/src/lib/encryption/__tests__/subscriber.test.ts +2 -54
  95. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -58
  96. package/src/lib/encryption/subscriber.ts +14 -2
  97. package/src/lib/encryption/tenantDataEncryptionService.ts +18 -64
  98. package/src/lib/i18n/config.ts +2 -2
  99. package/src/lib/i18n/server.ts +2 -6
  100. package/src/lib/logger/__tests__/logger.test.ts +0 -77
  101. package/src/lib/logger/index.ts +1 -59
  102. package/src/lib/modules/registry.ts +0 -10
  103. package/src/lib/query/__tests__/engine.test.ts +13 -65
  104. package/src/lib/query/engine.ts +58 -75
  105. package/src/lib/query/types.ts +5 -4
  106. package/src/lib/redis/__tests__/connection.test.ts +0 -16
  107. package/src/lib/redis/connection.ts +7 -10
  108. package/src/lib/search/__tests__/config.test.ts +0 -177
  109. package/src/lib/search/config.ts +8 -106
  110. package/src/lib/search/tokenize.ts +11 -33
  111. package/src/lib/webhooks/__tests__/verify.test.ts +1 -20
  112. package/src/lib/webhooks/index.ts +1 -19
  113. package/src/lib/webhooks/verify.ts +9 -17
  114. package/src/modules/__tests__/registry.test.ts +2 -10
  115. package/src/modules/analytics.ts +0 -7
  116. package/src/modules/customer-auth.ts +0 -1
  117. package/src/modules/encryption.ts +0 -3
  118. package/src/modules/navigation/backendChrome.ts +0 -15
  119. package/src/modules/overrides.ts +0 -103
  120. package/src/modules/payment_gateways/types.ts +0 -41
  121. package/src/modules/registry.ts +0 -3
  122. package/src/security/enabledModulesRegistry.ts +12 -64
  123. package/dist/lib/ai/safety-identifier.js +0 -32
  124. package/dist/lib/ai/safety-identifier.js.map +0 -7
  125. package/dist/lib/auth/organizationScope.js +0 -34
  126. package/dist/lib/auth/organizationScope.js.map +0 -7
  127. package/dist/lib/bootstrap/clientOnlyModules.js +0 -55
  128. package/dist/lib/bootstrap/clientOnlyModules.js.map +0 -7
  129. package/dist/lib/data/consistency.js +0 -19
  130. package/dist/lib/data/consistency.js.map +0 -7
  131. package/dist/lib/db/entityMetadata.js +0 -26
  132. package/dist/lib/db/entityMetadata.js.map +0 -7
  133. package/dist/lib/logger/extension.js +0 -29
  134. package/dist/lib/logger/extension.js.map +0 -7
  135. package/dist/lib/modules/surfaceFingerprint.js +0 -47
  136. package/dist/lib/modules/surfaceFingerprint.js.map +0 -7
  137. package/dist/lib/query/ciphertext-search-warning.js +0 -45
  138. package/dist/lib/query/ciphertext-search-warning.js.map +0 -7
  139. package/dist/lib/search/auto-indexing.js +0 -14
  140. package/dist/lib/search/auto-indexing.js.map +0 -7
  141. package/dist/lib/search/availability.js +0 -107
  142. package/dist/lib/search/availability.js.map +0 -7
  143. package/dist/lib/search/tokenLookup.js +0 -46
  144. package/dist/lib/search/tokenLookup.js.map +0 -7
  145. package/dist/lib/telemetry/runtime.js +0 -35
  146. package/dist/lib/telemetry/runtime.js.map +0 -7
  147. package/dist/lib/webhooks/body.js +0 -57
  148. package/dist/lib/webhooks/body.js.map +0 -7
  149. package/dist/lib/webhooks/inbound-types.js +0 -1
  150. package/dist/lib/webhooks/inbound-types.js.map +0 -7
  151. package/dist/modules/widgets/extension-points.js +0 -96
  152. package/dist/modules/widgets/extension-points.js.map +0 -7
  153. package/dist/security/featurePolicy.js +0 -62
  154. package/dist/security/featurePolicy.js.map +0 -7
  155. package/scripts/versionSource.cjs +0 -65
  156. package/src/lib/__tests__/versionSource.test.ts +0 -46
  157. package/src/lib/ai/__tests__/llm-provider-contract.test.ts +0 -59
  158. package/src/lib/ai/__tests__/safety-identifier.test.ts +0 -71
  159. package/src/lib/ai/safety-identifier.ts +0 -73
  160. package/src/lib/auth/__tests__/organizationScope.test.ts +0 -107
  161. package/src/lib/auth/organizationScope.ts +0 -65
  162. package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +0 -189
  163. package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +0 -98
  164. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +0 -211
  165. package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +0 -192
  166. package/src/lib/bootstrap/clientOnlyModules.ts +0 -85
  167. package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +0 -171
  168. package/src/lib/data/__tests__/consistency.test.ts +0 -38
  169. package/src/lib/data/consistency.ts +0 -17
  170. package/src/lib/db/__tests__/entityMetadata.test.ts +0 -38
  171. package/src/lib/db/__tests__/pg-errors.test.ts +0 -48
  172. package/src/lib/db/entityMetadata.ts +0 -43
  173. package/src/lib/di/__tests__/container-app-di-absent.test.ts +0 -155
  174. package/src/lib/di/__tests__/container-app-di.test.ts +0 -173
  175. package/src/lib/di/__tests__/registrar-error-log.test.ts +0 -154
  176. package/src/lib/i18n/__tests__/config.test.ts +0 -8
  177. package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +0 -30
  178. package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +0 -29
  179. package/src/lib/logger/extension.ts +0 -59
  180. package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +0 -122
  181. package/src/lib/modules/surfaceFingerprint.ts +0 -87
  182. package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +0 -178
  183. package/src/lib/query/ciphertext-search-warning.ts +0 -95
  184. package/src/lib/search/__tests__/availability.test.ts +0 -211
  185. package/src/lib/search/__tests__/tokenLookup.test.ts +0 -206
  186. package/src/lib/search/__tests__/tokenize.test.ts +0 -49
  187. package/src/lib/search/auto-indexing.ts +0 -22
  188. package/src/lib/search/availability.ts +0 -232
  189. package/src/lib/search/tokenLookup.ts +0 -133
  190. package/src/lib/telemetry/runtime.ts +0 -72
  191. package/src/lib/webhooks/__tests__/body.test.ts +0 -95
  192. package/src/lib/webhooks/body.ts +0 -70
  193. package/src/lib/webhooks/inbound-types.ts +0 -125
  194. package/src/modules/__tests__/nav-group-order-override.test.ts +0 -183
  195. package/src/modules/payment_gateways/__tests__/types.test.ts +0 -37
  196. package/src/modules/widgets/__tests__/extension-points.test.ts +0 -101
  197. package/src/modules/widgets/extension-points.ts +0 -524
  198. package/src/security/__tests__/featurePolicy.test.ts +0 -166
  199. 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
- })