@open-mercato/shared 0.4.2-canary-c02407ff85
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/build.mjs +101 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/api/crud.js +47 -0
- package/dist/lib/api/crud.js.map +7 -0
- package/dist/lib/api/scoped.js +140 -0
- package/dist/lib/api/scoped.js.map +7 -0
- package/dist/lib/auth/jwt.js +34 -0
- package/dist/lib/auth/jwt.js.map +7 -0
- package/dist/lib/auth/server.js +157 -0
- package/dist/lib/auth/server.js.map +7 -0
- package/dist/lib/boolean.js +22 -0
- package/dist/lib/boolean.js.map +7 -0
- package/dist/lib/bootstrap/appResolver.js +43 -0
- package/dist/lib/bootstrap/appResolver.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +108 -0
- package/dist/lib/bootstrap/dynamicLoader.js.map +7 -0
- package/dist/lib/bootstrap/factory.js +59 -0
- package/dist/lib/bootstrap/factory.js.map +7 -0
- package/dist/lib/bootstrap/index.js +11 -0
- package/dist/lib/bootstrap/index.js.map +7 -0
- package/dist/lib/bootstrap/types.js +1 -0
- package/dist/lib/bootstrap/types.js.map +7 -0
- package/dist/lib/cache/segments.js +36 -0
- package/dist/lib/cache/segments.js.map +7 -0
- package/dist/lib/cli/progress.js +46 -0
- package/dist/lib/cli/progress.js.map +7 -0
- package/dist/lib/commands/command-bus.js +285 -0
- package/dist/lib/commands/command-bus.js.map +7 -0
- package/dist/lib/commands/customFieldSnapshots.js +66 -0
- package/dist/lib/commands/customFieldSnapshots.js.map +7 -0
- package/dist/lib/commands/helpers.js +98 -0
- package/dist/lib/commands/helpers.js.map +7 -0
- package/dist/lib/commands/index.js +8 -0
- package/dist/lib/commands/index.js.map +7 -0
- package/dist/lib/commands/operationMetadata.js +32 -0
- package/dist/lib/commands/operationMetadata.js.map +7 -0
- package/dist/lib/commands/registry.js +43 -0
- package/dist/lib/commands/registry.js.map +7 -0
- package/dist/lib/commands/scope.js +44 -0
- package/dist/lib/commands/scope.js.map +7 -0
- package/dist/lib/commands/types.js +8 -0
- package/dist/lib/commands/types.js.map +7 -0
- package/dist/lib/crud/cache-stats.js +98 -0
- package/dist/lib/crud/cache-stats.js.map +7 -0
- package/dist/lib/crud/cache.js +175 -0
- package/dist/lib/crud/cache.js.map +7 -0
- package/dist/lib/crud/custom-fields-client.js +52 -0
- package/dist/lib/crud/custom-fields-client.js.map +7 -0
- package/dist/lib/crud/custom-fields.js +467 -0
- package/dist/lib/crud/custom-fields.js.map +7 -0
- package/dist/lib/crud/errors.js +24 -0
- package/dist/lib/crud/errors.js.map +7 -0
- package/dist/lib/crud/exporters.js +154 -0
- package/dist/lib/crud/exporters.js.map +7 -0
- package/dist/lib/crud/factory.js +1311 -0
- package/dist/lib/crud/factory.js.map +7 -0
- package/dist/lib/crud/types.js +1 -0
- package/dist/lib/crud/types.js.map +7 -0
- package/dist/lib/custom-fields/normalize.js +36 -0
- package/dist/lib/custom-fields/normalize.js.map +7 -0
- package/dist/lib/data/engine.js +396 -0
- package/dist/lib/data/engine.js.map +7 -0
- package/dist/lib/db/escapeLikePattern.js +5 -0
- package/dist/lib/db/escapeLikePattern.js.map +7 -0
- package/dist/lib/db/mikro.js +82 -0
- package/dist/lib/db/mikro.js.map +7 -0
- package/dist/lib/di/container.js +94 -0
- package/dist/lib/di/container.js.map +7 -0
- package/dist/lib/email/send.js +12 -0
- package/dist/lib/email/send.js.map +7 -0
- package/dist/lib/encryption/aes.js +58 -0
- package/dist/lib/encryption/aes.js.map +7 -0
- package/dist/lib/encryption/customFieldValues.js +49 -0
- package/dist/lib/encryption/customFieldValues.js.map +7 -0
- package/dist/lib/encryption/entityFields.js +26 -0
- package/dist/lib/encryption/entityFields.js.map +7 -0
- package/dist/lib/encryption/entityIds.js +80 -0
- package/dist/lib/encryption/entityIds.js.map +7 -0
- package/dist/lib/encryption/find.js +45 -0
- package/dist/lib/encryption/find.js.map +7 -0
- package/dist/lib/encryption/indexDoc.js +69 -0
- package/dist/lib/encryption/indexDoc.js.map +7 -0
- package/dist/lib/encryption/kms.js +282 -0
- package/dist/lib/encryption/kms.js.map +7 -0
- package/dist/lib/encryption/subscriber.js +330 -0
- package/dist/lib/encryption/subscriber.js.map +7 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js +252 -0
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +7 -0
- package/dist/lib/encryption/toggles.js +18 -0
- package/dist/lib/encryption/toggles.js.map +7 -0
- package/dist/lib/entities/naming.js +9 -0
- package/dist/lib/entities/naming.js.map +7 -0
- package/dist/lib/entities/system-entities.js +43 -0
- package/dist/lib/entities/system-entities.js.map +7 -0
- package/dist/lib/frontend/organizationEvents.js +41 -0
- package/dist/lib/frontend/organizationEvents.js.map +7 -0
- package/dist/lib/frontend/useOrganizationScope.js +32 -0
- package/dist/lib/frontend/useOrganizationScope.js.map +7 -0
- package/dist/lib/hotkeys/index.js +128 -0
- package/dist/lib/hotkeys/index.js.map +7 -0
- package/dist/lib/i18n/app-dictionaries.js +17 -0
- package/dist/lib/i18n/app-dictionaries.js.map +7 -0
- package/dist/lib/i18n/config.js +7 -0
- package/dist/lib/i18n/config.js.map +7 -0
- package/dist/lib/i18n/context.js +50 -0
- package/dist/lib/i18n/context.js.map +7 -0
- package/dist/lib/i18n/server.js +68 -0
- package/dist/lib/i18n/server.js.map +7 -0
- package/dist/lib/i18n/translate.js +45 -0
- package/dist/lib/i18n/translate.js.map +7 -0
- package/dist/lib/indexers/error-log.js +82 -0
- package/dist/lib/indexers/error-log.js.map +7 -0
- package/dist/lib/indexers/status-log.js +80 -0
- package/dist/lib/indexers/status-log.js.map +7 -0
- package/dist/lib/lib/auth/jwt.js +34 -0
- package/dist/lib/lib/auth/jwt.js.map +7 -0
- package/dist/lib/lib/auth/server.js +77 -0
- package/dist/lib/lib/auth/server.js.map +7 -0
- package/dist/lib/lib/email/send.js +12 -0
- package/dist/lib/lib/email/send.js.map +7 -0
- package/dist/lib/lib/i18n/config.js +7 -0
- package/dist/lib/lib/i18n/config.js.map +7 -0
- package/dist/lib/lib/i18n/context.js +31 -0
- package/dist/lib/lib/i18n/context.js.map +7 -0
- package/dist/lib/lib/utils.js +9 -0
- package/dist/lib/lib/utils.js.map +7 -0
- package/dist/lib/location/countries.js +68 -0
- package/dist/lib/location/countries.js.map +7 -0
- package/dist/lib/modules/index.js +6 -0
- package/dist/lib/modules/index.js.map +7 -0
- package/dist/lib/modules/registry.js +18 -0
- package/dist/lib/modules/registry.js.map +7 -0
- package/dist/lib/openapi/crud.js +137 -0
- package/dist/lib/openapi/crud.js.map +7 -0
- package/dist/lib/openapi/generator.js +1131 -0
- package/dist/lib/openapi/generator.js.map +7 -0
- package/dist/lib/openapi/index.js +10 -0
- package/dist/lib/openapi/index.js.map +7 -0
- package/dist/lib/openapi/sanitize.js +110 -0
- package/dist/lib/openapi/sanitize.js.map +7 -0
- package/dist/lib/openapi/types.js +1 -0
- package/dist/lib/openapi/types.js.map +7 -0
- package/dist/lib/profiler/index.js +258 -0
- package/dist/lib/profiler/index.js.map +7 -0
- package/dist/lib/query/engine.js +729 -0
- package/dist/lib/query/engine.js.map +7 -0
- package/dist/lib/query/join-utils.js +195 -0
- package/dist/lib/query/join-utils.js.map +7 -0
- package/dist/lib/query/types.js +9 -0
- package/dist/lib/query/types.js.map +7 -0
- package/dist/lib/search/config.js +32 -0
- package/dist/lib/search/config.js.map +7 -0
- package/dist/lib/search/tokenize.js +34 -0
- package/dist/lib/search/tokenize.js.map +7 -0
- package/dist/lib/slugify.js +24 -0
- package/dist/lib/slugify.js.map +7 -0
- package/dist/lib/testing/bootstrap.js +51 -0
- package/dist/lib/testing/bootstrap.js.map +7 -0
- package/dist/lib/testing/index.js +17 -0
- package/dist/lib/testing/index.js.map +7 -0
- package/dist/lib/testing/renderWithProviders.js +15 -0
- package/dist/lib/testing/renderWithProviders.js.map +7 -0
- package/dist/lib/url.js +12 -0
- package/dist/lib/url.js.map +7 -0
- package/dist/lib/utils.js +13 -0
- package/dist/lib/utils.js.map +7 -0
- package/dist/lib/version.js +7 -0
- package/dist/lib/version.js.map +7 -0
- package/dist/modules/dashboard/widgets.js +1 -0
- package/dist/modules/dashboard/widgets.js.map +7 -0
- package/dist/modules/dsl.js +30 -0
- package/dist/modules/dsl.js.map +7 -0
- package/dist/modules/entities/kinds.js +22 -0
- package/dist/modules/entities/kinds.js.map +7 -0
- package/dist/modules/entities/options.js +26 -0
- package/dist/modules/entities/options.js.map +7 -0
- package/dist/modules/entities/validation.js +102 -0
- package/dist/modules/entities/validation.js.map +7 -0
- package/dist/modules/entities/validators.js +88 -0
- package/dist/modules/entities/validators.js.map +7 -0
- package/dist/modules/entities.js +1 -0
- package/dist/modules/entities.js.map +7 -0
- package/dist/modules/navigation/sidebarPreferences.js +50 -0
- package/dist/modules/navigation/sidebarPreferences.js.map +7 -0
- package/dist/modules/perspectives/types.js +1 -0
- package/dist/modules/perspectives/types.js.map +7 -0
- package/dist/modules/registry.js +96 -0
- package/dist/modules/registry.js.map +7 -0
- package/dist/modules/search.js +15 -0
- package/dist/modules/search.js.map +7 -0
- package/dist/modules/vector.js +1 -0
- package/dist/modules/vector.js.map +7 -0
- package/dist/modules/widgets/injection-loader.js +180 -0
- package/dist/modules/widgets/injection-loader.js.map +7 -0
- package/dist/modules/widgets/injection.js +1 -0
- package/dist/modules/widgets/injection.js.map +7 -0
- package/dist/security/features.js +23 -0
- package/dist/security/features.js.map +7 -0
- package/dist/types/pg.d.js +1 -0
- package/dist/types/pg.d.js.map +7 -0
- package/dist/types/react-email.d.js +1 -0
- package/dist/types/react-email.d.js.map +7 -0
- package/dist/types/resend.d.js +1 -0
- package/dist/types/resend.d.js.map +7 -0
- package/jest.config.cjs +22 -0
- package/package.json +88 -0
- package/src/index.ts +0 -0
- package/src/lib/api/__tests__/scoped.test.ts +38 -0
- package/src/lib/api/crud.ts +59 -0
- package/src/lib/api/scoped.ts +239 -0
- package/src/lib/auth/jwt.ts +39 -0
- package/src/lib/auth/server.ts +199 -0
- package/src/lib/boolean.ts +17 -0
- package/src/lib/bootstrap/appResolver.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +177 -0
- package/src/lib/bootstrap/factory.ts +108 -0
- package/src/lib/bootstrap/index.ts +23 -0
- package/src/lib/bootstrap/types.ts +31 -0
- package/src/lib/cache/segments.ts +56 -0
- package/src/lib/cli/progress.ts +55 -0
- package/src/lib/commands/__tests__/command-bus.test.ts +84 -0
- package/src/lib/commands/__tests__/helpers.test.ts +42 -0
- package/src/lib/commands/command-bus.ts +349 -0
- package/src/lib/commands/customFieldSnapshots.ts +86 -0
- package/src/lib/commands/helpers.ts +143 -0
- package/src/lib/commands/index.ts +4 -0
- package/src/lib/commands/operationMetadata.ts +40 -0
- package/src/lib/commands/registry.ts +46 -0
- package/src/lib/commands/scope.ts +59 -0
- package/src/lib/commands/types.ts +63 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +333 -0
- package/src/lib/crud/__tests__/custom-fields.test.ts +150 -0
- package/src/lib/crud/cache-stats.ts +127 -0
- package/src/lib/crud/cache.ts +205 -0
- package/src/lib/crud/custom-fields-client.ts +54 -0
- package/src/lib/crud/custom-fields.ts +607 -0
- package/src/lib/crud/errors.ts +23 -0
- package/src/lib/crud/exporters.ts +188 -0
- package/src/lib/crud/factory.ts +1622 -0
- package/src/lib/crud/types.ts +29 -0
- package/src/lib/custom-fields/normalize.ts +45 -0
- package/src/lib/data/engine.ts +562 -0
- package/src/lib/db/escapeLikePattern.ts +2 -0
- package/src/lib/db/mikro.ts +100 -0
- package/src/lib/di/container.ts +105 -0
- package/src/lib/email/send.ts +18 -0
- package/src/lib/encryption/__tests__/customFieldValues.test.ts +63 -0
- package/src/lib/encryption/__tests__/indexDoc.test.ts +115 -0
- package/src/lib/encryption/aes.ts +64 -0
- package/src/lib/encryption/customFieldValues.ts +67 -0
- package/src/lib/encryption/entityFields.ts +39 -0
- package/src/lib/encryption/entityIds.ts +107 -0
- package/src/lib/encryption/find.ts +81 -0
- package/src/lib/encryption/indexDoc.ts +104 -0
- package/src/lib/encryption/kms.ts +337 -0
- package/src/lib/encryption/subscriber.ts +416 -0
- package/src/lib/encryption/tenantDataEncryptionService.ts +313 -0
- package/src/lib/encryption/toggles.ts +15 -0
- package/src/lib/entities/naming.ts +6 -0
- package/src/lib/entities/system-entities.ts +43 -0
- package/src/lib/frontend/organizationEvents.ts +55 -0
- package/src/lib/frontend/useOrganizationScope.ts +30 -0
- package/src/lib/hotkeys/index.ts +168 -0
- package/src/lib/i18n/app-dictionaries.ts +18 -0
- package/src/lib/i18n/config.ts +4 -0
- package/src/lib/i18n/context.tsx +66 -0
- package/src/lib/i18n/server.ts +74 -0
- package/src/lib/i18n/translate.ts +54 -0
- package/src/lib/indexers/error-log.ts +106 -0
- package/src/lib/indexers/status-log.ts +119 -0
- package/src/lib/lib/auth/jwt.ts +39 -0
- package/src/lib/lib/auth/server.ts +94 -0
- package/src/lib/lib/email/send.ts +18 -0
- package/src/lib/lib/i18n/config.ts +4 -0
- package/src/lib/lib/i18n/context.tsx +38 -0
- package/src/lib/lib/utils.ts +6 -0
- package/src/lib/location/countries.ts +97 -0
- package/src/lib/modules/index.ts +1 -0
- package/src/lib/modules/registry.ts +18 -0
- package/src/lib/openapi/crud.ts +218 -0
- package/src/lib/openapi/generator.ts +1311 -0
- package/src/lib/openapi/index.ts +4 -0
- package/src/lib/openapi/sanitize.ts +137 -0
- package/src/lib/openapi/types.ts +79 -0
- package/src/lib/profiler/index.ts +371 -0
- package/src/lib/query/__tests__/engine.test.ts +274 -0
- package/src/lib/query/engine.ts +837 -0
- package/src/lib/query/join-utils.ts +238 -0
- package/src/lib/query/types.ts +121 -0
- package/src/lib/search/config.ts +49 -0
- package/src/lib/search/tokenize.ts +45 -0
- package/src/lib/slugify.ts +28 -0
- package/src/lib/testing/bootstrap.ts +124 -0
- package/src/lib/testing/index.ts +15 -0
- package/src/lib/testing/renderWithProviders.tsx +31 -0
- package/src/lib/url.ts +12 -0
- package/src/lib/utils.ts +17 -0
- package/src/lib/version.ts +5 -0
- package/src/modules/__tests__/dsl.test.ts +35 -0
- package/src/modules/__tests__/registry.test.ts +300 -0
- package/src/modules/dashboard/widgets.ts +57 -0
- package/src/modules/dsl.ts +32 -0
- package/src/modules/entities/__tests__/validation.test.ts +52 -0
- package/src/modules/entities/kinds.ts +20 -0
- package/src/modules/entities/options.ts +36 -0
- package/src/modules/entities/validation.ts +118 -0
- package/src/modules/entities/validators.ts +93 -0
- package/src/modules/entities.ts +102 -0
- package/src/modules/navigation/sidebarPreferences.ts +62 -0
- package/src/modules/perspectives/types.ts +40 -0
- package/src/modules/registry.ts +249 -0
- package/src/modules/search.ts +325 -0
- package/src/modules/vector.ts +122 -0
- package/src/modules/widgets/__tests__/injection.test.ts +48 -0
- package/src/modules/widgets/injection-loader.ts +235 -0
- package/src/modules/widgets/injection.ts +120 -0
- package/src/security/features.ts +22 -0
- package/src/types/pg.d.ts +2 -0
- package/src/types/react-email.d.ts +2 -0
- package/src/types/resend.d.ts +2 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +6 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { BootstrapData } from './types'
|
|
2
|
+
import { findAppRoot, type AppRoot } from './appResolver'
|
|
3
|
+
import { registerEntityIds } from '../encryption/entityIds'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compile a TypeScript file to JavaScript using esbuild bundler.
|
|
10
|
+
* This bundles the file and all its dependencies, handling JSON imports properly.
|
|
11
|
+
* The compiled file is written next to the source file with a .mjs extension.
|
|
12
|
+
*/
|
|
13
|
+
async function compileAndImport(tsPath: string): Promise<Record<string, unknown>> {
|
|
14
|
+
const jsPath = tsPath.replace(/\.ts$/, '.mjs')
|
|
15
|
+
|
|
16
|
+
// Check if we need to recompile (source newer than compiled)
|
|
17
|
+
const tsExists = fs.existsSync(tsPath)
|
|
18
|
+
const jsExists = fs.existsSync(jsPath)
|
|
19
|
+
|
|
20
|
+
if (!tsExists) {
|
|
21
|
+
throw new Error(`Generated file not found: ${tsPath}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const needsCompile = !jsExists ||
|
|
25
|
+
fs.statSync(tsPath).mtimeMs > fs.statSync(jsPath).mtimeMs
|
|
26
|
+
|
|
27
|
+
if (needsCompile) {
|
|
28
|
+
// Dynamically import esbuild only when needed
|
|
29
|
+
const esbuild = await import('esbuild')
|
|
30
|
+
|
|
31
|
+
// The app root is 2 levels up from .mercato/generated/
|
|
32
|
+
const appRoot = path.dirname(path.dirname(path.dirname(tsPath)))
|
|
33
|
+
|
|
34
|
+
// Plugin to resolve @/ alias to app root (works for @app modules)
|
|
35
|
+
const aliasPlugin: import('esbuild').Plugin = {
|
|
36
|
+
name: 'alias-resolver',
|
|
37
|
+
setup(build) {
|
|
38
|
+
// Resolve @/ alias to app root
|
|
39
|
+
build.onResolve({ filter: /^@\// }, (args) => {
|
|
40
|
+
const resolved = path.join(appRoot, args.path.slice(2))
|
|
41
|
+
// Try with .ts extension if base path doesn't exist
|
|
42
|
+
if (!fs.existsSync(resolved) && fs.existsSync(resolved + '.ts')) {
|
|
43
|
+
return { path: resolved + '.ts' }
|
|
44
|
+
}
|
|
45
|
+
// Also check for /index.ts if it's a directory
|
|
46
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory() && fs.existsSync(path.join(resolved, 'index.ts'))) {
|
|
47
|
+
return { path: path.join(resolved, 'index.ts') }
|
|
48
|
+
}
|
|
49
|
+
return { path: resolved }
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Plugin to mark non-JSON package imports as external
|
|
55
|
+
const externalNonJsonPlugin: import('esbuild').Plugin = {
|
|
56
|
+
name: 'external-non-json',
|
|
57
|
+
setup(build) {
|
|
58
|
+
// Mark all package imports as external EXCEPT JSON files
|
|
59
|
+
build.onResolve({ filter: /^[^./]/ }, (args) => {
|
|
60
|
+
// If it's a JSON file, let esbuild bundle it
|
|
61
|
+
if (args.path.endsWith('.json')) {
|
|
62
|
+
return null // Let esbuild handle it
|
|
63
|
+
}
|
|
64
|
+
// Otherwise mark as external
|
|
65
|
+
return { path: args.path, external: true }
|
|
66
|
+
})
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Use esbuild.build with bundling to handle JSON imports
|
|
71
|
+
await esbuild.build({
|
|
72
|
+
entryPoints: [tsPath],
|
|
73
|
+
outfile: jsPath,
|
|
74
|
+
bundle: true,
|
|
75
|
+
format: 'esm',
|
|
76
|
+
platform: 'node',
|
|
77
|
+
target: 'node18',
|
|
78
|
+
plugins: [aliasPlugin, externalNonJsonPlugin],
|
|
79
|
+
// Allow JSON imports
|
|
80
|
+
loader: { '.json': 'json' },
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Import the compiled JavaScript
|
|
85
|
+
const fileUrl = pathToFileURL(jsPath).href
|
|
86
|
+
return import(fileUrl)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Dynamically load bootstrap data from a resolved app directory.
|
|
92
|
+
*
|
|
93
|
+
* IMPORTANT: This only works in unbundled contexts (CLI, tsx).
|
|
94
|
+
* Do NOT use this in Next.js bundled code - use static imports instead.
|
|
95
|
+
*
|
|
96
|
+
* For CLI context, we skip loading modules.generated.ts which has Next.js dependencies.
|
|
97
|
+
* CLI commands are discovered separately via the CLI module system.
|
|
98
|
+
*
|
|
99
|
+
* @param appRoot - Optional explicit app root path. If not provided, will search from cwd.
|
|
100
|
+
* @returns The loaded bootstrap data
|
|
101
|
+
* @throws Error if app root cannot be found or generated files are missing
|
|
102
|
+
*/
|
|
103
|
+
export async function loadBootstrapData(appRoot?: string): Promise<BootstrapData> {
|
|
104
|
+
const resolved: AppRoot | null = appRoot
|
|
105
|
+
? {
|
|
106
|
+
generatedDir: path.join(appRoot, '.mercato', 'generated'),
|
|
107
|
+
appDir: appRoot,
|
|
108
|
+
mercatoDir: path.join(appRoot, '.mercato'),
|
|
109
|
+
}
|
|
110
|
+
: findAppRoot()
|
|
111
|
+
|
|
112
|
+
if (!resolved) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'Could not find app root with .mercato/generated directory. ' +
|
|
115
|
+
'Make sure you run this command from within a Next.js app directory, ' +
|
|
116
|
+
'or run "yarn mercato generate" first to create the generated files.',
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { generatedDir } = resolved
|
|
121
|
+
|
|
122
|
+
// IMPORTANT: Load entity IDs FIRST and register them before loading modules.
|
|
123
|
+
// This is because modules (e.g., ce.ts files) use E.xxx.xxx at module scope,
|
|
124
|
+
// and they need entity IDs to be available when they're imported.
|
|
125
|
+
const entityIdsModule = await compileAndImport(path.join(generatedDir, 'entities.ids.generated.ts'))
|
|
126
|
+
registerEntityIds(entityIdsModule.E as BootstrapData['entityIds'])
|
|
127
|
+
|
|
128
|
+
// Now load the rest of the generated files.
|
|
129
|
+
// modules.cli.generated.ts excludes Next.js-dependent code (routes, APIs, widgets)
|
|
130
|
+
const [
|
|
131
|
+
modulesModule,
|
|
132
|
+
entitiesModule,
|
|
133
|
+
diModule,
|
|
134
|
+
searchModule,
|
|
135
|
+
] = await Promise.all([
|
|
136
|
+
compileAndImport(path.join(generatedDir, 'modules.cli.generated.ts')),
|
|
137
|
+
compileAndImport(path.join(generatedDir, 'entities.generated.ts')),
|
|
138
|
+
compileAndImport(path.join(generatedDir, 'di.generated.ts')),
|
|
139
|
+
compileAndImport(path.join(generatedDir, 'search.generated.ts')).catch(() => ({ searchModuleConfigs: [] })),
|
|
140
|
+
])
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
modules: modulesModule.modules as BootstrapData['modules'],
|
|
144
|
+
entities: entitiesModule.entities as BootstrapData['entities'],
|
|
145
|
+
diRegistrars: diModule.diRegistrars as BootstrapData['diRegistrars'],
|
|
146
|
+
entityIds: entityIdsModule.E as BootstrapData['entityIds'],
|
|
147
|
+
// Search configs are needed by workers for indexing
|
|
148
|
+
searchModuleConfigs: (searchModule.searchModuleConfigs ?? []) as BootstrapData['searchModuleConfigs'],
|
|
149
|
+
// Empty UI-related data - not needed for CLI
|
|
150
|
+
dashboardWidgetEntries: [],
|
|
151
|
+
injectionWidgetEntries: [],
|
|
152
|
+
injectionTables: [],
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create and execute bootstrap in CLI context.
|
|
158
|
+
*
|
|
159
|
+
* This is a convenience function that finds the app root, loads the generated
|
|
160
|
+
* data dynamically, and runs bootstrap. Use this in CLI entry points.
|
|
161
|
+
*
|
|
162
|
+
* Returns the loaded bootstrap data so the CLI can register modules directly
|
|
163
|
+
* (avoids module resolution issues when importing @open-mercato/cli/mercato).
|
|
164
|
+
*
|
|
165
|
+
* @param appRoot - Optional explicit app root path
|
|
166
|
+
* @returns The loaded bootstrap data (modules, entities, etc.)
|
|
167
|
+
*/
|
|
168
|
+
export async function bootstrapFromAppRoot(appRoot?: string): Promise<BootstrapData> {
|
|
169
|
+
const { createBootstrap, waitForAsyncRegistration } = await import('./factory.js')
|
|
170
|
+
const data = await loadBootstrapData(appRoot)
|
|
171
|
+
const bootstrap = createBootstrap(data)
|
|
172
|
+
bootstrap()
|
|
173
|
+
// In CLI context, wait for async registrations (UI widgets, search configs, etc.)
|
|
174
|
+
await waitForAsyncRegistration()
|
|
175
|
+
|
|
176
|
+
return data
|
|
177
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { BootstrapData, BootstrapOptions } from './types'
|
|
2
|
+
import { registerOrmEntities } from '../db/mikro'
|
|
3
|
+
import { registerDiRegistrars } from '../di/container'
|
|
4
|
+
import { registerModules } from '../modules/registry'
|
|
5
|
+
import { registerEntityIds } from '../encryption/entityIds'
|
|
6
|
+
import { registerEntityFields } from '../encryption/entityFields'
|
|
7
|
+
import { registerSearchModuleConfigs } from '../../modules/search'
|
|
8
|
+
|
|
9
|
+
let _bootstrapped = false
|
|
10
|
+
|
|
11
|
+
// Store the async registration promise so callers can await it if needed
|
|
12
|
+
let _asyncRegistrationPromise: Promise<void> | null = null
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a bootstrap function that registers all application dependencies.
|
|
16
|
+
*
|
|
17
|
+
* The returned function should be called once at application startup.
|
|
18
|
+
* In development mode, it can be called multiple times (for HMR).
|
|
19
|
+
*
|
|
20
|
+
* @param data - All generated registry data from .mercato/generated/
|
|
21
|
+
* @param options - Optional configuration
|
|
22
|
+
* @returns A bootstrap function to call at app startup
|
|
23
|
+
*/
|
|
24
|
+
export function createBootstrap(data: BootstrapData, options: BootstrapOptions = {}) {
|
|
25
|
+
return function bootstrap(): void {
|
|
26
|
+
// In development, always re-run registrations to handle HMR
|
|
27
|
+
// (Module state may be reset when Turbopack reloads packages)
|
|
28
|
+
if (_bootstrapped && process.env.NODE_ENV !== 'development') return
|
|
29
|
+
_bootstrapped = true
|
|
30
|
+
|
|
31
|
+
// === 1. Foundation: ORM entities and DI registrars ===
|
|
32
|
+
registerOrmEntities(data.entities)
|
|
33
|
+
registerDiRegistrars(data.diRegistrars.filter((r): r is NonNullable<typeof r> => r != null))
|
|
34
|
+
|
|
35
|
+
// === 2. Modules registry (required by i18n, query engine, dashboards, CLI) ===
|
|
36
|
+
registerModules(data.modules)
|
|
37
|
+
|
|
38
|
+
// === 3. Entity IDs (required by encryption, indexing, entity links) ===
|
|
39
|
+
registerEntityIds(data.entityIds)
|
|
40
|
+
|
|
41
|
+
// === 4. Entity fields registry (for encryption manager, Turbopack compatibility) ===
|
|
42
|
+
if (data.entityFieldsRegistry) {
|
|
43
|
+
registerEntityFields(data.entityFieldsRegistry)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// === 5. Search module configs (for search service registration in DI) ===
|
|
47
|
+
if (data.searchModuleConfigs) {
|
|
48
|
+
registerSearchModuleConfigs(data.searchModuleConfigs)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// === 6-7. UI Widgets and Optional packages (async to avoid circular deps) ===
|
|
52
|
+
// Store the promise so CLI context can await it
|
|
53
|
+
_asyncRegistrationPromise = registerWidgetsAndOptionalPackages(data, options)
|
|
54
|
+
void _asyncRegistrationPromise
|
|
55
|
+
|
|
56
|
+
options.onRegistrationComplete?.()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wait for async registrations (CLI modules, widgets, etc.) to complete.
|
|
62
|
+
* Call this after bootstrap() in CLI context where you need modules immediately.
|
|
63
|
+
*/
|
|
64
|
+
export async function waitForAsyncRegistration(): Promise<void> {
|
|
65
|
+
if (_asyncRegistrationPromise) {
|
|
66
|
+
await _asyncRegistrationPromise
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function registerWidgetsAndOptionalPackages(data: BootstrapData, options: BootstrapOptions): Promise<void> {
|
|
71
|
+
// Register UI widgets (dynamic imports to avoid circular deps with ui/core packages)
|
|
72
|
+
try {
|
|
73
|
+
const [dashboardRegistry, injectionRegistry, coreInjection] = await Promise.all([
|
|
74
|
+
import('@open-mercato/ui/backend/dashboard/widgetRegistry'),
|
|
75
|
+
import('@open-mercato/ui/backend/injection/widgetRegistry'),
|
|
76
|
+
import('@open-mercato/core/modules/widgets/lib/injection'),
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
dashboardRegistry.registerDashboardWidgets(data.dashboardWidgetEntries)
|
|
80
|
+
injectionRegistry.registerInjectionWidgets(data.injectionWidgetEntries)
|
|
81
|
+
coreInjection.registerCoreInjectionWidgets(data.injectionWidgetEntries)
|
|
82
|
+
coreInjection.registerCoreInjectionTables(data.injectionTables)
|
|
83
|
+
} catch {
|
|
84
|
+
// UI packages may not be available in all contexts
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Note: Search module configs are registered synchronously in the main bootstrap.
|
|
88
|
+
// The actual registerSearchModule() call happens in core/bootstrap.ts when the
|
|
89
|
+
// DI container is created, using getSearchModuleConfigs() from the global registry.
|
|
90
|
+
|
|
91
|
+
// Note: CLI module registration is handled separately in CLI context
|
|
92
|
+
// via bootstrapFromAppRoot in dynamicLoader. We don't import CLI here
|
|
93
|
+
// to avoid Turbopack tracing through the CLI package in Next.js context.
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if bootstrap has been called.
|
|
98
|
+
*/
|
|
99
|
+
export function isBootstrapped(): boolean {
|
|
100
|
+
return _bootstrapped
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reset bootstrap state. Useful for testing.
|
|
105
|
+
*/
|
|
106
|
+
export function resetBootstrapState(): void {
|
|
107
|
+
_bootstrapped = false
|
|
108
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap module for Open Mercato applications.
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for bootstrapping the application:
|
|
5
|
+
*
|
|
6
|
+
* - `createBootstrap(data)` - Factory to create a bootstrap function from generated data
|
|
7
|
+
* - `isBootstrapped()` - Check if bootstrap has been called
|
|
8
|
+
* - `resetBootstrapState()` - Reset bootstrap state (for testing)
|
|
9
|
+
* - `findAppRoot()` - Find the Next.js app root directory
|
|
10
|
+
*
|
|
11
|
+
* For CLI/dynamic contexts, import the dynamic loader directly:
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { bootstrapFromAppRoot } from '@open-mercato/shared/lib/bootstrap/dynamicLoader'
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export * from './types'
|
|
18
|
+
export { createBootstrap, isBootstrapped, resetBootstrapState } from './factory'
|
|
19
|
+
export { findAppRoot, findAllApps, type AppRoot } from './appResolver'
|
|
20
|
+
|
|
21
|
+
// Note: dynamicLoader is intentionally NOT exported from index
|
|
22
|
+
// It should be imported directly when needed to make it clear
|
|
23
|
+
// that it only works in unbundled contexts (CLI, tsx)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DiRegistrar } from '../di/container'
|
|
2
|
+
import type { EntityIds } from '../encryption/entityIds'
|
|
3
|
+
import type { EntityFieldsRegistry } from '../encryption/entityFields'
|
|
4
|
+
import type { Module, ModuleDashboardWidgetEntry, ModuleInjectionWidgetEntry } from '../../modules/registry'
|
|
5
|
+
import type { ModuleInjectionTable } from '../../modules/widgets/injection'
|
|
6
|
+
import type { SearchModuleConfig } from '../../modules/search'
|
|
7
|
+
import type { EntityClass, EntityClassGroup } from '@mikro-orm/core'
|
|
8
|
+
|
|
9
|
+
export type OrmEntity = EntityClass<unknown> | EntityClassGroup<unknown>
|
|
10
|
+
|
|
11
|
+
export interface InjectionTableEntry {
|
|
12
|
+
moduleId: string
|
|
13
|
+
table: ModuleInjectionTable
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface BootstrapData {
|
|
17
|
+
modules: Module[]
|
|
18
|
+
entities: OrmEntity[]
|
|
19
|
+
diRegistrars: (DiRegistrar | undefined)[]
|
|
20
|
+
entityIds: EntityIds
|
|
21
|
+
entityFieldsRegistry?: EntityFieldsRegistry
|
|
22
|
+
dashboardWidgetEntries: ModuleDashboardWidgetEntry[]
|
|
23
|
+
injectionWidgetEntries: ModuleInjectionWidgetEntry[]
|
|
24
|
+
injectionTables: InjectionTableEntry[]
|
|
25
|
+
searchModuleConfigs: SearchModuleConfig[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BootstrapOptions {
|
|
29
|
+
skipSearchConfigs?: boolean
|
|
30
|
+
onRegistrationComplete?: () => void
|
|
31
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CacheStrategy } from '@open-mercato/cache'
|
|
2
|
+
|
|
3
|
+
export type CacheSegmentAnalysisOptions = {
|
|
4
|
+
keysPattern: string
|
|
5
|
+
deriveSegment: (key: string) => string | null
|
|
6
|
+
filterKey?: (key: string) => boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type CacheSegmentInfo = {
|
|
10
|
+
segment: string
|
|
11
|
+
keys: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function analyzeCacheSegments(
|
|
15
|
+
cache: CacheStrategy,
|
|
16
|
+
options: CacheSegmentAnalysisOptions
|
|
17
|
+
): Promise<CacheSegmentInfo[]> {
|
|
18
|
+
const keys = await cache.keys(options.keysPattern)
|
|
19
|
+
const segments = new Map<string, Set<string>>()
|
|
20
|
+
|
|
21
|
+
for (const key of keys) {
|
|
22
|
+
if (options.filterKey && !options.filterKey(key)) continue
|
|
23
|
+
const segment = options.deriveSegment(key)
|
|
24
|
+
if (!segment) continue
|
|
25
|
+
if (!segments.has(segment)) segments.set(segment, new Set<string>())
|
|
26
|
+
segments.get(segment)!.add(key)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const results: CacheSegmentInfo[] = []
|
|
30
|
+
for (const [segment, keySet] of segments.entries()) {
|
|
31
|
+
results.push({
|
|
32
|
+
segment,
|
|
33
|
+
keys: Array.from(keySet).sort(),
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
results.sort((a, b) => a.segment.localeCompare(b.segment))
|
|
37
|
+
return results
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function purgeCacheSegment(
|
|
41
|
+
cache: CacheStrategy,
|
|
42
|
+
options: CacheSegmentAnalysisOptions,
|
|
43
|
+
segment: string
|
|
44
|
+
): Promise<{ deleted: number; keys: string[] }> {
|
|
45
|
+
const analyses = await analyzeCacheSegments(cache, options)
|
|
46
|
+
const target = analyses.find((entry) => entry.segment === segment)
|
|
47
|
+
if (!target) return { deleted: 0, keys: [] }
|
|
48
|
+
|
|
49
|
+
let deleted = 0
|
|
50
|
+
for (const key of target.keys) {
|
|
51
|
+
const removed = await cache.delete(key)
|
|
52
|
+
if (removed) deleted += 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { deleted, keys: target.keys }
|
|
56
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type ProgressBar = {
|
|
2
|
+
update(completed: number): void
|
|
3
|
+
complete(): void
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createProgressBar(label: string, total: number): ProgressBar {
|
|
7
|
+
const width = 28
|
|
8
|
+
let lastPercent = -1
|
|
9
|
+
let lastCompleted = -1
|
|
10
|
+
let finished = false
|
|
11
|
+
const startedAt = Date.now()
|
|
12
|
+
const minPercentStep =
|
|
13
|
+
total >= 1_000_000 ? 0.01 : total >= 100_000 ? 0.05 : total >= 10_000 ? 0.1 : 0.5
|
|
14
|
+
const minCompletedStep = Math.max(1, Math.floor(total / 1000))
|
|
15
|
+
|
|
16
|
+
const render = (completed: number) => {
|
|
17
|
+
if (total <= 0 || finished) return
|
|
18
|
+
const ratio = Math.min(1, Math.max(0, completed / total))
|
|
19
|
+
const percentRaw = ratio * 100
|
|
20
|
+
if (
|
|
21
|
+
completed < total &&
|
|
22
|
+
percentRaw - lastPercent < minPercentStep &&
|
|
23
|
+
completed - lastCompleted < minCompletedStep
|
|
24
|
+
) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
lastPercent = percentRaw
|
|
28
|
+
lastCompleted = completed
|
|
29
|
+
const filled = Math.round(ratio * width)
|
|
30
|
+
const bar = '#'.repeat(filled).padEnd(width, '-')
|
|
31
|
+
const percentLabel =
|
|
32
|
+
(percentRaw >= 10 ? percentRaw.toFixed(1) : percentRaw.toFixed(2)).padStart(6, ' ')
|
|
33
|
+
const countsLabel = `${completed.toLocaleString()}/${total.toLocaleString()}`
|
|
34
|
+
const elapsedMs = Math.max(1, Date.now() - startedAt)
|
|
35
|
+
const recordsPerSecond = completed > 0 ? (completed / elapsedMs) * 1000 : 0
|
|
36
|
+
const rateLabel = `${recordsPerSecond.toFixed(1)} r/s`
|
|
37
|
+
process.stdout.write(`\r${label} [${bar}] ${percentLabel}% (${countsLabel}) ${rateLabel}`)
|
|
38
|
+
if (completed >= total) {
|
|
39
|
+
finished = true
|
|
40
|
+
process.stdout.write('\n')
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
update: render,
|
|
46
|
+
complete() {
|
|
47
|
+
if (!finished && total > 0) {
|
|
48
|
+
render(total)
|
|
49
|
+
} else if (!finished) {
|
|
50
|
+
finished = true
|
|
51
|
+
process.stdout.write('\n')
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createContainer, asValue, InjectionMode } from 'awilix'
|
|
2
|
+
import { unregisterCommand, registerCommand, CommandBus } from '@open-mercato/shared/lib/commands'
|
|
3
|
+
|
|
4
|
+
describe('CommandBus', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
unregisterCommand('test.command')
|
|
7
|
+
unregisterCommand('test.command.with-capture')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('executes registered command and logs action metadata', async () => {
|
|
11
|
+
const logMock = jest.fn(async () => ({ id: 'log-entry' }))
|
|
12
|
+
registerCommand({
|
|
13
|
+
id: 'test.command',
|
|
14
|
+
execute: jest.fn(async () => ({ ok: true })),
|
|
15
|
+
buildLog: jest.fn(() => ({ actionLabel: 'Test', resourceKind: 'test', resourceId: '123' })),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
|
|
19
|
+
container.register({ actionLogService: asValue({ log: logMock }) })
|
|
20
|
+
|
|
21
|
+
const bus = new CommandBus()
|
|
22
|
+
const ctx = {
|
|
23
|
+
container,
|
|
24
|
+
auth: { sub: 'user-1', tenantId: 'tenant-1', orgId: null },
|
|
25
|
+
organizationScope: null,
|
|
26
|
+
selectedOrganizationId: null,
|
|
27
|
+
organizationIds: null,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { result, logEntry } = await bus.execute('test.command', { input: {}, ctx })
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual({ ok: true })
|
|
33
|
+
expect(logMock).toHaveBeenCalledWith(
|
|
34
|
+
expect.objectContaining({
|
|
35
|
+
commandId: 'test.command',
|
|
36
|
+
tenantId: 'tenant-1',
|
|
37
|
+
actorUserId: 'user-1',
|
|
38
|
+
resourceId: '123',
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
expect(logEntry).toEqual({ id: 'log-entry' })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('passes captureAfter snapshot to buildLog as snapshots.after', async () => {
|
|
45
|
+
const logMock = jest.fn(async () => ({ id: 'log-entry-2' }))
|
|
46
|
+
const buildLogMock = jest.fn(() => ({
|
|
47
|
+
actionLabel: 'Test with capture',
|
|
48
|
+
resourceKind: 'test',
|
|
49
|
+
resourceId: '456',
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
registerCommand({
|
|
53
|
+
id: 'test.command.with-capture',
|
|
54
|
+
prepare: jest.fn(async () => ({ before: { state: 'before-snapshot' } })),
|
|
55
|
+
execute: jest.fn(async () => ({ id: 'result-123' })),
|
|
56
|
+
captureAfter: jest.fn(async (_input, result) => ({ state: 'after-snapshot', resultId: result.id })),
|
|
57
|
+
buildLog: buildLogMock,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
|
|
61
|
+
container.register({ actionLogService: asValue({ log: logMock }) })
|
|
62
|
+
|
|
63
|
+
const bus = new CommandBus()
|
|
64
|
+
const ctx = {
|
|
65
|
+
container,
|
|
66
|
+
auth: { sub: 'user-2', tenantId: 'tenant-2', orgId: null },
|
|
67
|
+
organizationScope: null,
|
|
68
|
+
selectedOrganizationId: null,
|
|
69
|
+
organizationIds: null,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await bus.execute('test.command.with-capture', { input: { foo: 'bar' }, ctx })
|
|
73
|
+
|
|
74
|
+
// Verify buildLog received both before and after snapshots
|
|
75
|
+
expect(buildLogMock).toHaveBeenCalledWith(
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
snapshots: {
|
|
78
|
+
before: { state: 'before-snapshot' },
|
|
79
|
+
after: { state: 'after-snapshot', resultId: 'result-123' },
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { buildChanges, requireTenantScope, requireId } from '@open-mercato/shared/lib/commands/helpers'
|
|
2
|
+
|
|
3
|
+
describe('command helpers', () => {
|
|
4
|
+
describe('buildChanges', () => {
|
|
5
|
+
it('returns diff for changed keys', () => {
|
|
6
|
+
const diff = buildChanges({ a: 1, b: 2 }, { a: 1, b: 3, c: 4 }, ['a', 'b'])
|
|
7
|
+
expect(diff).toEqual({ b: { from: 2, to: 3 } })
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('handles missing before snapshot', () => {
|
|
11
|
+
expect(buildChanges(null, { a: 1 }, ['a'])).toEqual({})
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('requireTenantScope', () => {
|
|
16
|
+
it('prefers requested when allowed', () => {
|
|
17
|
+
expect(requireTenantScope('tenant-1', 'tenant-1')).toBe('tenant-1')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('throws when requested mismatches auth tenant', () => {
|
|
21
|
+
expect(() => requireTenantScope('tenant-1', 'tenant-2')).toThrow('Forbidden')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('throws when tenant missing', () => {
|
|
25
|
+
expect(() => requireTenantScope(null, null)).toThrow('Tenant scope required')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('requireId', () => {
|
|
30
|
+
it('returns string id directly', () => {
|
|
31
|
+
expect(requireId('123')).toBe('123')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('extracts from object tokens', () => {
|
|
35
|
+
expect(requireId({ body: { id: 'abc' } })).toBe('abc')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('throws when missing', () => {
|
|
39
|
+
expect(() => requireId(null)).toThrow('ID is required')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
})
|