@open-mercato/shared 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435
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 +4 -4
- package/dist/lib/auth/server.js +1 -1
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/dynamicLoader.js +6 -46
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/bootstrap/factory.js +3 -4
- package/dist/lib/bootstrap/factory.js.map +2 -2
- package/dist/lib/catalog-visibility/index.js +0 -4
- package/dist/lib/catalog-visibility/index.js.map +2 -2
- package/dist/lib/commands/command-bus.js +13 -45
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/types.js.map +2 -2
- package/dist/lib/crud/exporters.js +1 -1
- package/dist/lib/crud/exporters.js.map +2 -2
- package/dist/lib/crud/ids.js +1 -13
- package/dist/lib/crud/ids.js.map +2 -2
- package/dist/lib/db/pg-errors.js +0 -14
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/localization/index.js +1 -9
- package/dist/lib/localization/index.js.map +2 -2
- package/dist/lib/localization/translatable-fields.js +0 -26
- package/dist/lib/localization/translatable-fields.js.map +2 -2
- package/dist/lib/number.js +0 -6
- package/dist/lib/number.js.map +2 -2
- package/dist/lib/openapi/generator.js +67 -84
- package/dist/lib/openapi/generator.js.map +2 -2
- package/dist/lib/query/engine.js +10 -45
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/join-utils.js +1 -1
- package/dist/lib/query/join-utils.js.map +2 -2
- package/dist/lib/query/types.js.map +2 -2
- package/dist/lib/url.js +0 -24
- package/dist/lib/url.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +3 -7
- package/dist/lib/webhooks/body.js.map +2 -2
- package/dist/lib/webhooks/index.js +0 -2
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/modules/integrations/types.js.map +2 -2
- package/dist/modules/notifications/types.js +0 -4
- package/dist/modules/notifications/types.js.map +3 -3
- package/dist/modules/payment_gateways/types.js +1 -14
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/search.js.map +2 -2
- package/dist/modules/widgets/injection-loader.js +3 -14
- package/dist/modules/widgets/injection-loader.js.map +2 -2
- package/package.json +3 -10
- package/src/lib/__tests__/number.test.ts +1 -24
- package/src/lib/auth/__tests__/server.test.ts +0 -14
- package/src/lib/auth/server.ts +1 -1
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.appModuleOverrides.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.cacheRecovery.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +2 -2
- package/src/lib/bootstrap/__tests__/factory.test.ts +6 -18
- package/src/lib/bootstrap/dynamicLoader.ts +5 -63
- package/src/lib/bootstrap/factory.ts +3 -8
- package/src/lib/bootstrap/types.ts +1 -5
- package/src/lib/catalog-visibility/index.ts +0 -2
- package/src/lib/commands/__tests__/command-bus.test.ts +1 -100
- package/src/lib/commands/command-bus.ts +10 -42
- package/src/lib/commands/command-interceptor.ts +0 -4
- package/src/lib/commands/types.ts +0 -14
- package/src/lib/crud/__tests__/exporters.test.ts +0 -18
- package/src/lib/crud/__tests__/ids.test.ts +1 -34
- package/src/lib/crud/exporters.ts +1 -1
- package/src/lib/crud/ids.ts +0 -24
- package/src/lib/db/__tests__/pg-errors.test.ts +1 -68
- package/src/lib/db/pg-errors.ts +0 -35
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -23
- package/src/lib/localization/__tests__/translatable-fields.test.ts +0 -17
- package/src/lib/localization/index.ts +1 -8
- package/src/lib/localization/translatable-fields.ts +0 -46
- package/src/lib/number.ts +0 -13
- package/src/lib/openapi/generator.ts +77 -106
- package/src/lib/query/engine.ts +8 -58
- package/src/lib/query/join-utils.ts +1 -1
- package/src/lib/query/types.ts +1 -13
- package/src/lib/url.ts +0 -30
- package/src/lib/webhooks/__tests__/body.test.ts +0 -48
- package/src/lib/webhooks/body.ts +4 -11
- package/src/lib/webhooks/index.ts +0 -1
- package/src/modules/integrations/types.ts +0 -6
- package/src/modules/notifications/types.ts +0 -7
- package/src/modules/payment_gateways/__tests__/types.test.ts +0 -44
- package/src/modules/payment_gateways/types.ts +6 -52
- package/src/modules/registry.ts +0 -5
- package/src/modules/search.ts +0 -39
- package/src/modules/widgets/injection-loader.ts +3 -33
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js +0 -6
- package/dist/lib/bootstrap/next-subpath-resolve-hook-worker.js.map +0 -7
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js +0 -62
- package/dist/lib/bootstrap/next-subpath-resolve-hook.js.map +0 -7
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js +0 -33
- package/dist/lib/catalog-visibility/canonicalizeEffectiveScope.js.map +0 -7
- package/dist/lib/catalog-visibility/hashEffectiveScope.js +0 -9
- package/dist/lib/catalog-visibility/hashEffectiveScope.js.map +0 -7
- package/dist/lib/db/advisoryLock.js +0 -172
- package/dist/lib/db/advisoryLock.js.map +0 -7
- package/dist/lib/query/overlap.js +0 -58
- package/dist/lib/query/overlap.js.map +0 -7
- package/dist/modules/notifications/safeHref.js +0 -20
- package/dist/modules/notifications/safeHref.js.map +0 -7
- package/src/lib/__tests__/url.cross-site.test.ts +0 -140
- package/src/lib/bootstrap/__tests__/dynamicLoader.externalSubpath.test.ts +0 -141
- package/src/lib/bootstrap/__tests__/next-subpath-resolve-hook.test.ts +0 -174
- package/src/lib/bootstrap/next-subpath-resolve-hook-worker.ts +0 -3
- package/src/lib/bootstrap/next-subpath-resolve-hook.ts +0 -130
- package/src/lib/catalog-visibility/__tests__/hashEffectiveScope.test.ts +0 -121
- package/src/lib/catalog-visibility/canonicalizeEffectiveScope.ts +0 -41
- package/src/lib/catalog-visibility/hashEffectiveScope.ts +0 -8
- package/src/lib/db/__tests__/advisoryLock.integration.test.ts +0 -343
- package/src/lib/db/__tests__/advisoryLock.test.ts +0 -564
- package/src/lib/db/advisoryLock.ts +0 -271
- package/src/lib/openapi/__tests__/optional-path-params.test.ts +0 -102
- package/src/lib/query/__tests__/empty-ids-filter.pg.test.ts +0 -89
- package/src/lib/query/__tests__/engine.empty-in-filter.test.ts +0 -79
- package/src/lib/query/__tests__/overlap.test.ts +0 -219
- package/src/lib/query/overlap.ts +0 -106
- package/src/modules/notifications/safeHref.ts +0 -19
- package/src/modules/widgets/__tests__/injection-loader.partitioned-registration.test.ts +0 -78
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseLocaleNumber, parseNumberWithDefault, resolveLocaleNumberSeparators } from '../number'
|
|
2
2
|
|
|
3
3
|
describe('parseNumberWithDefault', () => {
|
|
4
4
|
it('returns the fallback when raw is missing or blank', () => {
|
|
@@ -121,26 +121,3 @@ describe('parseLocaleNumber', () => {
|
|
|
121
121
|
expect(parseLocaleNumber('110.70', undefined)).toBe(110.7)
|
|
122
122
|
})
|
|
123
123
|
})
|
|
124
|
-
|
|
125
|
-
describe('formatLocaleNumberInput (issue #6309)', () => {
|
|
126
|
-
it('uses the locale decimal separator instead of the dot String() always emits', () => {
|
|
127
|
-
expect(formatLocaleNumberInput(2.5, 'pl-PL')).toBe('2,5')
|
|
128
|
-
expect(formatLocaleNumberInput(-2.5, 'pl-PL')).toBe('-2,5')
|
|
129
|
-
expect(formatLocaleNumberInput(1234.56, 'pl-PL')).toBe('1234,56')
|
|
130
|
-
expect(formatLocaleNumberInput(2.5, 'en-US')).toBe('2.5')
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('leaves integers untouched and applies no grouping', () => {
|
|
134
|
-
expect(formatLocaleNumberInput(1500, 'pl-PL')).toBe('1500')
|
|
135
|
-
expect(formatLocaleNumberInput(1500, 'en-US')).toBe('1500')
|
|
136
|
-
expect(formatLocaleNumberInput(0, 'pl-PL')).toBe('0')
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('round-trips through parseLocaleNumber', () => {
|
|
140
|
-
for (const value of [2.5, -0.125, 1234.56, 0.1 + 0.2, 7]) {
|
|
141
|
-
for (const locale of ['pl-PL', 'de-DE', 'en-US', 'fr-FR']) {
|
|
142
|
-
expect(parseLocaleNumber(formatLocaleNumberInput(value, locale), locale)).toBe(value)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
})
|
|
@@ -122,20 +122,6 @@ describe('auth server integrity checks', () => {
|
|
|
122
122
|
await expect(resolveAuthFromRequestDetailed(request)).resolves.toEqual({ auth: null, status: 'invalid' })
|
|
123
123
|
})
|
|
124
124
|
|
|
125
|
-
it('treats a malformed %-escape auth_token as an invalid token instead of 500ing (issue #7382)', async () => {
|
|
126
|
-
const { resolveAuthFromRequestDetailed } = await import('@open-mercato/shared/lib/auth/server')
|
|
127
|
-
verifyJwt.mockImplementation(() => {
|
|
128
|
-
throw new Error('malformed token')
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
const request = new Request('https://example.test/api/test', {
|
|
132
|
-
headers: { cookie: 'auth_token=%zz' },
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
await expect(resolveAuthFromRequestDetailed(request)).resolves.toEqual({ auth: null, status: 'invalid' })
|
|
136
|
-
expect(verifyJwt).toHaveBeenCalledWith('%zz')
|
|
137
|
-
})
|
|
138
|
-
|
|
139
125
|
it('replaces stale JWT roles with canonical roles from the database', async () => {
|
|
140
126
|
const { getAuthFromRequest } = await import('@open-mercato/shared/lib/auth/server')
|
|
141
127
|
const jwtAuth = {
|
package/src/lib/auth/server.ts
CHANGED
|
@@ -413,7 +413,7 @@ export async function resolveAuthFromRequestDetailed(req: Request): Promise<Auth
|
|
|
413
413
|
if (authHeader.toLowerCase().startsWith('bearer ')) token = authHeader.slice(7).trim()
|
|
414
414
|
if (!token) {
|
|
415
415
|
const match = cookieHeader.match(/(?:^|;\s*)auth_token=([^;]+)/)
|
|
416
|
-
if (match) token =
|
|
416
|
+
if (match) token = decodeURIComponent(match[1])
|
|
417
417
|
}
|
|
418
418
|
if (token) {
|
|
419
419
|
try {
|
|
@@ -91,9 +91,9 @@ function writeCompiledPair(
|
|
|
91
91
|
fs.writeFileSync(compiledPath, source.compiled)
|
|
92
92
|
const sourceRelativePath = path.relative(appRoot, sourcePath).split(path.sep).join('/')
|
|
93
93
|
fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
|
|
94
|
-
version:
|
|
94
|
+
version: 4,
|
|
95
95
|
inputHash: hash(JSON.stringify({
|
|
96
|
-
version:
|
|
96
|
+
version: 4,
|
|
97
97
|
sourceHash: hash(source.ts),
|
|
98
98
|
tsconfigHashes: {
|
|
99
99
|
'tsconfig.json': hash(APP_TSCONFIG),
|
|
@@ -132,9 +132,9 @@ function writeCompiledPair(
|
|
|
132
132
|
fs.writeFileSync(compiledPath, source.compiled)
|
|
133
133
|
const sourceRelativePath = path.relative(appRoot, sourcePath).split(path.sep).join('/')
|
|
134
134
|
fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
|
|
135
|
-
version:
|
|
135
|
+
version: 4,
|
|
136
136
|
inputHash: hash(JSON.stringify({
|
|
137
|
-
version:
|
|
137
|
+
version: 4,
|
|
138
138
|
sourceHash: hash(source.ts),
|
|
139
139
|
tsconfigHashes: {
|
|
140
140
|
'tsconfig.json': hash(APP_TSCONFIG),
|
|
@@ -77,7 +77,7 @@ function writeGeneratedModule(generatedDir: string, baseName: string, source: {
|
|
|
77
77
|
function writeCompiledSibling(generatedDir: string, baseName: string, compiled: string) {
|
|
78
78
|
const source = fs.readFileSync(path.join(generatedDir, `${baseName}.ts`), 'utf8')
|
|
79
79
|
const inputHash = hash(JSON.stringify({
|
|
80
|
-
version:
|
|
80
|
+
version: 4,
|
|
81
81
|
sourceHash: hash(source),
|
|
82
82
|
tsconfigHashes: {
|
|
83
83
|
'tsconfig.json': hash(APP_TSCONFIG),
|
|
@@ -90,7 +90,7 @@ function writeCompiledSibling(generatedDir: string, baseName: string, compiled:
|
|
|
90
90
|
const compiledPath = path.join(generatedDir, `${baseName}.mjs`)
|
|
91
91
|
fs.writeFileSync(compiledPath, compiled)
|
|
92
92
|
fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
|
|
93
|
-
version:
|
|
93
|
+
version: 4,
|
|
94
94
|
inputHash,
|
|
95
95
|
outputHash: hash(compiled),
|
|
96
96
|
dependencies: {
|
|
@@ -77,9 +77,9 @@ function writeGeneratedModule(generatedDir: string, baseName: string, source: {
|
|
|
77
77
|
fs.writeFileSync(sourcePath, source.ts)
|
|
78
78
|
fs.writeFileSync(compiledPath, source.compiled)
|
|
79
79
|
fs.writeFileSync(`${compiledPath}.cache.json`, JSON.stringify({
|
|
80
|
-
version:
|
|
80
|
+
version: 4,
|
|
81
81
|
inputHash: hash(JSON.stringify({
|
|
82
|
-
version:
|
|
82
|
+
version: 4,
|
|
83
83
|
sourceHash: hash(source.ts),
|
|
84
84
|
tsconfigHashes: {
|
|
85
85
|
'tsconfig.json': hash(APP_TSCONFIG),
|
|
@@ -133,7 +133,7 @@ describe('dynamic loader app tsconfig and content-addressed cache', () => {
|
|
|
133
133
|
loadBootstrapDataInNode(appRoot)
|
|
134
134
|
const after = readCacheMetadata(appRoot, 'entities')
|
|
135
135
|
|
|
136
|
-
expect(after.version).toBe(
|
|
136
|
+
expect(after.version).toBe(4)
|
|
137
137
|
expect(after.inputHash).not.toBe(before.inputHash)
|
|
138
138
|
})
|
|
139
139
|
|
|
@@ -287,6 +287,6 @@ describe('dynamic loader app tsconfig and content-addressed cache', () => {
|
|
|
287
287
|
|
|
288
288
|
loadBootstrapDataInNode(appRoot)
|
|
289
289
|
|
|
290
|
-
expect(readCacheMetadata(appRoot, 'entities').version).toBe(
|
|
290
|
+
expect(readCacheMetadata(appRoot, 'entities').version).toBe(4)
|
|
291
291
|
})
|
|
292
292
|
})
|
|
@@ -62,8 +62,7 @@ describe('partitioned bootstrap registration', () => {
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
it('keeps API-only bootstrap from replacing core injection widgets', async () => {
|
|
65
|
-
const
|
|
66
|
-
const apiBootstrap = createBootstrap({ ...emptyBootstrapData, injectionWidgetEntries: [entry] }, {
|
|
65
|
+
const apiBootstrap = createBootstrap(emptyBootstrapData, {
|
|
67
66
|
registrationKey: 'api-only',
|
|
68
67
|
skipUiRegistries: true,
|
|
69
68
|
skipCoreInjectionWidgets: true,
|
|
@@ -72,22 +71,11 @@ describe('partitioned bootstrap registration', () => {
|
|
|
72
71
|
apiBootstrap()
|
|
73
72
|
await waitForAsyncRegistration()
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// override resolves to the `widgetId` the slots reference (#5152).
|
|
81
|
-
expect(registerCoreInjectionTablesMock).toHaveBeenCalledWith([], [entry])
|
|
74
|
+
expect(registerCoreInjectionWidgetsMock).not.toHaveBeenCalled()
|
|
75
|
+
// The raw widget entries still travel with the tables so a `key`-spelled injection
|
|
76
|
+
// override resolves to the `widgetId` the slots reference (#5152), even though this
|
|
77
|
+
// bootstrap deliberately skips registering the widgets themselves.
|
|
78
|
+
expect(registerCoreInjectionTablesMock).toHaveBeenCalledWith([], [])
|
|
82
79
|
expect(registerEnabledModuleIdsMock).toHaveBeenCalledTimes(1)
|
|
83
80
|
})
|
|
84
|
-
|
|
85
|
-
it('lets the full bootstrap replace the registry outright', async () => {
|
|
86
|
-
const fullBootstrap = createBootstrap(emptyBootstrapData, { registrationKey: 'full-only' })
|
|
87
|
-
|
|
88
|
-
fullBootstrap()
|
|
89
|
-
await waitForAsyncRegistration()
|
|
90
|
-
|
|
91
|
-
expect(registerCoreInjectionWidgetsMock).toHaveBeenCalledWith([], undefined)
|
|
92
|
-
})
|
|
93
81
|
})
|
|
@@ -16,7 +16,7 @@ import { CLIENT_ONLY_STUB_NAMESPACE, createClientOnlyStubPlugin } from './client
|
|
|
16
16
|
import path from 'node:path'
|
|
17
17
|
import fs from 'node:fs'
|
|
18
18
|
import crypto from 'node:crypto'
|
|
19
|
-
import { createRequire
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
20
|
import { pathToFileURL } from 'node:url'
|
|
21
21
|
|
|
22
22
|
let activeBootstrapLoads = 0
|
|
@@ -77,44 +77,6 @@ class GeneratedFileNotFoundError extends Error {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function splitBareSpecifier(specifier: string): { packageName: string; subpath: string } | null {
|
|
81
|
-
const segments = specifier.split('/')
|
|
82
|
-
const nameLength = specifier.startsWith('@') ? 2 : 1
|
|
83
|
-
if (segments.length <= nameLength || segments.slice(0, nameLength).some((segment) => !segment)) return null
|
|
84
|
-
return {
|
|
85
|
-
packageName: segments.slice(0, nameLength).join('/'),
|
|
86
|
-
subpath: segments.slice(nameLength).join('/'),
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Rewrites an externalized `pkg/subpath` specifier to the file Node's ESM resolver can load
|
|
92
|
-
* when `pkg` publishes no `exports` map (#6993). Builtins, packages with `exports`, subpaths
|
|
93
|
-
* that already carry an extension and anything that fails to resolve keep the original
|
|
94
|
-
* specifier, so the bundle never gets worse than before.
|
|
95
|
-
*/
|
|
96
|
-
function rewriteLegacySubpathSpecifier(
|
|
97
|
-
specifier: string,
|
|
98
|
-
appRequire: NodeRequire,
|
|
99
|
-
onRewrite: (manifestPath: string) => void,
|
|
100
|
-
): string {
|
|
101
|
-
if (isBuiltin(specifier)) return specifier
|
|
102
|
-
const parts = splitBareSpecifier(specifier)
|
|
103
|
-
if (!parts || path.posix.extname(parts.subpath)) return specifier
|
|
104
|
-
try {
|
|
105
|
-
const manifestPath = appRequire.resolve(`${parts.packageName}/package.json`)
|
|
106
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as { exports?: unknown }
|
|
107
|
-
if (manifest.exports != null) return specifier
|
|
108
|
-
const resolvedFile = appRequire.resolve(specifier)
|
|
109
|
-
const relativeFile = path.relative(path.dirname(manifestPath), resolvedFile)
|
|
110
|
-
if (!relativeFile || relativeFile.startsWith('..') || path.isAbsolute(relativeFile)) return specifier
|
|
111
|
-
onRewrite(manifestPath)
|
|
112
|
-
return `${parts.packageName}/${relativeFile.split(path.sep).join('/')}`
|
|
113
|
-
} catch {
|
|
114
|
-
return specifier
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
80
|
/**
|
|
119
81
|
* esbuild plugins for the CLI bundle, in resolution order. The client-only stub must come
|
|
120
82
|
* first so it wins over the alias and external plugins for `*.client` dynamic imports.
|
|
@@ -123,10 +85,7 @@ function rewriteLegacySubpathSpecifier(
|
|
|
123
85
|
* `createClientOnlyStubPlugin` in isolation stays green if the plugin is dropped from this
|
|
124
86
|
* list, which would silently reintroduce #4623.
|
|
125
87
|
*/
|
|
126
|
-
export function createCliBundlePlugins(
|
|
127
|
-
appRoot: string,
|
|
128
|
-
options: { onLegacySubpathRewrite?: (manifestPath: string) => void } = {},
|
|
129
|
-
): import('esbuild').Plugin[] {
|
|
88
|
+
export function createCliBundlePlugins(appRoot: string): import('esbuild').Plugin[] {
|
|
130
89
|
// Plugin to resolve the @/ alias the way the app tsconfig maps it:
|
|
131
90
|
// `@/.mercato/*` to the app root, every other `@/*` to the app's src/ directory.
|
|
132
91
|
const aliasPlugin: import('esbuild').Plugin = {
|
|
@@ -154,19 +113,6 @@ export function createCliBundlePlugins(
|
|
|
154
113
|
},
|
|
155
114
|
}
|
|
156
115
|
|
|
157
|
-
// Node's ESM resolver only maps an extensionless package subpath (`next/link`) when the
|
|
158
|
-
// package publishes an `exports` map; legacy packages need the real file path instead.
|
|
159
|
-
const appRequire = createRequire(path.join(appRoot, 'package.json'))
|
|
160
|
-
const externalSpecifierCache = new Map<string, string>()
|
|
161
|
-
const onLegacySubpathRewrite = options.onLegacySubpathRewrite ?? (() => undefined)
|
|
162
|
-
const resolveExternalSpecifier = (specifier: string): string => {
|
|
163
|
-
const cached = externalSpecifierCache.get(specifier)
|
|
164
|
-
if (cached !== undefined) return cached
|
|
165
|
-
const resolved = rewriteLegacySubpathSpecifier(specifier, appRequire, onLegacySubpathRewrite)
|
|
166
|
-
externalSpecifierCache.set(specifier, resolved)
|
|
167
|
-
return resolved
|
|
168
|
-
}
|
|
169
|
-
|
|
170
116
|
// Plugin to mark non-JSON package imports as external
|
|
171
117
|
const externalNonJsonPlugin: import('esbuild').Plugin = {
|
|
172
118
|
name: 'external-non-json',
|
|
@@ -183,7 +129,7 @@ export function createCliBundlePlugins(
|
|
|
183
129
|
return null // Let esbuild handle it
|
|
184
130
|
}
|
|
185
131
|
// Otherwise mark as external
|
|
186
|
-
return { path:
|
|
132
|
+
return { path: args.path, external: true }
|
|
187
133
|
})
|
|
188
134
|
},
|
|
189
135
|
}
|
|
@@ -191,7 +137,7 @@ export function createCliBundlePlugins(
|
|
|
191
137
|
return [createClientOnlyStubPlugin(), aliasPlugin, externalNonJsonPlugin]
|
|
192
138
|
}
|
|
193
139
|
|
|
194
|
-
const DYNAMIC_LOADER_CACHE_VERSION =
|
|
140
|
+
const DYNAMIC_LOADER_CACHE_VERSION = 4
|
|
195
141
|
|
|
196
142
|
type DynamicLoaderCacheMetadata = {
|
|
197
143
|
version: number
|
|
@@ -487,7 +433,6 @@ async function compileAppSourceFileWithActiveEsbuild(
|
|
|
487
433
|
// Dynamically import esbuild only when needed
|
|
488
434
|
const esbuild = await getEsbuildRuntime()
|
|
489
435
|
|
|
490
|
-
const rewrittenPackageManifests = new Set<string>()
|
|
491
436
|
// Use esbuild.build with bundling to handle JSON imports
|
|
492
437
|
const result = await esbuild.build({
|
|
493
438
|
entryPoints: [tsPath],
|
|
@@ -499,9 +444,7 @@ async function compileAppSourceFileWithActiveEsbuild(
|
|
|
499
444
|
platform: 'node',
|
|
500
445
|
target: 'node18',
|
|
501
446
|
tsconfig: appTsconfig,
|
|
502
|
-
plugins: createCliBundlePlugins(appRoot,
|
|
503
|
-
onLegacySubpathRewrite: (manifestPath) => rewrittenPackageManifests.add(manifestPath),
|
|
504
|
-
}),
|
|
447
|
+
plugins: createCliBundlePlugins(appRoot),
|
|
505
448
|
// Allow JSON imports
|
|
506
449
|
loader: { '.json': 'json' },
|
|
507
450
|
})
|
|
@@ -512,7 +455,6 @@ async function compileAppSourceFileWithActiveEsbuild(
|
|
|
512
455
|
dependencies: {
|
|
513
456
|
...collectDependencyHashes(appRoot, result.metafile.inputs),
|
|
514
457
|
...hashFilesRelativeTo(appRoot, tsconfigPaths),
|
|
515
|
-
...hashFilesRelativeTo(appRoot, [...rewrittenPackageManifests]),
|
|
516
458
|
},
|
|
517
459
|
}
|
|
518
460
|
fs.writeFileSync(metadataPath, JSON.stringify(metadata))
|
|
@@ -143,14 +143,9 @@ async function registerWidgetsAndOptionalPackages(data: BootstrapData, options:
|
|
|
143
143
|
// browser-facing UI registries. API-only bootstraps avoid loading @open-mercato/ui.
|
|
144
144
|
try {
|
|
145
145
|
const coreInjection = await import('@open-mercato/core/modules/widgets/lib/injection')
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// API-only processes with an unregistered registry that throws on first read.
|
|
150
|
-
coreInjection.registerCoreInjectionWidgets(
|
|
151
|
-
data.injectionWidgetEntries,
|
|
152
|
-
options.skipCoreInjectionWidgets ? { mode: 'merge' } : undefined,
|
|
153
|
-
)
|
|
146
|
+
if (!options.skipCoreInjectionWidgets) {
|
|
147
|
+
coreInjection.registerCoreInjectionWidgets(data.injectionWidgetEntries)
|
|
148
|
+
}
|
|
154
149
|
coreInjection.registerCoreInjectionTables(data.injectionTables, data.injectionWidgetEntries)
|
|
155
150
|
coreInjection.registerEnabledModuleIds(
|
|
156
151
|
data.modules.map((module) => module.id).filter((id): id is string => typeof id === 'string' && id.length > 0),
|
|
@@ -73,11 +73,7 @@ export interface BootstrapOptions {
|
|
|
73
73
|
registrationKey?: string
|
|
74
74
|
/** Skip browser-facing dashboard/injection registries in API-only runtimes. */
|
|
75
75
|
skipUiRegistries?: boolean
|
|
76
|
-
/**
|
|
77
|
-
* Keep an existing core injection-widget registry intact in partitioned runtimes: this
|
|
78
|
-
* bootstrap's entries are merged in rather than published as the whole registry, so a
|
|
79
|
-
* partition that registers second never shrinks what the first one already exposed.
|
|
80
|
-
*/
|
|
76
|
+
/** Keep an existing core injection-widget registry intact in partitioned runtimes. */
|
|
81
77
|
skipCoreInjectionWidgets?: boolean
|
|
82
78
|
onRegistrationComplete?: () => void
|
|
83
79
|
appDiRegistrar?: AppDiRegistrar
|
|
@@ -3,5 +3,3 @@ export { matchesOne } from './matchesOne'
|
|
|
3
3
|
export { matchesScope } from './matchesScope'
|
|
4
4
|
export { unionScopes } from './unionScopes'
|
|
5
5
|
export { intersectScopes } from './intersectScopes'
|
|
6
|
-
export { canonicalizeEffectiveScope } from './canonicalizeEffectiveScope'
|
|
7
|
-
export { hashEffectiveScope } from './hashEffectiveScope'
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
isCommandInterceptorError,
|
|
8
8
|
} from '@open-mercato/shared/lib/commands'
|
|
9
9
|
import { registerCommandInterceptors } from '@open-mercato/shared/lib/commands/command-interceptor-store'
|
|
10
|
-
import type { CommandInterceptor
|
|
10
|
+
import type { CommandInterceptor } from '@open-mercato/shared/lib/commands/command-interceptor'
|
|
11
11
|
|
|
12
12
|
describe('CommandBus', () => {
|
|
13
13
|
afterEach(() => {
|
|
@@ -296,57 +296,6 @@ describe('CommandBus', () => {
|
|
|
296
296
|
)
|
|
297
297
|
})
|
|
298
298
|
|
|
299
|
-
it('defers opted-in before snapshots until after the transactional write guard', async () => {
|
|
300
|
-
const calls: string[] = []
|
|
301
|
-
const transactionalEm = { id: 'transaction-em' }
|
|
302
|
-
const buildLogMock = jest.fn(() => ({
|
|
303
|
-
actionLabel: 'Transaction snapshot',
|
|
304
|
-
resourceKind: 'test',
|
|
305
|
-
resourceId: 'transactional',
|
|
306
|
-
}))
|
|
307
|
-
const prepare = jest.fn(async (_input, ctx) => {
|
|
308
|
-
calls.push('prepare')
|
|
309
|
-
expect(ctx.transactionalEm).toBe(transactionalEm)
|
|
310
|
-
return { before: { state: 'locked-before' } }
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
registerCommand({
|
|
314
|
-
id: 'test.command.transactional-snapshot',
|
|
315
|
-
prepare,
|
|
316
|
-
prepareSnapshotInsideTransaction: true,
|
|
317
|
-
execute: jest.fn(async (_input, ctx) => {
|
|
318
|
-
calls.push('execute')
|
|
319
|
-
await ctx.beforeTransactionalWrite?.(transactionalEm as never)
|
|
320
|
-
calls.push('mutate')
|
|
321
|
-
return { ok: true }
|
|
322
|
-
}),
|
|
323
|
-
buildLog: buildLogMock,
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
const logMock = jest.fn(async () => ({ id: 'transaction-log' }))
|
|
327
|
-
const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
|
|
328
|
-
container.register({ actionLogService: asValue({ log: logMock }) })
|
|
329
|
-
const bus = new CommandBus()
|
|
330
|
-
const ctx = {
|
|
331
|
-
container,
|
|
332
|
-
auth: { sub: 'user-transaction', tenantId: 'tenant-transaction', orgId: null },
|
|
333
|
-
organizationScope: null,
|
|
334
|
-
selectedOrganizationId: null,
|
|
335
|
-
organizationIds: null,
|
|
336
|
-
beforeTransactionalWrite: jest.fn(async () => {
|
|
337
|
-
calls.push('lock')
|
|
338
|
-
}),
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
await bus.execute('test.command.transactional-snapshot', { input: {}, ctx })
|
|
342
|
-
|
|
343
|
-
expect(calls).toEqual(['execute', 'lock', 'prepare', 'mutate'])
|
|
344
|
-
expect(prepare).toHaveBeenCalledTimes(1)
|
|
345
|
-
expect(buildLogMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
346
|
-
snapshots: { before: { state: 'locked-before' }, after: undefined },
|
|
347
|
-
}))
|
|
348
|
-
})
|
|
349
|
-
|
|
350
299
|
it('loads a command file lazily before execution', async () => {
|
|
351
300
|
const execute = jest.fn(async () => ({ ok: true }))
|
|
352
301
|
registerCommandLoaders([
|
|
@@ -589,54 +538,6 @@ describe('CommandBus', () => {
|
|
|
589
538
|
})
|
|
590
539
|
|
|
591
540
|
describe('interceptor rejections', () => {
|
|
592
|
-
it('passes the original request to beforeExecute for preflight version checks', async () => {
|
|
593
|
-
registerCommand({ id: 'test.request-context', execute: jest.fn(async () => ({ ok: true })) })
|
|
594
|
-
const beforeExecute = jest.fn(async (_input: unknown, context: { request?: Request | null }) => {
|
|
595
|
-
expect(context.request?.headers.get('x-om-ext-optimistic-lock-expected-updated-at')).toBe('2026-09-29T11:00:00.000Z')
|
|
596
|
-
return { ok: false, status: 422 }
|
|
597
|
-
})
|
|
598
|
-
registerCommandInterceptors([{ moduleId: 'test', interceptors: [{
|
|
599
|
-
id: 'test.request-context-interceptor', targetCommand: 'test.request-context', beforeExecute,
|
|
600
|
-
}] }])
|
|
601
|
-
const request = new Request('http://localhost/test', { headers: { 'x-om-ext-optimistic-lock-expected-updated-at': '2026-09-29T11:00:00.000Z' } })
|
|
602
|
-
const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
|
|
603
|
-
const bus = new CommandBus()
|
|
604
|
-
await expect(bus.execute('test.request-context', { input: {}, ctx: {
|
|
605
|
-
container,
|
|
606
|
-
auth: { sub: 'user-1', tenantId: 'tenant-1', orgId: null },
|
|
607
|
-
organizationScope: null,
|
|
608
|
-
selectedOrganizationId: null,
|
|
609
|
-
organizationIds: null,
|
|
610
|
-
request,
|
|
611
|
-
} })).rejects.toMatchObject({ status: 422 })
|
|
612
|
-
expect(beforeExecute).toHaveBeenCalledTimes(1)
|
|
613
|
-
})
|
|
614
|
-
|
|
615
|
-
it('passes request and organization scope to both execution lifecycle hooks', async () => {
|
|
616
|
-
registerCommand({ id: 'test.lifecycle-context', execute: async () => ({ ok: true }) })
|
|
617
|
-
const request = new Request('http://localhost/test')
|
|
618
|
-
const organizationScope = { selectedId: 'parent', filterIds: ['parent', 'child'], allowedIds: ['parent', 'child'], tenantId: 'tenant-1' }
|
|
619
|
-
const beforeExecute = jest.fn(async (_input: unknown, context: CommandInterceptorContext) => {
|
|
620
|
-
expect(context.request).toBe(request)
|
|
621
|
-
expect(context.organizationScope).toBe(organizationScope)
|
|
622
|
-
return { ok: true }
|
|
623
|
-
})
|
|
624
|
-
const afterExecute = jest.fn(async (_input: unknown, _result: unknown, context: CommandInterceptorContext) => {
|
|
625
|
-
expect(context.request).toBe(request)
|
|
626
|
-
expect(context.organizationScope).toBe(organizationScope)
|
|
627
|
-
})
|
|
628
|
-
registerCommandInterceptors([{ moduleId: 'test', interceptors: [{
|
|
629
|
-
id: 'test.lifecycle-context-interceptor', targetCommand: 'test.lifecycle-context', beforeExecute, afterExecute,
|
|
630
|
-
}] }])
|
|
631
|
-
await new CommandBus().execute('test.lifecycle-context', { input: {}, ctx: {
|
|
632
|
-
container: createContainer({ injectionMode: InjectionMode.CLASSIC }),
|
|
633
|
-
auth: { sub: 'user-1', tenantId: 'tenant-1', orgId: 'parent' },
|
|
634
|
-
organizationScope, selectedOrganizationId: 'parent', organizationIds: ['parent', 'child'], request,
|
|
635
|
-
} })
|
|
636
|
-
expect(beforeExecute).toHaveBeenCalledTimes(1)
|
|
637
|
-
expect(afterExecute).toHaveBeenCalledTimes(1)
|
|
638
|
-
})
|
|
639
|
-
|
|
640
541
|
const blockingInterceptor = (result: Record<string, unknown>): CommandInterceptor => ({
|
|
641
542
|
id: 'test.block',
|
|
642
543
|
targetCommand: 'test.*',
|
|
@@ -297,8 +297,6 @@ export class CommandBus {
|
|
|
297
297
|
auth: baseOptions.ctx.auth ?? null,
|
|
298
298
|
selectedOrganizationId: baseOptions.ctx.selectedOrganizationId ?? baseOptions.ctx.auth?.orgId ?? null,
|
|
299
299
|
container: baseOptions.ctx.container,
|
|
300
|
-
organizationScope: baseOptions.ctx.organizationScope ?? null,
|
|
301
|
-
request: baseOptions.ctx.request ?? null,
|
|
302
300
|
}
|
|
303
301
|
const beforeResult = await runCommandInterceptorsBefore(
|
|
304
302
|
allInterceptors, commandId, baseOptions.input, interceptorCtx, resolvedUserFeatures,
|
|
@@ -340,40 +338,16 @@ export class CommandBus {
|
|
|
340
338
|
}
|
|
341
339
|
|
|
342
340
|
const executeCore = async (coreOptions: CommandExecutionOptions<TInput>) => {
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
if (handler.prepareSnapshotInsideTransaction && handler.prepare) {
|
|
346
|
-
const prepare = handler.prepare
|
|
347
|
-
const originalCtx = coreOptions.ctx
|
|
348
|
-
const originalBeforeTransactionalWrite = originalCtx.beforeTransactionalWrite
|
|
349
|
-
let prepared = false
|
|
350
|
-
let transactionalCtx: CommandRuntimeContext
|
|
351
|
-
transactionalCtx = {
|
|
352
|
-
...originalCtx,
|
|
353
|
-
beforeTransactionalWrite: async (em, input) => {
|
|
354
|
-
await originalBeforeTransactionalWrite?.(em, input)
|
|
355
|
-
if (prepared) return
|
|
356
|
-
Object.assign(
|
|
357
|
-
snapshots,
|
|
358
|
-
(await prepare(coreOptions.input, { ...transactionalCtx, transactionalEm: em })) ?? {},
|
|
359
|
-
)
|
|
360
|
-
prepared = true
|
|
361
|
-
},
|
|
362
|
-
}
|
|
363
|
-
executionOptions = { ...coreOptions, ctx: transactionalCtx }
|
|
364
|
-
} else {
|
|
365
|
-
Object.assign(snapshots, await this.prepareSnapshots(handler, coreOptions))
|
|
366
|
-
}
|
|
367
|
-
effectiveOptions = executionOptions
|
|
368
|
-
const sourceLog = executionOptions.redoLogEntry ?? null
|
|
341
|
+
const sourceLog = coreOptions.redoLogEntry ?? null
|
|
342
|
+
const snapshots = await this.prepareSnapshots(handler, coreOptions)
|
|
369
343
|
const result =
|
|
370
344
|
sourceLog && typeof handler.redo === 'function'
|
|
371
|
-
? await handler.redo({ input:
|
|
372
|
-
: await handler.execute(
|
|
373
|
-
const afterSnapshot = await this.captureAfter(handler,
|
|
345
|
+
? await handler.redo({ input: coreOptions.input, ctx: coreOptions.ctx, logEntry: sourceLog })
|
|
346
|
+
: await handler.execute(coreOptions.input, coreOptions.ctx)
|
|
347
|
+
const afterSnapshot = await this.captureAfter(handler, coreOptions, result)
|
|
374
348
|
const snapshotsWithAfter = { ...snapshots, after: afterSnapshot }
|
|
375
|
-
const logMeta = await this.buildLog(handler,
|
|
376
|
-
let mergedMeta = this.mergeMetadata(
|
|
349
|
+
const logMeta = await this.buildLog(handler, coreOptions, result, snapshotsWithAfter)
|
|
350
|
+
let mergedMeta = this.mergeMetadata(coreOptions.metadata, logMeta)
|
|
377
351
|
// Interceptors opt into audit-log enrichment with a reserved `logContext` key rather
|
|
378
352
|
// than the generic `context` one, so the metadata an interceptor already passes to its
|
|
379
353
|
// own afterExecute hook is never silently promoted into audit storage.
|
|
@@ -388,7 +362,7 @@ export class CommandBus {
|
|
|
388
362
|
...logContextRecord,
|
|
389
363
|
}
|
|
390
364
|
}
|
|
391
|
-
const baseContext = asRecord(
|
|
365
|
+
const baseContext = asRecord(coreOptions.metadata?.context) ?? {}
|
|
392
366
|
const logMetaContext = asRecord(logMeta?.context) ?? {}
|
|
393
367
|
if (Object.keys(interceptorContextMerged).length > 0 || Object.keys(baseContext).length > 0 || Object.keys(logMetaContext).length > 0) {
|
|
394
368
|
mergedMeta = mergedMeta ?? {}
|
|
@@ -402,7 +376,7 @@ export class CommandBus {
|
|
|
402
376
|
if (undoable && mergedMeta?.replayable !== false) {
|
|
403
377
|
mergedMeta = mergedMeta ?? {}
|
|
404
378
|
if (!mergedMeta.undoToken) mergedMeta.undoToken = defaultUndoToken()
|
|
405
|
-
if (mergedMeta.actorUserId === undefined) mergedMeta.actorUserId =
|
|
379
|
+
if (mergedMeta.actorUserId === undefined) mergedMeta.actorUserId = coreOptions.ctx.auth?.sub ?? null
|
|
406
380
|
} else if (mergedMeta?.replayable === false) {
|
|
407
381
|
mergedMeta.undoToken = null
|
|
408
382
|
}
|
|
@@ -431,7 +405,7 @@ export class CommandBus {
|
|
|
431
405
|
if (inferred) mergedMeta.changes = inferred
|
|
432
406
|
}
|
|
433
407
|
}
|
|
434
|
-
const logEntry = await this.persistLog(commandId,
|
|
408
|
+
const logEntry = await this.persistLog(commandId, coreOptions, mergedMeta)
|
|
435
409
|
return { result, mergedMeta, logEntry }
|
|
436
410
|
}
|
|
437
411
|
|
|
@@ -500,8 +474,6 @@ export class CommandBus {
|
|
|
500
474
|
auth: effectiveOptions.ctx.auth ?? null,
|
|
501
475
|
selectedOrganizationId: effectiveOptions.ctx.selectedOrganizationId ?? effectiveOptions.ctx.auth?.orgId ?? null,
|
|
502
476
|
container: effectiveOptions.ctx.container,
|
|
503
|
-
organizationScope: effectiveOptions.ctx.organizationScope ?? null,
|
|
504
|
-
request: effectiveOptions.ctx.request ?? null,
|
|
505
477
|
}
|
|
506
478
|
const afterResult = await runCommandInterceptorsAfter(
|
|
507
479
|
allInterceptors, commandId, effectiveOptions.input, result, interceptorCtx,
|
|
@@ -573,8 +545,6 @@ export class CommandBus {
|
|
|
573
545
|
auth: replayCtx.auth ?? null,
|
|
574
546
|
selectedOrganizationId: replayCtx.selectedOrganizationId ?? replayCtx.auth?.orgId ?? null,
|
|
575
547
|
container: replayCtx.container,
|
|
576
|
-
organizationScope: replayCtx.organizationScope ?? null,
|
|
577
|
-
request: replayCtx.request ?? null,
|
|
578
548
|
}
|
|
579
549
|
const beforeResult = await runCommandInterceptorsBeforeUndo(
|
|
580
550
|
allInterceptors, log.commandId, undoCtx, interceptorCtx, resolvedUserFeatures,
|
|
@@ -689,8 +659,6 @@ export class CommandBus {
|
|
|
689
659
|
auth: ctx.auth ?? null,
|
|
690
660
|
selectedOrganizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
691
661
|
container: ctx.container,
|
|
692
|
-
organizationScope: ctx.organizationScope ?? null,
|
|
693
|
-
request: ctx.request ?? null,
|
|
694
662
|
}
|
|
695
663
|
await runCommandInterceptorsAfterUndo(
|
|
696
664
|
allInterceptors, log.commandId, undoCtx, interceptorCtx,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AwilixContainer } from 'awilix'
|
|
2
|
-
import type { OrganizationScope } from '@open-mercato/shared/lib/auth/principal-service'
|
|
3
2
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
4
3
|
|
|
5
4
|
export interface CommandInterceptor {
|
|
@@ -48,11 +47,8 @@ export interface CommandInterceptorContext {
|
|
|
48
47
|
auth: AuthContext | null
|
|
49
48
|
/** Selected organization ID */
|
|
50
49
|
selectedOrganizationId: string | null
|
|
51
|
-
organizationScope?: OrganizationScope | null
|
|
52
50
|
/** DI container (read-only usage recommended) */
|
|
53
51
|
container: AwilixContainer
|
|
54
|
-
/** Original request, when the command was called through HTTP. */
|
|
55
|
-
request?: Request | null
|
|
56
52
|
/** Metadata passthrough from beforeExecute to afterExecute (or beforeUndo to afterUndo) */
|
|
57
53
|
metadata?: Record<string, unknown>
|
|
58
54
|
}
|
|
@@ -67,14 +67,6 @@ export type CommandRuntimeContext = {
|
|
|
67
67
|
* surrounding work as a single atomic, single-locked operation.
|
|
68
68
|
*/
|
|
69
69
|
transactionalEm?: EntityManager
|
|
70
|
-
/**
|
|
71
|
-
* Optional server-side guard that supported command handlers invoke after
|
|
72
|
-
* opening their write transaction and before reading or mutating records.
|
|
73
|
-
* The guard receives that same transaction-bound EntityManager and, when the
|
|
74
|
-
* handler provides it, the final input after command interceptors. The input
|
|
75
|
-
* stays optional so existing handlers and callers remain source-compatible.
|
|
76
|
-
*/
|
|
77
|
-
beforeTransactionalWrite?: (em: EntityManager, input?: unknown) => Promise<void>
|
|
78
70
|
/**
|
|
79
71
|
* Identifies the owner of an already-active {@link transactionalEm} lifetime.
|
|
80
72
|
* Atomic replay accepts an ambient EntityManager only when this is the exact
|
|
@@ -251,12 +243,6 @@ export interface CommandHandler<TInput = unknown, TResult = unknown> {
|
|
|
251
243
|
*/
|
|
252
244
|
readonly outputSchema?: ZodTypeAny
|
|
253
245
|
prepare?(input: TInput, ctx: CommandRuntimeContext): Promise<{ before?: unknown } | null> | { before?: unknown } | null
|
|
254
|
-
/**
|
|
255
|
-
* Opt in to taking the undo snapshot inside the write transaction. The bus
|
|
256
|
-
* defers `prepare()` into `ctx.beforeTransactionalWrite`, after the handler's
|
|
257
|
-
* transaction guard and before its first read or mutation.
|
|
258
|
-
*/
|
|
259
|
-
readonly prepareSnapshotInsideTransaction?: boolean
|
|
260
246
|
execute(input: TInput, ctx: CommandRuntimeContext): Promise<TResult> | TResult
|
|
261
247
|
buildLog?(args: CommandLogBuilderArgs<TInput, TResult>): Promise<CommandLogMetadata | null | undefined> | CommandLogMetadata | null | undefined
|
|
262
248
|
captureAfter?(input: TInput, result: TResult, ctx: CommandRuntimeContext): Promise<unknown> | unknown
|
|
@@ -28,22 +28,4 @@ describe('CRUD export serializers', () => {
|
|
|
28
28
|
'"\'\t=HYPERLINK(""https://example.invalid"")","\'\r+1+1",\'-7',
|
|
29
29
|
])
|
|
30
30
|
})
|
|
31
|
-
|
|
32
|
-
it('escapes and neutralizes CSV header cells the same way as data cells', () => {
|
|
33
|
-
const serialized = serializeExport({
|
|
34
|
-
columns: [
|
|
35
|
-
{ field: 'company', header: 'Name, Inc.' },
|
|
36
|
-
{ field: 'formula', header: '=HEADER' },
|
|
37
|
-
{ field: 'plain', header: 'Balance' },
|
|
38
|
-
],
|
|
39
|
-
rows: [
|
|
40
|
-
{ company: 'Acme', formula: 'ok', plain: 1 },
|
|
41
|
-
],
|
|
42
|
-
}, 'csv')
|
|
43
|
-
|
|
44
|
-
expect(serialized.body.split('\n')).toEqual([
|
|
45
|
-
'"Name, Inc.",\'=HEADER,Balance',
|
|
46
|
-
'Acme,ok,1',
|
|
47
|
-
])
|
|
48
|
-
})
|
|
49
31
|
})
|