@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,349 @@
|
|
|
1
|
+
import type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'
|
|
2
|
+
import type { ActionLogCreateInput } from '@open-mercato/core/modules/audit_logs/data/validators'
|
|
3
|
+
import { commandRegistry } from './registry'
|
|
4
|
+
import type {
|
|
5
|
+
CommandExecutionOptions,
|
|
6
|
+
CommandExecuteResult,
|
|
7
|
+
CommandHandler,
|
|
8
|
+
CommandLogBuilderArgs,
|
|
9
|
+
CommandLogMetadata,
|
|
10
|
+
CommandRuntimeContext,
|
|
11
|
+
} from './types'
|
|
12
|
+
import { defaultUndoToken } from './types'
|
|
13
|
+
import type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'
|
|
14
|
+
import type { AwilixContainer } from 'awilix'
|
|
15
|
+
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
16
|
+
import {
|
|
17
|
+
canonicalizeResourceTag,
|
|
18
|
+
deriveResourceFromCommandId,
|
|
19
|
+
invalidateCrudCache,
|
|
20
|
+
pickFirstIdentifier,
|
|
21
|
+
isCrudCacheDebugEnabled,
|
|
22
|
+
} from '@open-mercato/shared/lib/crud/cache'
|
|
23
|
+
|
|
24
|
+
const SKIPPED_ACTION_LOG_RESOURCE_KINDS = new Set<string>([
|
|
25
|
+
'audit_logs.access',
|
|
26
|
+
'audit_logs.action',
|
|
27
|
+
'dashboards.layout',
|
|
28
|
+
'dashboards.user_widgets',
|
|
29
|
+
'dashboards.role_widgets',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
function asRecord(input: unknown): Record<string, unknown> | null {
|
|
33
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null
|
|
34
|
+
return input as Record<string, unknown>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function extractAliasList(source: unknown): string[] {
|
|
38
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) return []
|
|
39
|
+
const record = source as Record<string, unknown>
|
|
40
|
+
const raw = record.cacheAliases
|
|
41
|
+
if (!Array.isArray(raw)) return []
|
|
42
|
+
const aliases = new Set<string>()
|
|
43
|
+
for (const value of raw) {
|
|
44
|
+
if (typeof value !== 'string') continue
|
|
45
|
+
const normalized = canonicalizeResourceTag(value)
|
|
46
|
+
if (normalized) aliases.add(normalized)
|
|
47
|
+
}
|
|
48
|
+
return Array.from(aliases)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class CommandBus {
|
|
52
|
+
async execute<TInput = unknown, TResult = unknown>(
|
|
53
|
+
commandId: string,
|
|
54
|
+
options: CommandExecutionOptions<TInput>
|
|
55
|
+
): Promise<CommandExecuteResult<TResult>> {
|
|
56
|
+
const handler = this.resolveHandler<TInput, TResult>(commandId)
|
|
57
|
+
const snapshots = await this.prepareSnapshots(handler, options)
|
|
58
|
+
const result = await handler.execute(options.input, options.ctx)
|
|
59
|
+
const afterSnapshot = await this.captureAfter(handler, options, result)
|
|
60
|
+
const snapshotsWithAfter = { ...snapshots, after: afterSnapshot }
|
|
61
|
+
const logMeta = await this.buildLog(handler, options, result, snapshotsWithAfter)
|
|
62
|
+
let mergedMeta = this.mergeMetadata(options.metadata, logMeta)
|
|
63
|
+
const undoable = this.isUndoable(handler)
|
|
64
|
+
if (undoable) {
|
|
65
|
+
mergedMeta = mergedMeta ?? {}
|
|
66
|
+
if (!mergedMeta.undoToken) mergedMeta.undoToken = defaultUndoToken()
|
|
67
|
+
if (mergedMeta.actorUserId === undefined) mergedMeta.actorUserId = options.ctx.auth?.sub ?? null
|
|
68
|
+
}
|
|
69
|
+
if (afterSnapshot !== undefined && afterSnapshot !== null) {
|
|
70
|
+
if (!mergedMeta) {
|
|
71
|
+
mergedMeta = { snapshotAfter: afterSnapshot }
|
|
72
|
+
} else if (!mergedMeta.snapshotAfter) {
|
|
73
|
+
mergedMeta.snapshotAfter = afterSnapshot
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (snapshots.before) {
|
|
77
|
+
if (!mergedMeta) {
|
|
78
|
+
mergedMeta = { snapshotBefore: snapshots.before }
|
|
79
|
+
} else if (!mergedMeta.snapshotBefore) {
|
|
80
|
+
mergedMeta.snapshotBefore = snapshots.before
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const logEntry = await this.persistLog(commandId, options, mergedMeta)
|
|
84
|
+
await this.invalidateCacheAfterExecute(commandId, options, result, mergedMeta)
|
|
85
|
+
await this.flushCrudSideEffects(options.ctx.container)
|
|
86
|
+
return { result, logEntry }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async undo(undoToken: string, ctx: CommandRuntimeContext): Promise<void> {
|
|
90
|
+
const service = (ctx.container.resolve('actionLogService') as ActionLogService)
|
|
91
|
+
const log = await service.findByUndoToken(undoToken)
|
|
92
|
+
if (!log) throw new Error('Undo token expired or not found')
|
|
93
|
+
const handler = this.resolveHandler(log.commandId)
|
|
94
|
+
if (!handler.undo || this.isUndoable(handler) === false) {
|
|
95
|
+
throw new Error(`Command ${log.commandId} is not undoable`)
|
|
96
|
+
}
|
|
97
|
+
await handler.undo({
|
|
98
|
+
input: log.commandPayload as Parameters<NonNullable<typeof handler.undo>>[0]['input'],
|
|
99
|
+
ctx,
|
|
100
|
+
logEntry: log,
|
|
101
|
+
})
|
|
102
|
+
await service.markUndone(log.id)
|
|
103
|
+
await this.invalidateCacheAfterUndo(log, ctx)
|
|
104
|
+
await this.flushCrudSideEffects(ctx.container)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private resolveHandler<TInput, TResult>(commandId: string): CommandHandler<TInput, TResult> {
|
|
108
|
+
const handler = commandRegistry.get<TInput, TResult>(commandId)
|
|
109
|
+
if (!handler) throw new Error(`Command handler not registered for id ${commandId}`)
|
|
110
|
+
return handler
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async prepareSnapshots<TInput, TResult>(
|
|
114
|
+
handler: CommandHandler<TInput, TResult>,
|
|
115
|
+
options: CommandExecutionOptions<TInput>
|
|
116
|
+
): Promise<{ before?: unknown }> {
|
|
117
|
+
if (!handler.prepare) return {}
|
|
118
|
+
try {
|
|
119
|
+
return (await handler.prepare(options.input, options.ctx)) || {}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
throw err
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async captureAfter<TInput, TResult>(
|
|
126
|
+
handler: CommandHandler<TInput, TResult>,
|
|
127
|
+
options: CommandExecutionOptions<TInput>,
|
|
128
|
+
result: TResult
|
|
129
|
+
): Promise<unknown> {
|
|
130
|
+
if (!handler.captureAfter) return undefined
|
|
131
|
+
return handler.captureAfter(options.input, result, options.ctx)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async buildLog<TInput, TResult>(
|
|
135
|
+
handler: CommandHandler<TInput, TResult>,
|
|
136
|
+
options: CommandExecutionOptions<TInput>,
|
|
137
|
+
result: TResult,
|
|
138
|
+
snapshots: { before?: unknown; after?: unknown }
|
|
139
|
+
): Promise<CommandLogMetadata | null> {
|
|
140
|
+
if (!handler.buildLog) return null
|
|
141
|
+
const args: CommandLogBuilderArgs<TInput, TResult> = {
|
|
142
|
+
input: options.input,
|
|
143
|
+
result,
|
|
144
|
+
ctx: options.ctx,
|
|
145
|
+
snapshots,
|
|
146
|
+
}
|
|
147
|
+
return (await handler.buildLog(args)) || null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private mergeMetadata(primary?: CommandLogMetadata | null, secondary?: CommandLogMetadata | null): CommandLogMetadata | null {
|
|
151
|
+
if (!primary && !secondary) return null
|
|
152
|
+
return {
|
|
153
|
+
tenantId: primary?.tenantId ?? secondary?.tenantId ?? null,
|
|
154
|
+
organizationId: primary?.organizationId ?? secondary?.organizationId ?? null,
|
|
155
|
+
actorUserId: primary?.actorUserId ?? secondary?.actorUserId ?? null,
|
|
156
|
+
actionLabel: primary?.actionLabel ?? secondary?.actionLabel ?? null,
|
|
157
|
+
resourceKind: primary?.resourceKind ?? secondary?.resourceKind ?? null,
|
|
158
|
+
resourceId: primary?.resourceId ?? secondary?.resourceId ?? null,
|
|
159
|
+
undoToken: primary?.undoToken ?? secondary?.undoToken ?? null,
|
|
160
|
+
payload: primary?.payload ?? secondary?.payload ?? null,
|
|
161
|
+
snapshotBefore: primary?.snapshotBefore ?? secondary?.snapshotBefore ?? null,
|
|
162
|
+
snapshotAfter: primary?.snapshotAfter ?? secondary?.snapshotAfter ?? null,
|
|
163
|
+
changes: primary?.changes ?? secondary?.changes ?? null,
|
|
164
|
+
context: primary?.context ?? secondary?.context ?? null,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async persistLog<TInput>(
|
|
169
|
+
commandId: string,
|
|
170
|
+
options: CommandExecutionOptions<TInput>,
|
|
171
|
+
metadata: CommandLogMetadata | null
|
|
172
|
+
): Promise<ActionLog | null> {
|
|
173
|
+
if (!metadata) return null
|
|
174
|
+
const resourceKind =
|
|
175
|
+
typeof metadata.resourceKind === 'string' ? metadata.resourceKind : null
|
|
176
|
+
if (resourceKind && SKIPPED_ACTION_LOG_RESOURCE_KINDS.has(resourceKind)) {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
let service: ActionLogService | null = null
|
|
180
|
+
try {
|
|
181
|
+
service = (options.ctx.container.resolve('actionLogService') as ActionLogService)
|
|
182
|
+
} catch {
|
|
183
|
+
service = null
|
|
184
|
+
}
|
|
185
|
+
if (!service) return null
|
|
186
|
+
|
|
187
|
+
const tenantId = metadata.tenantId ?? options.ctx.auth?.tenantId ?? null
|
|
188
|
+
const organizationId =
|
|
189
|
+
metadata.organizationId ?? options.ctx.selectedOrganizationId ?? options.ctx.auth?.orgId ?? null
|
|
190
|
+
const actorUserId = metadata.actorUserId ?? options.ctx.auth?.sub ?? null
|
|
191
|
+
const payload: Record<string, unknown> = {
|
|
192
|
+
tenantId: tenantId ?? undefined,
|
|
193
|
+
organizationId: organizationId ?? undefined,
|
|
194
|
+
actorUserId: actorUserId ?? undefined,
|
|
195
|
+
commandId,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (metadata) {
|
|
199
|
+
if ('actionLabel' in metadata && metadata.actionLabel != null) payload.actionLabel = metadata.actionLabel
|
|
200
|
+
if ('resourceKind' in metadata && metadata.resourceKind != null) payload.resourceKind = metadata.resourceKind
|
|
201
|
+
if ('resourceId' in metadata && metadata.resourceId != null) payload.resourceId = metadata.resourceId
|
|
202
|
+
if ('undoToken' in metadata && metadata.undoToken != null) payload.undoToken = metadata.undoToken
|
|
203
|
+
if ('payload' in metadata && metadata.payload !== undefined) payload.commandPayload = metadata.payload
|
|
204
|
+
if ('snapshotBefore' in metadata && metadata.snapshotBefore !== undefined) payload.snapshotBefore = metadata.snapshotBefore
|
|
205
|
+
if ('snapshotAfter' in metadata && metadata.snapshotAfter !== undefined) payload.snapshotAfter = metadata.snapshotAfter
|
|
206
|
+
if ('changes' in metadata && metadata.changes !== undefined && metadata.changes !== null) payload.changes = metadata.changes
|
|
207
|
+
if ('context' in metadata && metadata.context !== undefined && metadata.context !== null) payload.context = metadata.context
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const redoEnvelope = wrapRedoPayload('commandPayload' in payload ? (payload.commandPayload as unknown) : undefined, options.input)
|
|
211
|
+
payload.commandPayload = redoEnvelope
|
|
212
|
+
|
|
213
|
+
return await service.log(payload as ActionLogCreateInput)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private isUndoable(handler: CommandHandler<unknown, unknown>): boolean {
|
|
217
|
+
return handler.isUndoable !== false && typeof handler.undo === 'function'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private async invalidateCacheAfterExecute<TResult>(
|
|
221
|
+
commandId: string,
|
|
222
|
+
options: CommandExecutionOptions<unknown>,
|
|
223
|
+
result: TResult,
|
|
224
|
+
metadata: CommandLogMetadata | null
|
|
225
|
+
): Promise<void> {
|
|
226
|
+
const resource = typeof metadata?.resourceKind === 'string' ? metadata.resourceKind : null
|
|
227
|
+
if (!resource) return
|
|
228
|
+
try {
|
|
229
|
+
const ctx = options.ctx
|
|
230
|
+
const resultRecord = asRecord(result)
|
|
231
|
+
const resultEntity = asRecord(resultRecord?.entity)
|
|
232
|
+
const inputRecord = asRecord(options.input)
|
|
233
|
+
const inputEntity = asRecord(inputRecord?.entity)
|
|
234
|
+
|
|
235
|
+
const recordId = pickFirstIdentifier(
|
|
236
|
+
metadata?.resourceId,
|
|
237
|
+
resultRecord?.entityId,
|
|
238
|
+
resultRecord?.id,
|
|
239
|
+
resultRecord?.recordId,
|
|
240
|
+
resultEntity?.id,
|
|
241
|
+
inputRecord?.id,
|
|
242
|
+
inputRecord?.entityId,
|
|
243
|
+
inputRecord?.recordId,
|
|
244
|
+
inputEntity?.id
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const organizationId = pickFirstIdentifier(
|
|
248
|
+
metadata?.organizationId,
|
|
249
|
+
resultRecord?.organizationId,
|
|
250
|
+
resultEntity?.organizationId,
|
|
251
|
+
inputRecord?.organizationId,
|
|
252
|
+
inputEntity?.organizationId,
|
|
253
|
+
ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const tenantId = pickFirstIdentifier(
|
|
257
|
+
metadata?.tenantId,
|
|
258
|
+
resultRecord?.tenantId,
|
|
259
|
+
resultEntity?.tenantId,
|
|
260
|
+
inputRecord?.tenantId,
|
|
261
|
+
inputEntity?.tenantId,
|
|
262
|
+
ctx.auth?.tenantId ?? null
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const fallbackTenant = pickFirstIdentifier(metadata?.tenantId, ctx.auth?.tenantId ?? null)
|
|
266
|
+
|
|
267
|
+
const aliasSet = new Set<string>()
|
|
268
|
+
for (const alias of extractAliasList(metadata?.context ?? null)) {
|
|
269
|
+
aliasSet.add(alias)
|
|
270
|
+
}
|
|
271
|
+
const derived = deriveResourceFromCommandId(commandId)
|
|
272
|
+
if (derived) aliasSet.add(derived)
|
|
273
|
+
const aliasExtras = Array.from(aliasSet)
|
|
274
|
+
await invalidateCrudCache(
|
|
275
|
+
ctx.container,
|
|
276
|
+
resource,
|
|
277
|
+
{ id: recordId, organizationId, tenantId },
|
|
278
|
+
fallbackTenant,
|
|
279
|
+
`command:${commandId}:execute`,
|
|
280
|
+
aliasExtras
|
|
281
|
+
)
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (isCrudCacheDebugEnabled()) {
|
|
284
|
+
try {
|
|
285
|
+
console.debug('[crud][cache] execute-invalidation failed', { commandId, err })
|
|
286
|
+
} catch {}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private async invalidateCacheAfterUndo(log: ActionLog, ctx: CommandRuntimeContext): Promise<void> {
|
|
292
|
+
const resource = typeof log.resourceKind === 'string' ? log.resourceKind : null
|
|
293
|
+
if (!resource) return
|
|
294
|
+
try {
|
|
295
|
+
const recordId = pickFirstIdentifier(log.resourceId)
|
|
296
|
+
const organizationId = pickFirstIdentifier(log.organizationId, ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null)
|
|
297
|
+
const tenantId = pickFirstIdentifier(log.tenantId, ctx.auth?.tenantId ?? null)
|
|
298
|
+
const fallbackTenant = pickFirstIdentifier(log.tenantId, ctx.auth?.tenantId ?? null)
|
|
299
|
+
const aliasSet = new Set<string>()
|
|
300
|
+
for (const alias of extractAliasList(log.contextJson ?? null)) {
|
|
301
|
+
aliasSet.add(alias)
|
|
302
|
+
}
|
|
303
|
+
const derived = deriveResourceFromCommandId(log.commandId)
|
|
304
|
+
if (derived) aliasSet.add(derived)
|
|
305
|
+
const aliasExtras = Array.from(aliasSet)
|
|
306
|
+
await invalidateCrudCache(
|
|
307
|
+
ctx.container,
|
|
308
|
+
resource,
|
|
309
|
+
{ id: recordId, organizationId, tenantId },
|
|
310
|
+
fallbackTenant,
|
|
311
|
+
`command:${log.commandId}:undo`,
|
|
312
|
+
aliasExtras
|
|
313
|
+
)
|
|
314
|
+
} catch (err) {
|
|
315
|
+
if (isCrudCacheDebugEnabled()) {
|
|
316
|
+
try {
|
|
317
|
+
console.debug('[crud][cache] undo-invalidation failed', { commandId: log.commandId, err })
|
|
318
|
+
} catch {}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async flushCrudSideEffects(container: AwilixContainer): Promise<void> {
|
|
324
|
+
try {
|
|
325
|
+
const dataEngine = (container.resolve('dataEngine') as DataEngine)
|
|
326
|
+
await dataEngine.flushOrmEntityChanges()
|
|
327
|
+
} catch {
|
|
328
|
+
// best-effort: failures should not block command execution
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
type RedoEnvelope = {
|
|
334
|
+
__redoInput: unknown
|
|
335
|
+
[key: string]: unknown
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function wrapRedoPayload(existing: unknown, input: unknown): RedoEnvelope {
|
|
339
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
340
|
+
const envelope: RedoEnvelope = { __redoInput: input }
|
|
341
|
+
if (existing !== undefined) envelope.value = existing
|
|
342
|
+
return envelope
|
|
343
|
+
}
|
|
344
|
+
const current = existing as Record<string, unknown>
|
|
345
|
+
if ('__redoInput' in current && current.__redoInput !== undefined) {
|
|
346
|
+
return current as RedoEnvelope
|
|
347
|
+
}
|
|
348
|
+
return { __redoInput: input, ...current }
|
|
349
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
3
|
+
|
|
4
|
+
export type CustomFieldSnapshot = Record<string, unknown>
|
|
5
|
+
|
|
6
|
+
type LoadSnapshotOptions = {
|
|
7
|
+
entityId: string
|
|
8
|
+
recordId: string
|
|
9
|
+
tenantId?: string | null
|
|
10
|
+
organizationId?: string | null
|
|
11
|
+
tenantFallbacks?: Array<string | null | undefined>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function loadCustomFieldSnapshot(
|
|
15
|
+
em: EntityManager,
|
|
16
|
+
{ entityId, recordId, tenantId, organizationId, tenantFallbacks }: LoadSnapshotOptions
|
|
17
|
+
): Promise<CustomFieldSnapshot> {
|
|
18
|
+
const tenant = tenantId ?? null
|
|
19
|
+
const organization = organizationId ?? undefined
|
|
20
|
+
const records = await loadCustomFieldValues({
|
|
21
|
+
em,
|
|
22
|
+
entityId: entityId as any,
|
|
23
|
+
recordIds: [recordId],
|
|
24
|
+
tenantIdByRecord: { [recordId]: tenant },
|
|
25
|
+
organizationIdByRecord: organization === undefined ? undefined : { [recordId]: organization ?? null },
|
|
26
|
+
tenantFallbacks: tenantFallbacks ?? [tenant],
|
|
27
|
+
})
|
|
28
|
+
const raw = records[recordId] ?? {}
|
|
29
|
+
const custom: Record<string, unknown> = {}
|
|
30
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
31
|
+
if (key.startsWith('cf_')) custom[key.slice(3)] = value
|
|
32
|
+
}
|
|
33
|
+
return custom
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildCustomFieldResetMap(
|
|
37
|
+
before: CustomFieldSnapshot | undefined,
|
|
38
|
+
after: CustomFieldSnapshot | undefined
|
|
39
|
+
): Record<string, unknown> {
|
|
40
|
+
const values: Record<string, unknown> = {}
|
|
41
|
+
const keys = new Set<string>()
|
|
42
|
+
if (before) for (const key of Object.keys(before)) keys.add(key)
|
|
43
|
+
if (after) for (const key of Object.keys(after)) keys.add(key)
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
const hasBefore = Boolean(before && Object.prototype.hasOwnProperty.call(before, key))
|
|
46
|
+
if (hasBefore) {
|
|
47
|
+
const beforeValue = before?.[key]
|
|
48
|
+
if (beforeValue === null && Array.isArray(after?.[key])) {
|
|
49
|
+
values[key] = []
|
|
50
|
+
} else {
|
|
51
|
+
values[key] = beforeValue
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
values[key] = Array.isArray(after?.[key]) ? [] : null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return values
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type CustomFieldChangeSet = Record<string, { from: unknown; to: unknown }>
|
|
61
|
+
|
|
62
|
+
export function diffCustomFieldChanges(
|
|
63
|
+
before: CustomFieldSnapshot | undefined,
|
|
64
|
+
after: CustomFieldSnapshot | undefined
|
|
65
|
+
): CustomFieldChangeSet {
|
|
66
|
+
const out: CustomFieldChangeSet = {}
|
|
67
|
+
const keys = new Set<string>()
|
|
68
|
+
if (before) for (const key of Object.keys(before)) keys.add(key)
|
|
69
|
+
if (after) for (const key of Object.keys(after)) keys.add(key)
|
|
70
|
+
for (const key of keys) {
|
|
71
|
+
const prev = before ? before[key] : undefined
|
|
72
|
+
const next = after ? after[key] : undefined
|
|
73
|
+
if (!customFieldValuesEqual(prev, next)) {
|
|
74
|
+
out[key] = { from: prev ?? null, to: next ?? null }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function customFieldValuesEqual(a: unknown, b: unknown): boolean {
|
|
81
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
82
|
+
if (a.length !== b.length) return false
|
|
83
|
+
return a.every((value, idx) => customFieldValuesEqual(value, b[idx]))
|
|
84
|
+
}
|
|
85
|
+
return a === b
|
|
86
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'
|
|
2
|
+
import type { z } from 'zod'
|
|
3
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
4
|
+
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
5
|
+
import { normalizeCustomFieldValues } from '../custom-fields/normalize'
|
|
6
|
+
export { normalizeCustomFieldValues } from '../custom-fields/normalize'
|
|
7
|
+
import type { CrudEventsConfig, CrudIndexerConfig, CrudEmitContext } from '@open-mercato/shared/lib/crud/types'
|
|
8
|
+
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
9
|
+
import type { CommandLogMetadata } from '@open-mercato/shared/lib/commands'
|
|
10
|
+
|
|
11
|
+
export type ParsedPayload<TSchema extends z.ZodTypeAny> = {
|
|
12
|
+
parsed: z.infer<TSchema>
|
|
13
|
+
custom: Record<string, unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseWithCustomFields<TSchema extends z.ZodTypeAny>(
|
|
17
|
+
schema: TSchema,
|
|
18
|
+
raw: unknown
|
|
19
|
+
): ParsedPayload<TSchema> {
|
|
20
|
+
const { base, custom } = splitCustomFieldPayload(raw)
|
|
21
|
+
const parsed = schema.parse(base)
|
|
22
|
+
return { parsed, custom }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function setCustomFieldsIfAny(opts: {
|
|
26
|
+
dataEngine: DataEngine
|
|
27
|
+
entityId: string
|
|
28
|
+
recordId: string
|
|
29
|
+
tenantId: string | null
|
|
30
|
+
organizationId: string | null
|
|
31
|
+
values: Record<string, unknown>
|
|
32
|
+
notify?: boolean
|
|
33
|
+
}) {
|
|
34
|
+
const { values } = opts
|
|
35
|
+
if (!values || !Object.keys(values).length) return
|
|
36
|
+
const { dataEngine, entityId, recordId, tenantId, organizationId, notify = false } = opts
|
|
37
|
+
const normalized = normalizeCustomFieldValues(values)
|
|
38
|
+
await dataEngine.setCustomFields({
|
|
39
|
+
entityId,
|
|
40
|
+
recordId,
|
|
41
|
+
tenantId,
|
|
42
|
+
organizationId,
|
|
43
|
+
values: normalized,
|
|
44
|
+
notify,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function emitCrudSideEffects<TEntity>(opts: {
|
|
49
|
+
dataEngine: DataEngine
|
|
50
|
+
action: 'created' | 'updated' | 'deleted'
|
|
51
|
+
entity: TEntity
|
|
52
|
+
identifiers: CrudEmitContext<TEntity>['identifiers']
|
|
53
|
+
events?: CrudEventsConfig<TEntity>
|
|
54
|
+
indexer?: CrudIndexerConfig<TEntity>
|
|
55
|
+
}) {
|
|
56
|
+
const { dataEngine, action, entity, identifiers, events, indexer } = opts
|
|
57
|
+
dataEngine.markOrmEntityChange({
|
|
58
|
+
action,
|
|
59
|
+
entity,
|
|
60
|
+
identifiers,
|
|
61
|
+
events,
|
|
62
|
+
indexer,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function emitCrudUndoSideEffects<TEntity>(opts: {
|
|
67
|
+
dataEngine: DataEngine
|
|
68
|
+
action: 'created' | 'updated' | 'deleted'
|
|
69
|
+
entity: TEntity | null | undefined
|
|
70
|
+
identifiers: CrudEmitContext<TEntity>['identifiers']
|
|
71
|
+
events?: CrudEventsConfig<TEntity>
|
|
72
|
+
indexer?: CrudIndexerConfig<TEntity>
|
|
73
|
+
}) {
|
|
74
|
+
const { dataEngine, action, entity, identifiers, events, indexer } = opts
|
|
75
|
+
if (!entity) return
|
|
76
|
+
dataEngine.markOrmEntityChange({
|
|
77
|
+
action,
|
|
78
|
+
entity,
|
|
79
|
+
identifiers,
|
|
80
|
+
events,
|
|
81
|
+
indexer,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function flushCrudSideEffects(dataEngine: DataEngine): Promise<void> {
|
|
86
|
+
await dataEngine.flushOrmEntityChanges()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildChanges(
|
|
90
|
+
before: Record<string, unknown> | null | undefined,
|
|
91
|
+
after: Record<string, unknown>,
|
|
92
|
+
keys: readonly string[]
|
|
93
|
+
): Record<string, { from: unknown; to: unknown }> {
|
|
94
|
+
if (!before) return {}
|
|
95
|
+
const diff: Record<string, { from: unknown; to: unknown }> = {}
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const prev = before[key]
|
|
98
|
+
const next = after[key]
|
|
99
|
+
if (prev !== next) diff[key] = { from: prev, to: next }
|
|
100
|
+
}
|
|
101
|
+
return diff
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function requireTenantScope(authTenantId: string | null, requested?: string | null): string {
|
|
105
|
+
if (authTenantId && requested && requested !== authTenantId) {
|
|
106
|
+
throw new CrudHttpError(403, { error: 'Forbidden' })
|
|
107
|
+
}
|
|
108
|
+
const tenantId = requested || authTenantId
|
|
109
|
+
if (!tenantId) throw new CrudHttpError(400, { error: 'Tenant scope required' })
|
|
110
|
+
return tenantId
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function requireId(value: unknown, message = 'ID is required'): string {
|
|
114
|
+
if (typeof value === 'string' && value.trim()) return value
|
|
115
|
+
if (typeof value === 'number' || typeof value === 'bigint') return String(value)
|
|
116
|
+
if (value && typeof value === 'object') {
|
|
117
|
+
const source = value as Record<string, unknown>
|
|
118
|
+
const candidates: unknown[] = [
|
|
119
|
+
source.id,
|
|
120
|
+
source.recordId,
|
|
121
|
+
isRecord(source.body) ? source.body.id : undefined,
|
|
122
|
+
isRecord(source.query) ? source.query.id : undefined,
|
|
123
|
+
]
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
if (typeof candidate === 'string' && candidate.trim()) return candidate
|
|
126
|
+
if (typeof candidate === 'number' || typeof candidate === 'bigint') return String(candidate)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw new CrudHttpError(400, { error: message })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isRecord(input: unknown): input is { [key: string]: unknown } {
|
|
133
|
+
return !!input && typeof input === 'object'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type LogBuilderArgs<TInput, TResult> = {
|
|
137
|
+
input: TInput
|
|
138
|
+
result: TResult
|
|
139
|
+
ctx: CommandRuntimeContext
|
|
140
|
+
snapshots: { before?: unknown; after?: unknown }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type LogBuilder<TInput, TResult> = (args: LogBuilderArgs<TInput, TResult>) => CommandLogMetadata | null | Promise<CommandLogMetadata | null>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type OperationMetadataPayload = {
|
|
2
|
+
id: string
|
|
3
|
+
undoToken: string
|
|
4
|
+
commandId: string
|
|
5
|
+
actionLabel: string | null
|
|
6
|
+
resourceKind: string | null
|
|
7
|
+
resourceId: string | null
|
|
8
|
+
executedAt: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const HEADER_PREFIX = 'omop:'
|
|
12
|
+
|
|
13
|
+
export function serializeOperationMetadata(payload: OperationMetadataPayload): string {
|
|
14
|
+
const encoded = encodeURIComponent(JSON.stringify(payload))
|
|
15
|
+
return `${HEADER_PREFIX}${encoded}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function deserializeOperationMetadata(value: string | null | undefined): OperationMetadataPayload | null {
|
|
19
|
+
if (!value || typeof value !== 'string') return null
|
|
20
|
+
const trimmed = value.startsWith(HEADER_PREFIX) ? value.slice(HEADER_PREFIX.length) : value
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(decodeURIComponent(trimmed))
|
|
23
|
+
if (!parsed || typeof parsed !== 'object') return null
|
|
24
|
+
if (typeof parsed.id !== 'string' || typeof parsed.commandId !== 'string') return null
|
|
25
|
+
if (typeof parsed.undoToken !== 'string' || !parsed.undoToken) return null
|
|
26
|
+
if (typeof parsed.executedAt !== 'string') return null
|
|
27
|
+
return {
|
|
28
|
+
id: parsed.id,
|
|
29
|
+
undoToken: parsed.undoToken,
|
|
30
|
+
commandId: parsed.commandId,
|
|
31
|
+
actionLabel: parsed.actionLabel ?? null,
|
|
32
|
+
resourceKind: parsed.resourceKind ?? null,
|
|
33
|
+
resourceId: parsed.resourceId ?? null,
|
|
34
|
+
executedAt: parsed.executedAt,
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CommandHandler } from './types'
|
|
2
|
+
|
|
3
|
+
class CommandRegistry {
|
|
4
|
+
private handlers = new Map<string, CommandHandler>()
|
|
5
|
+
|
|
6
|
+
register(handler: CommandHandler) {
|
|
7
|
+
if (!handler?.id) throw new Error('Command handler must define an id')
|
|
8
|
+
if (this.handlers.has(handler.id)) {
|
|
9
|
+
throw new Error(`Duplicate command registration for id ${handler.id}`)
|
|
10
|
+
}
|
|
11
|
+
this.handlers.set(handler.id, handler)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
unregister(id: string) {
|
|
15
|
+
this.handlers.delete(id)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get<TInput = unknown, TResult = unknown>(id: string): CommandHandler<TInput, TResult> | null {
|
|
19
|
+
return (this.handlers.get(id) as CommandHandler<TInput, TResult> | undefined) ?? null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
has(id: string): boolean {
|
|
23
|
+
return this.handlers.has(id)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* List all registered command handler IDs.
|
|
28
|
+
*/
|
|
29
|
+
list(): string[] {
|
|
30
|
+
return Array.from(this.handlers.keys())
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
clear() {
|
|
34
|
+
this.handlers.clear()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const commandRegistry = new CommandRegistry()
|
|
39
|
+
|
|
40
|
+
export function registerCommand(handler: CommandHandler) {
|
|
41
|
+
commandRegistry.register(handler)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function unregisterCommand(id: string) {
|
|
45
|
+
commandRegistry.unregister(id)
|
|
46
|
+
}
|