@open-mercato/shared 0.6.5-develop.4718.1.56d834bb34 → 0.6.5-develop.4732.1.732f6c67a5

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.
@@ -1,2 +1,2 @@
1
- [build:shared] found 221 entry points
1
+ [build:shared] found 222 entry points
2
2
  [build:shared] built successfully
@@ -183,7 +183,8 @@ class CommandBus {
183
183
  }
184
184
  }
185
185
  const snapshots = await this.prepareSnapshots(handler, effectiveOptions);
186
- const result = await handler.execute(effectiveOptions.input, effectiveOptions.ctx);
186
+ const redoLogEntry = effectiveOptions.redoLogEntry ?? null;
187
+ const result = redoLogEntry && typeof handler.redo === "function" ? await handler.redo({ input: effectiveOptions.input, ctx: effectiveOptions.ctx, logEntry: redoLogEntry }) : await handler.execute(effectiveOptions.input, effectiveOptions.ctx);
187
188
  const afterSnapshot = await this.captureAfter(handler, effectiveOptions, result);
188
189
  const snapshotsWithAfter = { ...snapshots, after: afterSnapshot };
189
190
  const logMeta = await this.buildLog(handler, effectiveOptions, result, snapshotsWithAfter);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/commands/command-bus.ts"],
4
- "sourcesContent": ["import type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport type { ActionLogCreateInput } from '@open-mercato/core/modules/audit_logs/data/validators'\nimport { commandRegistry } from './registry'\nimport type {\n CommandExecutionOptions,\n CommandExecuteResult,\n CommandHandler,\n CommandLogBuilderArgs,\n CommandLogMetadata,\n CommandRuntimeContext,\n} from './types'\nimport { defaultUndoToken } from './types'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { AwilixContainer } from 'awilix'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport {\n canonicalizeResourceTag,\n deriveResourceFromCommandId,\n invalidateCrudCache,\n pickFirstIdentifier,\n isCrudCacheDebugEnabled,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { normalizeCustomFieldKey } from '@open-mercato/shared/lib/custom-fields/keys'\nimport { getAllCommandInterceptorInstances } from './command-interceptor-store'\nimport {\n runCommandInterceptorsBefore,\n runCommandInterceptorsAfter,\n runCommandInterceptorsBeforeUndo,\n runCommandInterceptorsAfterUndo,\n} from './command-interceptor-runner'\nimport type { CommandInterceptorContext } from './command-interceptor'\nimport { CommandInterceptorError } from './errors'\n\nconst SKIPPED_ACTION_LOG_RESOURCE_KINDS = new Set<string>([\n 'audit_logs.access',\n 'audit_logs.action',\n 'dashboards.layout',\n 'dashboards.user_widgets',\n 'dashboards.role_widgets',\n])\n\nfunction asRecord(input: unknown): Record<string, unknown> | null {\n if (!input || typeof input !== 'object' || Array.isArray(input)) return null\n return input as Record<string, unknown>\n}\n\nfunction toISOString(value: unknown): string | null {\n if (value instanceof Date) {\n const iso = value.toISOString()\n return Number.isNaN(value.getTime()) ? null : iso\n }\n if (typeof value === 'string') {\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n }\n return null\n}\n\nfunction deepEqual(a: unknown, b: unknown, seen?: Set<unknown>): boolean {\n if (Object.is(a, b)) return true\n if (a instanceof Date || b instanceof Date) {\n const aIso = toISOString(a)\n const bIso = toISOString(b)\n if (aIso != null && bIso != null) return aIso === bIso\n return false\n }\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((value, index) => deepEqual(value, b[index], seen))\n }\n if (a && b && typeof a === 'object' && typeof b === 'object') {\n if (!seen) seen = new Set()\n if (seen.has(a) || seen.has(b)) return false\n seen.add(a)\n seen.add(b)\n const aRec = a as Record<string, unknown>\n const bRec = b as Record<string, unknown>\n const keysA = Object.keys(aRec)\n const keysB = Object.keys(bRec)\n if (keysA.length !== keysB.length) return false\n return keysA.every((key) => deepEqual(aRec[key], bRec[key], seen))\n }\n return false\n}\n\nconst CUSTOM_FIELD_CONTAINER_KEYS = new Set(['custom', 'customFields', 'customValues', 'cf'])\nconst SKIPPED_CHANGE_KEYS = new Set(['updatedAt', 'updated_at'])\n\nfunction appendCustomFieldChanges(\n changes: Record<string, { from: unknown; to: unknown }>,\n before: unknown,\n after: unknown\n): boolean {\n const beforeRec = asRecord(before)\n const afterRec = asRecord(after)\n if (!beforeRec && !afterRec) return false\n const left = beforeRec ?? {}\n const right = afterRec ?? {}\n const keys = new Set([...Object.keys(left), ...Object.keys(right)])\n for (const key of keys) {\n const from = left[key]\n const to = right[key]\n if (!deepEqual(from, to)) {\n changes[normalizeCustomFieldKey(key)] = { from, to }\n }\n }\n return true\n}\n\nfunction buildRecordChanges(\n before: Record<string, unknown>,\n after: Record<string, unknown>,\n): Record<string, { from: unknown; to: unknown }> {\n return buildRecordChangesDeep(before, after)\n}\n\nfunction buildRecordChangesDeep(\n before: Record<string, unknown>,\n after: Record<string, unknown>,\n prefix?: string,\n seen?: Set<unknown>,\n): Record<string, { from: unknown; to: unknown }> {\n const changes: Record<string, { from: unknown; to: unknown }> = {}\n if (!seen) seen = new Set()\n if (seen.has(before) || seen.has(after)) return changes\n seen.add(before)\n seen.add(after)\n const keys = new Set([...Object.keys(before), ...Object.keys(after)])\n for (const key of keys) {\n if (SKIPPED_CHANGE_KEYS.has(key)) continue\n if (CUSTOM_FIELD_CONTAINER_KEYS.has(key)) {\n const handled = appendCustomFieldChanges(changes, before[key], after[key])\n if (handled) continue\n }\n const from = before[key]\n const to = after[key]\n const path = prefix ? `${prefix}.${key}` : key\n const fromRec = asRecord(from)\n const toRec = asRecord(to)\n if (fromRec && toRec) {\n const nested = buildRecordChangesDeep(fromRec, toRec, path, seen)\n if (Object.keys(nested).length) {\n Object.assign(changes, nested)\n continue\n }\n }\n if (!deepEqual(from, to)) {\n changes[path] = { from, to }\n }\n }\n return changes\n}\n\nfunction deriveChangesFromSnapshots(\n before: unknown,\n after: unknown,\n): Record<string, { from: unknown; to: unknown }> | null {\n const beforeRec = asRecord(before)\n const afterRec = asRecord(after)\n if (!beforeRec || !afterRec) return null\n const changes = buildRecordChanges(beforeRec, afterRec)\n return Object.keys(changes).length ? changes : null\n}\n\nfunction invertRecordedChanges(\n changes: unknown,\n): Record<string, { from: unknown; to: unknown }> | null {\n const source = asRecord(changes)\n if (!source) return null\n const inverted: Record<string, { from: unknown; to: unknown }> = {}\n for (const [key, value] of Object.entries(source)) {\n const entry = asRecord(value)\n if (!entry || (!('from' in entry) && !('to' in entry))) continue\n inverted[key] = {\n from: entry.to,\n to: entry.from,\n }\n }\n return Object.keys(inverted).length ? inverted : null\n}\n\nfunction extractAliasList(source: unknown): string[] {\n if (!source || typeof source !== 'object' || Array.isArray(source)) return []\n const record = source as Record<string, unknown>\n const raw = record.cacheAliases\n if (!Array.isArray(raw)) return []\n const aliases = new Set<string>()\n for (const value of raw) {\n if (typeof value !== 'string') continue\n const normalized = canonicalizeResourceTag(value)\n if (normalized) aliases.add(normalized)\n }\n return Array.from(aliases)\n}\n\nexport class CommandBus {\n async execute<TInput = unknown, TResult = unknown>(\n commandId: string,\n options: CommandExecutionOptions<TInput>\n ): Promise<CommandExecuteResult<TResult>> {\n const handler = this.resolveHandler<TInput, TResult>(commandId)\n\n // Run beforeExecute command interceptors\n const allInterceptors = getAllCommandInterceptorInstances()\n let interceptorMetadata = new Map<string, Record<string, unknown>>()\n let effectiveOptions = options\n const userFeatures = allInterceptors.length\n ? await this.resolveUserFeaturesForInterceptors(options.ctx)\n : []\n if (allInterceptors.length) {\n const interceptorCtx: CommandInterceptorContext = {\n commandId,\n auth: options.ctx.auth ?? null,\n selectedOrganizationId: options.ctx.selectedOrganizationId ?? options.ctx.auth?.orgId ?? null,\n container: options.ctx.container,\n }\n const beforeResult = await runCommandInterceptorsBefore(\n allInterceptors, commandId, options.input, interceptorCtx, userFeatures,\n )\n if (!beforeResult.ok) {\n throw new CommandInterceptorError(beforeResult.error!.message)\n }\n interceptorMetadata = beforeResult.metadataByInterceptor\n if (beforeResult.modifiedInput) {\n effectiveOptions = {\n ...options,\n input: { ...(options.input as object), ...beforeResult.modifiedInput } as TInput,\n }\n }\n }\n\n const snapshots = await this.prepareSnapshots(handler, effectiveOptions)\n const result = await handler.execute(effectiveOptions.input, effectiveOptions.ctx)\n const afterSnapshot = await this.captureAfter(handler, effectiveOptions, result)\n const snapshotsWithAfter = { ...snapshots, after: afterSnapshot }\n const logMeta = await this.buildLog(handler, effectiveOptions, result, snapshotsWithAfter)\n let mergedMeta = this.mergeMetadata(effectiveOptions.metadata, logMeta)\n const undoable = this.isUndoable(handler)\n if (undoable) {\n mergedMeta = mergedMeta ?? {}\n if (!mergedMeta.undoToken) mergedMeta.undoToken = defaultUndoToken()\n if (mergedMeta.actorUserId === undefined) mergedMeta.actorUserId = effectiveOptions.ctx.auth?.sub ?? null\n }\n if (afterSnapshot !== undefined && afterSnapshot !== null) {\n if (!mergedMeta) {\n mergedMeta = { snapshotAfter: afterSnapshot }\n } else if (!mergedMeta.snapshotAfter) {\n mergedMeta.snapshotAfter = afterSnapshot\n }\n }\n if (snapshots.before) {\n if (!mergedMeta) {\n mergedMeta = { snapshotBefore: snapshots.before }\n } else if (!mergedMeta.snapshotBefore) {\n mergedMeta.snapshotBefore = snapshots.before\n }\n }\n if (mergedMeta?.snapshotBefore !== undefined && mergedMeta?.snapshotAfter !== undefined) {\n const currentChanges = mergedMeta.changes\n const shouldInfer =\n currentChanges === undefined ||\n currentChanges === null ||\n (typeof currentChanges === 'object' && !Array.isArray(currentChanges) && Object.keys(currentChanges).length === 0)\n if (shouldInfer) {\n const inferred = deriveChangesFromSnapshots(mergedMeta.snapshotBefore, mergedMeta.snapshotAfter)\n if (inferred) mergedMeta.changes = inferred\n }\n }\n const logEntry = await this.persistLog(commandId, effectiveOptions, mergedMeta)\n\n // Run afterExecute command interceptors\n let finalResult = result\n if (allInterceptors.length) {\n const interceptorCtx: CommandInterceptorContext = {\n commandId,\n auth: effectiveOptions.ctx.auth ?? null,\n selectedOrganizationId: effectiveOptions.ctx.selectedOrganizationId ?? effectiveOptions.ctx.auth?.orgId ?? null,\n container: effectiveOptions.ctx.container,\n }\n const afterResult = await runCommandInterceptorsAfter(\n allInterceptors, commandId, effectiveOptions.input, result, interceptorCtx,\n userFeatures, interceptorMetadata,\n )\n if (afterResult.modifiedResult && typeof result === 'object' && result) {\n finalResult = { ...(result as object), ...afterResult.modifiedResult } as Awaited<TResult>\n }\n }\n\n if (!effectiveOptions.skipCacheInvalidation) {\n await this.invalidateCacheAfterExecute(commandId, effectiveOptions, finalResult, mergedMeta)\n }\n await this.flushCrudSideEffects(effectiveOptions.ctx.container)\n return { result: finalResult, logEntry }\n }\n\n async undo(undoToken: string, ctx: CommandRuntimeContext): Promise<void> {\n const service = (ctx.container.resolve('actionLogService') as ActionLogService)\n const log = await service.findByUndoToken(undoToken)\n if (!log) throw new Error('Undo token expired or not found')\n const handler = this.resolveHandler(log.commandId)\n if (!handler.undo || this.isUndoable(handler) === false) {\n throw new Error(`Command ${log.commandId} is not undoable`)\n }\n\n // Run beforeUndo command interceptors\n const allInterceptors = getAllCommandInterceptorInstances()\n let undoInterceptorMetadata = new Map<string, Record<string, unknown>>()\n const userFeatures = allInterceptors.length\n ? await this.resolveUserFeaturesForInterceptors(ctx)\n : []\n if (allInterceptors.length) {\n const undoCtx = { input: log.commandPayload, logEntry: log, undoToken }\n const interceptorCtx: CommandInterceptorContext = {\n commandId: log.commandId,\n auth: ctx.auth ?? null,\n selectedOrganizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n container: ctx.container,\n }\n const beforeResult = await runCommandInterceptorsBeforeUndo(\n allInterceptors, log.commandId, undoCtx, interceptorCtx, userFeatures,\n )\n if (!beforeResult.ok) {\n throw new CommandInterceptorError(beforeResult.error!.message)\n }\n undoInterceptorMetadata = beforeResult.metadataByInterceptor\n }\n\n await handler.undo({\n input: log.commandPayload as Parameters<NonNullable<typeof handler.undo>>[0]['input'],\n ctx,\n logEntry: log,\n })\n await service.markUndone(log.id, this.buildUndoTraceLog(log, ctx))\n\n // Run afterUndo command interceptors\n if (allInterceptors.length) {\n const undoCtx = { input: log.commandPayload, logEntry: log, undoToken }\n const interceptorCtx: CommandInterceptorContext = {\n commandId: log.commandId,\n auth: ctx.auth ?? null,\n selectedOrganizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n container: ctx.container,\n }\n await runCommandInterceptorsAfterUndo(\n allInterceptors, log.commandId, undoCtx, interceptorCtx,\n userFeatures, undoInterceptorMetadata,\n )\n }\n\n await this.invalidateCacheAfterUndo(log, ctx)\n await this.flushCrudSideEffects(ctx.container)\n }\n\n private buildUndoTraceLog(log: ActionLog, ctx: CommandRuntimeContext): ActionLogCreateInput | undefined {\n const snapshotBefore = log.snapshotAfter ?? null\n const snapshotAfter = log.snapshotBefore ?? null\n const changes =\n deriveChangesFromSnapshots(snapshotBefore, snapshotAfter)\n ?? invertRecordedChanges(log.changesJson)\n ?? undefined\n\n const baseContext = asRecord(log.contextJson) ?? {}\n const context = {\n ...baseContext,\n historyAction: 'undo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n\n return {\n tenantId: log.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: log.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n actorUserId: ctx.auth?.sub ?? log.actorUserId ?? null,\n commandId: log.commandId,\n actionLabel: log.actionLabel ?? undefined,\n resourceKind: log.resourceKind ?? undefined,\n resourceId: log.resourceId ?? undefined,\n parentResourceKind: log.parentResourceKind ?? null,\n parentResourceId: log.parentResourceId ?? null,\n relatedResourceKind: log.relatedResourceKind ?? null,\n relatedResourceId: log.relatedResourceId ?? null,\n snapshotBefore,\n snapshotAfter,\n changes,\n context,\n }\n }\n\n private async resolveUserFeaturesForInterceptors(ctx: CommandRuntimeContext): Promise<string[]> {\n if (!ctx.auth) return []\n try {\n type RbacLike = { getGrantedFeatures: (userId: string, opts: { tenantId: string | null; organizationId: string | null }) => Promise<string[]> }\n const rbac = ctx.container.resolve('rbacService') as RbacLike | undefined\n if (rbac?.getGrantedFeatures) {\n return await rbac.getGrantedFeatures(ctx.auth.sub, {\n tenantId: ctx.auth.tenantId,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId,\n })\n }\n } catch {\n // Intentional: rbacService is not registered in all runtime contexts (CLI, tests, bootstrap).\n // Falling through to return [] is safe \u2014 interceptors without feature gating still run.\n }\n return []\n }\n\n private resolveHandler<TInput, TResult>(commandId: string): CommandHandler<TInput, TResult> {\n const handler = commandRegistry.get<TInput, TResult>(commandId)\n if (!handler) {\n const moduleName = commandId.split('.')[0]\n const registered = commandRegistry.list()\n const sameModule = registered.filter((id) => id.split('.')[0] === moduleName)\n const hint = sameModule.length > 0\n ? ` Registered commands for module \"${moduleName}\": [${sameModule.join(', ')}].`\n : ` No commands registered for module \"${moduleName}\". Ensure the command file is imported (side-effect) in the module's index.ts.`\n throw new Error(`Command handler not registered for id ${commandId}.${hint}`)\n }\n return handler\n }\n\n private async prepareSnapshots<TInput, TResult>(\n handler: CommandHandler<TInput, TResult>,\n options: CommandExecutionOptions<TInput>\n ): Promise<{ before?: unknown }> {\n if (!handler.prepare) return {}\n try {\n return (await handler.prepare(options.input, options.ctx)) || {}\n } catch (err) {\n throw err\n }\n }\n\n private async captureAfter<TInput, TResult>(\n handler: CommandHandler<TInput, TResult>,\n options: CommandExecutionOptions<TInput>,\n result: TResult\n ): Promise<unknown> {\n if (!handler.captureAfter) return undefined\n return handler.captureAfter(options.input, result, options.ctx)\n }\n\n private async buildLog<TInput, TResult>(\n handler: CommandHandler<TInput, TResult>,\n options: CommandExecutionOptions<TInput>,\n result: TResult,\n snapshots: { before?: unknown; after?: unknown }\n ): Promise<CommandLogMetadata | null> {\n if (!handler.buildLog) return null\n const args: CommandLogBuilderArgs<TInput, TResult> = {\n input: options.input,\n result,\n ctx: options.ctx,\n snapshots,\n }\n return (await handler.buildLog(args)) || null\n }\n\n private mergeMetadata(primary?: CommandLogMetadata | null, secondary?: CommandLogMetadata | null): CommandLogMetadata | null {\n if (!primary && !secondary) return null\n return {\n skipLog: secondary?.skipLog ?? primary?.skipLog ?? false,\n tenantId: secondary?.tenantId ?? primary?.tenantId ?? null,\n organizationId: secondary?.organizationId ?? primary?.organizationId ?? null,\n actorUserId: secondary?.actorUserId ?? primary?.actorUserId ?? null,\n actionLabel: secondary?.actionLabel ?? primary?.actionLabel ?? null,\n resourceKind: secondary?.resourceKind ?? primary?.resourceKind ?? null,\n resourceId: secondary?.resourceId ?? primary?.resourceId ?? null,\n parentResourceKind: secondary?.parentResourceKind ?? primary?.parentResourceKind ?? null,\n parentResourceId: secondary?.parentResourceId ?? primary?.parentResourceId ?? null,\n relatedResourceKind: secondary?.relatedResourceKind ?? primary?.relatedResourceKind ?? null,\n relatedResourceId: secondary?.relatedResourceId ?? primary?.relatedResourceId ?? null,\n undoToken: secondary?.undoToken ?? primary?.undoToken ?? null,\n payload: secondary?.payload ?? primary?.payload ?? null,\n snapshotBefore: secondary?.snapshotBefore ?? primary?.snapshotBefore ?? null,\n snapshotAfter: secondary?.snapshotAfter ?? primary?.snapshotAfter ?? null,\n changes: secondary?.changes ?? primary?.changes ?? null,\n context: secondary?.context ?? primary?.context ?? null,\n }\n }\n\n private async persistLog<TInput>(\n commandId: string,\n options: CommandExecutionOptions<TInput>,\n metadata: CommandLogMetadata | null\n ): Promise<ActionLog | null> {\n if (!metadata) return null\n if (metadata.skipLog) return null\n const resourceKind =\n typeof metadata.resourceKind === 'string' ? metadata.resourceKind : null\n if (resourceKind && SKIPPED_ACTION_LOG_RESOURCE_KINDS.has(resourceKind)) {\n return null\n }\n let service: ActionLogService | null = null\n try {\n service = (options.ctx.container.resolve('actionLogService') as ActionLogService)\n } catch {\n service = null\n }\n if (!service) return null\n\n const tenantId = metadata.tenantId ?? options.ctx.auth?.tenantId ?? null\n const organizationId =\n metadata.organizationId ?? options.ctx.selectedOrganizationId ?? options.ctx.auth?.orgId ?? null\n const actorUserId = metadata.actorUserId ?? options.ctx.auth?.sub ?? null\n const payload: Record<string, unknown> = {\n tenantId: tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n actorUserId: actorUserId ?? undefined,\n commandId,\n }\n\n if (metadata) {\n if ('actionLabel' in metadata && metadata.actionLabel != null) payload.actionLabel = metadata.actionLabel\n if ('resourceKind' in metadata && metadata.resourceKind != null) payload.resourceKind = metadata.resourceKind\n if ('resourceId' in metadata && metadata.resourceId != null) payload.resourceId = metadata.resourceId\n if ('parentResourceKind' in metadata && metadata.parentResourceKind != null) payload.parentResourceKind = metadata.parentResourceKind\n if ('parentResourceId' in metadata && metadata.parentResourceId != null) payload.parentResourceId = metadata.parentResourceId\n if ('relatedResourceKind' in metadata && metadata.relatedResourceKind != null) payload.relatedResourceKind = metadata.relatedResourceKind\n if ('relatedResourceId' in metadata && metadata.relatedResourceId != null) payload.relatedResourceId = metadata.relatedResourceId\n if ('undoToken' in metadata && metadata.undoToken != null) payload.undoToken = metadata.undoToken\n if ('payload' in metadata && metadata.payload !== undefined) payload.commandPayload = metadata.payload\n if ('snapshotBefore' in metadata && metadata.snapshotBefore !== undefined) payload.snapshotBefore = metadata.snapshotBefore\n if ('snapshotAfter' in metadata && metadata.snapshotAfter !== undefined) payload.snapshotAfter = metadata.snapshotAfter\n if ('changes' in metadata && metadata.changes !== undefined && metadata.changes !== null) payload.changes = metadata.changes\n if ('context' in metadata && metadata.context !== undefined && metadata.context !== null) payload.context = metadata.context\n }\n\n const redoEnvelope = wrapRedoPayload('commandPayload' in payload ? (payload.commandPayload as unknown) : undefined, options.input)\n payload.commandPayload = redoEnvelope\n\n return await service.log(payload as ActionLogCreateInput)\n }\n\n private isUndoable(handler: CommandHandler<unknown, unknown>): boolean {\n return handler.isUndoable !== false && typeof handler.undo === 'function'\n }\n\n private async invalidateCacheAfterExecute<TResult>(\n commandId: string,\n options: CommandExecutionOptions<unknown>,\n result: TResult,\n metadata: CommandLogMetadata | null\n ): Promise<void> {\n const resource = typeof metadata?.resourceKind === 'string' ? metadata.resourceKind : null\n if (!resource) return\n try {\n const ctx = options.ctx\n const resultRecord = asRecord(result)\n const resultEntity = asRecord(resultRecord?.entity)\n const inputRecord = asRecord(options.input)\n const inputEntity = asRecord(inputRecord?.entity)\n\n const recordId = pickFirstIdentifier(\n metadata?.resourceId,\n resultRecord?.entityId,\n resultRecord?.id,\n resultRecord?.recordId,\n resultEntity?.id,\n inputRecord?.id,\n inputRecord?.entityId,\n inputRecord?.recordId,\n inputEntity?.id\n )\n\n const organizationId = pickFirstIdentifier(\n metadata?.organizationId,\n resultRecord?.organizationId,\n resultEntity?.organizationId,\n inputRecord?.organizationId,\n inputEntity?.organizationId,\n ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null\n )\n\n const tenantId = pickFirstIdentifier(\n metadata?.tenantId,\n resultRecord?.tenantId,\n resultEntity?.tenantId,\n inputRecord?.tenantId,\n inputEntity?.tenantId,\n ctx.auth?.tenantId ?? null\n )\n\n const fallbackTenant = pickFirstIdentifier(metadata?.tenantId, ctx.auth?.tenantId ?? null)\n\n const aliasSet = new Set<string>()\n for (const alias of extractAliasList(metadata?.context ?? null)) {\n aliasSet.add(alias)\n }\n const derived = deriveResourceFromCommandId(commandId)\n if (derived) aliasSet.add(derived)\n const aliasExtras = Array.from(aliasSet)\n await invalidateCrudCache(\n ctx.container,\n resource,\n { id: recordId, organizationId, tenantId },\n fallbackTenant,\n `command:${commandId}:execute`,\n aliasExtras\n )\n } catch (err) {\n if (isCrudCacheDebugEnabled()) {\n try {\n console.debug('[crud][cache] execute-invalidation failed', { commandId, err })\n } catch {}\n }\n }\n }\n\n private async invalidateCacheAfterUndo(log: ActionLog, ctx: CommandRuntimeContext): Promise<void> {\n const resource = typeof log.resourceKind === 'string' ? log.resourceKind : null\n if (!resource) return\n try {\n const recordId = pickFirstIdentifier(log.resourceId)\n const organizationId = pickFirstIdentifier(log.organizationId, ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null)\n const tenantId = pickFirstIdentifier(log.tenantId, ctx.auth?.tenantId ?? null)\n const fallbackTenant = pickFirstIdentifier(log.tenantId, ctx.auth?.tenantId ?? null)\n const aliasSet = new Set<string>()\n for (const alias of extractAliasList(log.contextJson ?? null)) {\n aliasSet.add(alias)\n }\n const derived = deriveResourceFromCommandId(log.commandId)\n if (derived) aliasSet.add(derived)\n const aliasExtras = Array.from(aliasSet)\n await invalidateCrudCache(\n ctx.container,\n resource,\n { id: recordId, organizationId, tenantId },\n fallbackTenant,\n `command:${log.commandId}:undo`,\n aliasExtras\n )\n } catch (err) {\n if (isCrudCacheDebugEnabled()) {\n try {\n console.debug('[crud][cache] undo-invalidation failed', { commandId: log.commandId, err })\n } catch {}\n }\n }\n }\n\n private async flushCrudSideEffects(container: AwilixContainer): Promise<void> {\n try {\n const dataEngine = (container.resolve('dataEngine') as DataEngine)\n await dataEngine.flushOrmEntityChanges()\n } catch {\n // best-effort: failures should not block command execution\n }\n }\n}\n\ntype RedoEnvelope = {\n __redoInput: unknown\n [key: string]: unknown\n}\n\nfunction wrapRedoPayload(existing: unknown, input: unknown): RedoEnvelope {\n if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {\n const envelope: RedoEnvelope = { __redoInput: input }\n if (existing !== undefined) envelope.value = existing\n return envelope\n }\n const current = existing as Record<string, unknown>\n if ('__redoInput' in current && current.__redoInput !== undefined) {\n return current as RedoEnvelope\n }\n return { __redoInput: input, ...current }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport type { ActionLogCreateInput } from '@open-mercato/core/modules/audit_logs/data/validators'\nimport { commandRegistry } from './registry'\nimport type {\n CommandExecutionOptions,\n CommandExecuteResult,\n CommandHandler,\n CommandLogBuilderArgs,\n CommandLogMetadata,\n CommandRuntimeContext,\n} from './types'\nimport { defaultUndoToken } from './types'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { AwilixContainer } from 'awilix'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport {\n canonicalizeResourceTag,\n deriveResourceFromCommandId,\n invalidateCrudCache,\n pickFirstIdentifier,\n isCrudCacheDebugEnabled,\n} from '@open-mercato/shared/lib/crud/cache'\nimport { normalizeCustomFieldKey } from '@open-mercato/shared/lib/custom-fields/keys'\nimport { getAllCommandInterceptorInstances } from './command-interceptor-store'\nimport {\n runCommandInterceptorsBefore,\n runCommandInterceptorsAfter,\n runCommandInterceptorsBeforeUndo,\n runCommandInterceptorsAfterUndo,\n} from './command-interceptor-runner'\nimport type { CommandInterceptorContext } from './command-interceptor'\nimport { CommandInterceptorError } from './errors'\n\nconst SKIPPED_ACTION_LOG_RESOURCE_KINDS = new Set<string>([\n 'audit_logs.access',\n 'audit_logs.action',\n 'dashboards.layout',\n 'dashboards.user_widgets',\n 'dashboards.role_widgets',\n])\n\nfunction asRecord(input: unknown): Record<string, unknown> | null {\n if (!input || typeof input !== 'object' || Array.isArray(input)) return null\n return input as Record<string, unknown>\n}\n\nfunction toISOString(value: unknown): string | null {\n if (value instanceof Date) {\n const iso = value.toISOString()\n return Number.isNaN(value.getTime()) ? null : iso\n }\n if (typeof value === 'string') {\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n }\n return null\n}\n\nfunction deepEqual(a: unknown, b: unknown, seen?: Set<unknown>): boolean {\n if (Object.is(a, b)) return true\n if (a instanceof Date || b instanceof Date) {\n const aIso = toISOString(a)\n const bIso = toISOString(b)\n if (aIso != null && bIso != null) return aIso === bIso\n return false\n }\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n return a.every((value, index) => deepEqual(value, b[index], seen))\n }\n if (a && b && typeof a === 'object' && typeof b === 'object') {\n if (!seen) seen = new Set()\n if (seen.has(a) || seen.has(b)) return false\n seen.add(a)\n seen.add(b)\n const aRec = a as Record<string, unknown>\n const bRec = b as Record<string, unknown>\n const keysA = Object.keys(aRec)\n const keysB = Object.keys(bRec)\n if (keysA.length !== keysB.length) return false\n return keysA.every((key) => deepEqual(aRec[key], bRec[key], seen))\n }\n return false\n}\n\nconst CUSTOM_FIELD_CONTAINER_KEYS = new Set(['custom', 'customFields', 'customValues', 'cf'])\nconst SKIPPED_CHANGE_KEYS = new Set(['updatedAt', 'updated_at'])\n\nfunction appendCustomFieldChanges(\n changes: Record<string, { from: unknown; to: unknown }>,\n before: unknown,\n after: unknown\n): boolean {\n const beforeRec = asRecord(before)\n const afterRec = asRecord(after)\n if (!beforeRec && !afterRec) return false\n const left = beforeRec ?? {}\n const right = afterRec ?? {}\n const keys = new Set([...Object.keys(left), ...Object.keys(right)])\n for (const key of keys) {\n const from = left[key]\n const to = right[key]\n if (!deepEqual(from, to)) {\n changes[normalizeCustomFieldKey(key)] = { from, to }\n }\n }\n return true\n}\n\nfunction buildRecordChanges(\n before: Record<string, unknown>,\n after: Record<string, unknown>,\n): Record<string, { from: unknown; to: unknown }> {\n return buildRecordChangesDeep(before, after)\n}\n\nfunction buildRecordChangesDeep(\n before: Record<string, unknown>,\n after: Record<string, unknown>,\n prefix?: string,\n seen?: Set<unknown>,\n): Record<string, { from: unknown; to: unknown }> {\n const changes: Record<string, { from: unknown; to: unknown }> = {}\n if (!seen) seen = new Set()\n if (seen.has(before) || seen.has(after)) return changes\n seen.add(before)\n seen.add(after)\n const keys = new Set([...Object.keys(before), ...Object.keys(after)])\n for (const key of keys) {\n if (SKIPPED_CHANGE_KEYS.has(key)) continue\n if (CUSTOM_FIELD_CONTAINER_KEYS.has(key)) {\n const handled = appendCustomFieldChanges(changes, before[key], after[key])\n if (handled) continue\n }\n const from = before[key]\n const to = after[key]\n const path = prefix ? `${prefix}.${key}` : key\n const fromRec = asRecord(from)\n const toRec = asRecord(to)\n if (fromRec && toRec) {\n const nested = buildRecordChangesDeep(fromRec, toRec, path, seen)\n if (Object.keys(nested).length) {\n Object.assign(changes, nested)\n continue\n }\n }\n if (!deepEqual(from, to)) {\n changes[path] = { from, to }\n }\n }\n return changes\n}\n\nfunction deriveChangesFromSnapshots(\n before: unknown,\n after: unknown,\n): Record<string, { from: unknown; to: unknown }> | null {\n const beforeRec = asRecord(before)\n const afterRec = asRecord(after)\n if (!beforeRec || !afterRec) return null\n const changes = buildRecordChanges(beforeRec, afterRec)\n return Object.keys(changes).length ? changes : null\n}\n\nfunction invertRecordedChanges(\n changes: unknown,\n): Record<string, { from: unknown; to: unknown }> | null {\n const source = asRecord(changes)\n if (!source) return null\n const inverted: Record<string, { from: unknown; to: unknown }> = {}\n for (const [key, value] of Object.entries(source)) {\n const entry = asRecord(value)\n if (!entry || (!('from' in entry) && !('to' in entry))) continue\n inverted[key] = {\n from: entry.to,\n to: entry.from,\n }\n }\n return Object.keys(inverted).length ? inverted : null\n}\n\nfunction extractAliasList(source: unknown): string[] {\n if (!source || typeof source !== 'object' || Array.isArray(source)) return []\n const record = source as Record<string, unknown>\n const raw = record.cacheAliases\n if (!Array.isArray(raw)) return []\n const aliases = new Set<string>()\n for (const value of raw) {\n if (typeof value !== 'string') continue\n const normalized = canonicalizeResourceTag(value)\n if (normalized) aliases.add(normalized)\n }\n return Array.from(aliases)\n}\n\nexport class CommandBus {\n async execute<TInput = unknown, TResult = unknown>(\n commandId: string,\n options: CommandExecutionOptions<TInput>\n ): Promise<CommandExecuteResult<TResult>> {\n const handler = this.resolveHandler<TInput, TResult>(commandId)\n\n // Run beforeExecute command interceptors\n const allInterceptors = getAllCommandInterceptorInstances()\n let interceptorMetadata = new Map<string, Record<string, unknown>>()\n let effectiveOptions = options\n const userFeatures = allInterceptors.length\n ? await this.resolveUserFeaturesForInterceptors(options.ctx)\n : []\n if (allInterceptors.length) {\n const interceptorCtx: CommandInterceptorContext = {\n commandId,\n auth: options.ctx.auth ?? null,\n selectedOrganizationId: options.ctx.selectedOrganizationId ?? options.ctx.auth?.orgId ?? null,\n container: options.ctx.container,\n }\n const beforeResult = await runCommandInterceptorsBefore(\n allInterceptors, commandId, options.input, interceptorCtx, userFeatures,\n )\n if (!beforeResult.ok) {\n throw new CommandInterceptorError(beforeResult.error!.message)\n }\n interceptorMetadata = beforeResult.metadataByInterceptor\n if (beforeResult.modifiedInput) {\n effectiveOptions = {\n ...options,\n input: { ...(options.input as object), ...beforeResult.modifiedInput } as TInput,\n }\n }\n }\n\n const snapshots = await this.prepareSnapshots(handler, effectiveOptions)\n const redoLogEntry = effectiveOptions.redoLogEntry ?? null\n const result =\n redoLogEntry && typeof handler.redo === 'function'\n ? await handler.redo({ input: effectiveOptions.input, ctx: effectiveOptions.ctx, logEntry: redoLogEntry })\n : await handler.execute(effectiveOptions.input, effectiveOptions.ctx)\n const afterSnapshot = await this.captureAfter(handler, effectiveOptions, result)\n const snapshotsWithAfter = { ...snapshots, after: afterSnapshot }\n const logMeta = await this.buildLog(handler, effectiveOptions, result, snapshotsWithAfter)\n let mergedMeta = this.mergeMetadata(effectiveOptions.metadata, logMeta)\n const undoable = this.isUndoable(handler)\n if (undoable) {\n mergedMeta = mergedMeta ?? {}\n if (!mergedMeta.undoToken) mergedMeta.undoToken = defaultUndoToken()\n if (mergedMeta.actorUserId === undefined) mergedMeta.actorUserId = effectiveOptions.ctx.auth?.sub ?? null\n }\n if (afterSnapshot !== undefined && afterSnapshot !== null) {\n if (!mergedMeta) {\n mergedMeta = { snapshotAfter: afterSnapshot }\n } else if (!mergedMeta.snapshotAfter) {\n mergedMeta.snapshotAfter = afterSnapshot\n }\n }\n if (snapshots.before) {\n if (!mergedMeta) {\n mergedMeta = { snapshotBefore: snapshots.before }\n } else if (!mergedMeta.snapshotBefore) {\n mergedMeta.snapshotBefore = snapshots.before\n }\n }\n if (mergedMeta?.snapshotBefore !== undefined && mergedMeta?.snapshotAfter !== undefined) {\n const currentChanges = mergedMeta.changes\n const shouldInfer =\n currentChanges === undefined ||\n currentChanges === null ||\n (typeof currentChanges === 'object' && !Array.isArray(currentChanges) && Object.keys(currentChanges).length === 0)\n if (shouldInfer) {\n const inferred = deriveChangesFromSnapshots(mergedMeta.snapshotBefore, mergedMeta.snapshotAfter)\n if (inferred) mergedMeta.changes = inferred\n }\n }\n const logEntry = await this.persistLog(commandId, effectiveOptions, mergedMeta)\n\n // Run afterExecute command interceptors\n let finalResult = result\n if (allInterceptors.length) {\n const interceptorCtx: CommandInterceptorContext = {\n commandId,\n auth: effectiveOptions.ctx.auth ?? null,\n selectedOrganizationId: effectiveOptions.ctx.selectedOrganizationId ?? effectiveOptions.ctx.auth?.orgId ?? null,\n container: effectiveOptions.ctx.container,\n }\n const afterResult = await runCommandInterceptorsAfter(\n allInterceptors, commandId, effectiveOptions.input, result, interceptorCtx,\n userFeatures, interceptorMetadata,\n )\n if (afterResult.modifiedResult && typeof result === 'object' && result) {\n finalResult = { ...(result as object), ...afterResult.modifiedResult } as Awaited<TResult>\n }\n }\n\n if (!effectiveOptions.skipCacheInvalidation) {\n await this.invalidateCacheAfterExecute(commandId, effectiveOptions, finalResult, mergedMeta)\n }\n await this.flushCrudSideEffects(effectiveOptions.ctx.container)\n return { result: finalResult, logEntry }\n }\n\n async undo(undoToken: string, ctx: CommandRuntimeContext): Promise<void> {\n const service = (ctx.container.resolve('actionLogService') as ActionLogService)\n const log = await service.findByUndoToken(undoToken)\n if (!log) throw new Error('Undo token expired or not found')\n const handler = this.resolveHandler(log.commandId)\n if (!handler.undo || this.isUndoable(handler) === false) {\n throw new Error(`Command ${log.commandId} is not undoable`)\n }\n\n // Run beforeUndo command interceptors\n const allInterceptors = getAllCommandInterceptorInstances()\n let undoInterceptorMetadata = new Map<string, Record<string, unknown>>()\n const userFeatures = allInterceptors.length\n ? await this.resolveUserFeaturesForInterceptors(ctx)\n : []\n if (allInterceptors.length) {\n const undoCtx = { input: log.commandPayload, logEntry: log, undoToken }\n const interceptorCtx: CommandInterceptorContext = {\n commandId: log.commandId,\n auth: ctx.auth ?? null,\n selectedOrganizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n container: ctx.container,\n }\n const beforeResult = await runCommandInterceptorsBeforeUndo(\n allInterceptors, log.commandId, undoCtx, interceptorCtx, userFeatures,\n )\n if (!beforeResult.ok) {\n throw new CommandInterceptorError(beforeResult.error!.message)\n }\n undoInterceptorMetadata = beforeResult.metadataByInterceptor\n }\n\n await handler.undo({\n input: log.commandPayload as Parameters<NonNullable<typeof handler.undo>>[0]['input'],\n ctx,\n logEntry: log,\n })\n await service.markUndone(log.id, this.buildUndoTraceLog(log, ctx))\n\n // Run afterUndo command interceptors\n if (allInterceptors.length) {\n const undoCtx = { input: log.commandPayload, logEntry: log, undoToken }\n const interceptorCtx: CommandInterceptorContext = {\n commandId: log.commandId,\n auth: ctx.auth ?? null,\n selectedOrganizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n container: ctx.container,\n }\n await runCommandInterceptorsAfterUndo(\n allInterceptors, log.commandId, undoCtx, interceptorCtx,\n userFeatures, undoInterceptorMetadata,\n )\n }\n\n await this.invalidateCacheAfterUndo(log, ctx)\n await this.flushCrudSideEffects(ctx.container)\n }\n\n private buildUndoTraceLog(log: ActionLog, ctx: CommandRuntimeContext): ActionLogCreateInput | undefined {\n const snapshotBefore = log.snapshotAfter ?? null\n const snapshotAfter = log.snapshotBefore ?? null\n const changes =\n deriveChangesFromSnapshots(snapshotBefore, snapshotAfter)\n ?? invertRecordedChanges(log.changesJson)\n ?? undefined\n\n const baseContext = asRecord(log.contextJson) ?? {}\n const context = {\n ...baseContext,\n historyAction: 'undo',\n sourceLogId: log.id,\n sourceCommandId: log.commandId,\n }\n\n return {\n tenantId: log.tenantId ?? ctx.auth?.tenantId ?? null,\n organizationId: log.organizationId ?? ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n actorUserId: ctx.auth?.sub ?? log.actorUserId ?? null,\n commandId: log.commandId,\n actionLabel: log.actionLabel ?? undefined,\n resourceKind: log.resourceKind ?? undefined,\n resourceId: log.resourceId ?? undefined,\n parentResourceKind: log.parentResourceKind ?? null,\n parentResourceId: log.parentResourceId ?? null,\n relatedResourceKind: log.relatedResourceKind ?? null,\n relatedResourceId: log.relatedResourceId ?? null,\n snapshotBefore,\n snapshotAfter,\n changes,\n context,\n }\n }\n\n private async resolveUserFeaturesForInterceptors(ctx: CommandRuntimeContext): Promise<string[]> {\n if (!ctx.auth) return []\n try {\n type RbacLike = { getGrantedFeatures: (userId: string, opts: { tenantId: string | null; organizationId: string | null }) => Promise<string[]> }\n const rbac = ctx.container.resolve('rbacService') as RbacLike | undefined\n if (rbac?.getGrantedFeatures) {\n return await rbac.getGrantedFeatures(ctx.auth.sub, {\n tenantId: ctx.auth.tenantId,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId,\n })\n }\n } catch {\n // Intentional: rbacService is not registered in all runtime contexts (CLI, tests, bootstrap).\n // Falling through to return [] is safe \u2014 interceptors without feature gating still run.\n }\n return []\n }\n\n private resolveHandler<TInput, TResult>(commandId: string): CommandHandler<TInput, TResult> {\n const handler = commandRegistry.get<TInput, TResult>(commandId)\n if (!handler) {\n const moduleName = commandId.split('.')[0]\n const registered = commandRegistry.list()\n const sameModule = registered.filter((id) => id.split('.')[0] === moduleName)\n const hint = sameModule.length > 0\n ? ` Registered commands for module \"${moduleName}\": [${sameModule.join(', ')}].`\n : ` No commands registered for module \"${moduleName}\". Ensure the command file is imported (side-effect) in the module's index.ts.`\n throw new Error(`Command handler not registered for id ${commandId}.${hint}`)\n }\n return handler\n }\n\n private async prepareSnapshots<TInput, TResult>(\n handler: CommandHandler<TInput, TResult>,\n options: CommandExecutionOptions<TInput>\n ): Promise<{ before?: unknown }> {\n if (!handler.prepare) return {}\n try {\n return (await handler.prepare(options.input, options.ctx)) || {}\n } catch (err) {\n throw err\n }\n }\n\n private async captureAfter<TInput, TResult>(\n handler: CommandHandler<TInput, TResult>,\n options: CommandExecutionOptions<TInput>,\n result: TResult\n ): Promise<unknown> {\n if (!handler.captureAfter) return undefined\n return handler.captureAfter(options.input, result, options.ctx)\n }\n\n private async buildLog<TInput, TResult>(\n handler: CommandHandler<TInput, TResult>,\n options: CommandExecutionOptions<TInput>,\n result: TResult,\n snapshots: { before?: unknown; after?: unknown }\n ): Promise<CommandLogMetadata | null> {\n if (!handler.buildLog) return null\n const args: CommandLogBuilderArgs<TInput, TResult> = {\n input: options.input,\n result,\n ctx: options.ctx,\n snapshots,\n }\n return (await handler.buildLog(args)) || null\n }\n\n private mergeMetadata(primary?: CommandLogMetadata | null, secondary?: CommandLogMetadata | null): CommandLogMetadata | null {\n if (!primary && !secondary) return null\n return {\n skipLog: secondary?.skipLog ?? primary?.skipLog ?? false,\n tenantId: secondary?.tenantId ?? primary?.tenantId ?? null,\n organizationId: secondary?.organizationId ?? primary?.organizationId ?? null,\n actorUserId: secondary?.actorUserId ?? primary?.actorUserId ?? null,\n actionLabel: secondary?.actionLabel ?? primary?.actionLabel ?? null,\n resourceKind: secondary?.resourceKind ?? primary?.resourceKind ?? null,\n resourceId: secondary?.resourceId ?? primary?.resourceId ?? null,\n parentResourceKind: secondary?.parentResourceKind ?? primary?.parentResourceKind ?? null,\n parentResourceId: secondary?.parentResourceId ?? primary?.parentResourceId ?? null,\n relatedResourceKind: secondary?.relatedResourceKind ?? primary?.relatedResourceKind ?? null,\n relatedResourceId: secondary?.relatedResourceId ?? primary?.relatedResourceId ?? null,\n undoToken: secondary?.undoToken ?? primary?.undoToken ?? null,\n payload: secondary?.payload ?? primary?.payload ?? null,\n snapshotBefore: secondary?.snapshotBefore ?? primary?.snapshotBefore ?? null,\n snapshotAfter: secondary?.snapshotAfter ?? primary?.snapshotAfter ?? null,\n changes: secondary?.changes ?? primary?.changes ?? null,\n context: secondary?.context ?? primary?.context ?? null,\n }\n }\n\n private async persistLog<TInput>(\n commandId: string,\n options: CommandExecutionOptions<TInput>,\n metadata: CommandLogMetadata | null\n ): Promise<ActionLog | null> {\n if (!metadata) return null\n if (metadata.skipLog) return null\n const resourceKind =\n typeof metadata.resourceKind === 'string' ? metadata.resourceKind : null\n if (resourceKind && SKIPPED_ACTION_LOG_RESOURCE_KINDS.has(resourceKind)) {\n return null\n }\n let service: ActionLogService | null = null\n try {\n service = (options.ctx.container.resolve('actionLogService') as ActionLogService)\n } catch {\n service = null\n }\n if (!service) return null\n\n const tenantId = metadata.tenantId ?? options.ctx.auth?.tenantId ?? null\n const organizationId =\n metadata.organizationId ?? options.ctx.selectedOrganizationId ?? options.ctx.auth?.orgId ?? null\n const actorUserId = metadata.actorUserId ?? options.ctx.auth?.sub ?? null\n const payload: Record<string, unknown> = {\n tenantId: tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n actorUserId: actorUserId ?? undefined,\n commandId,\n }\n\n if (metadata) {\n if ('actionLabel' in metadata && metadata.actionLabel != null) payload.actionLabel = metadata.actionLabel\n if ('resourceKind' in metadata && metadata.resourceKind != null) payload.resourceKind = metadata.resourceKind\n if ('resourceId' in metadata && metadata.resourceId != null) payload.resourceId = metadata.resourceId\n if ('parentResourceKind' in metadata && metadata.parentResourceKind != null) payload.parentResourceKind = metadata.parentResourceKind\n if ('parentResourceId' in metadata && metadata.parentResourceId != null) payload.parentResourceId = metadata.parentResourceId\n if ('relatedResourceKind' in metadata && metadata.relatedResourceKind != null) payload.relatedResourceKind = metadata.relatedResourceKind\n if ('relatedResourceId' in metadata && metadata.relatedResourceId != null) payload.relatedResourceId = metadata.relatedResourceId\n if ('undoToken' in metadata && metadata.undoToken != null) payload.undoToken = metadata.undoToken\n if ('payload' in metadata && metadata.payload !== undefined) payload.commandPayload = metadata.payload\n if ('snapshotBefore' in metadata && metadata.snapshotBefore !== undefined) payload.snapshotBefore = metadata.snapshotBefore\n if ('snapshotAfter' in metadata && metadata.snapshotAfter !== undefined) payload.snapshotAfter = metadata.snapshotAfter\n if ('changes' in metadata && metadata.changes !== undefined && metadata.changes !== null) payload.changes = metadata.changes\n if ('context' in metadata && metadata.context !== undefined && metadata.context !== null) payload.context = metadata.context\n }\n\n const redoEnvelope = wrapRedoPayload('commandPayload' in payload ? (payload.commandPayload as unknown) : undefined, options.input)\n payload.commandPayload = redoEnvelope\n\n return await service.log(payload as ActionLogCreateInput)\n }\n\n private isUndoable(handler: CommandHandler<unknown, unknown>): boolean {\n return handler.isUndoable !== false && typeof handler.undo === 'function'\n }\n\n private async invalidateCacheAfterExecute<TResult>(\n commandId: string,\n options: CommandExecutionOptions<unknown>,\n result: TResult,\n metadata: CommandLogMetadata | null\n ): Promise<void> {\n const resource = typeof metadata?.resourceKind === 'string' ? metadata.resourceKind : null\n if (!resource) return\n try {\n const ctx = options.ctx\n const resultRecord = asRecord(result)\n const resultEntity = asRecord(resultRecord?.entity)\n const inputRecord = asRecord(options.input)\n const inputEntity = asRecord(inputRecord?.entity)\n\n const recordId = pickFirstIdentifier(\n metadata?.resourceId,\n resultRecord?.entityId,\n resultRecord?.id,\n resultRecord?.recordId,\n resultEntity?.id,\n inputRecord?.id,\n inputRecord?.entityId,\n inputRecord?.recordId,\n inputEntity?.id\n )\n\n const organizationId = pickFirstIdentifier(\n metadata?.organizationId,\n resultRecord?.organizationId,\n resultEntity?.organizationId,\n inputRecord?.organizationId,\n inputEntity?.organizationId,\n ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null\n )\n\n const tenantId = pickFirstIdentifier(\n metadata?.tenantId,\n resultRecord?.tenantId,\n resultEntity?.tenantId,\n inputRecord?.tenantId,\n inputEntity?.tenantId,\n ctx.auth?.tenantId ?? null\n )\n\n const fallbackTenant = pickFirstIdentifier(metadata?.tenantId, ctx.auth?.tenantId ?? null)\n\n const aliasSet = new Set<string>()\n for (const alias of extractAliasList(metadata?.context ?? null)) {\n aliasSet.add(alias)\n }\n const derived = deriveResourceFromCommandId(commandId)\n if (derived) aliasSet.add(derived)\n const aliasExtras = Array.from(aliasSet)\n await invalidateCrudCache(\n ctx.container,\n resource,\n { id: recordId, organizationId, tenantId },\n fallbackTenant,\n `command:${commandId}:execute`,\n aliasExtras\n )\n } catch (err) {\n if (isCrudCacheDebugEnabled()) {\n try {\n console.debug('[crud][cache] execute-invalidation failed', { commandId, err })\n } catch {}\n }\n }\n }\n\n private async invalidateCacheAfterUndo(log: ActionLog, ctx: CommandRuntimeContext): Promise<void> {\n const resource = typeof log.resourceKind === 'string' ? log.resourceKind : null\n if (!resource) return\n try {\n const recordId = pickFirstIdentifier(log.resourceId)\n const organizationId = pickFirstIdentifier(log.organizationId, ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null)\n const tenantId = pickFirstIdentifier(log.tenantId, ctx.auth?.tenantId ?? null)\n const fallbackTenant = pickFirstIdentifier(log.tenantId, ctx.auth?.tenantId ?? null)\n const aliasSet = new Set<string>()\n for (const alias of extractAliasList(log.contextJson ?? null)) {\n aliasSet.add(alias)\n }\n const derived = deriveResourceFromCommandId(log.commandId)\n if (derived) aliasSet.add(derived)\n const aliasExtras = Array.from(aliasSet)\n await invalidateCrudCache(\n ctx.container,\n resource,\n { id: recordId, organizationId, tenantId },\n fallbackTenant,\n `command:${log.commandId}:undo`,\n aliasExtras\n )\n } catch (err) {\n if (isCrudCacheDebugEnabled()) {\n try {\n console.debug('[crud][cache] undo-invalidation failed', { commandId: log.commandId, err })\n } catch {}\n }\n }\n }\n\n private async flushCrudSideEffects(container: AwilixContainer): Promise<void> {\n try {\n const dataEngine = (container.resolve('dataEngine') as DataEngine)\n await dataEngine.flushOrmEntityChanges()\n } catch {\n // best-effort: failures should not block command execution\n }\n }\n}\n\ntype RedoEnvelope = {\n __redoInput: unknown\n [key: string]: unknown\n}\n\nfunction wrapRedoPayload(existing: unknown, input: unknown): RedoEnvelope {\n if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {\n const envelope: RedoEnvelope = { __redoInput: input }\n if (existing !== undefined) envelope.value = existing\n return envelope\n }\n const current = existing as Record<string, unknown>\n if ('__redoInput' in current && current.__redoInput !== undefined) {\n return current as RedoEnvelope\n }\n return { __redoInput: input, ...current }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -3,6 +3,7 @@ export * from "./registry.js";
3
3
  import { CommandBus } from "./command-bus.js";
4
4
  export * from "./customFieldSnapshots.js";
5
5
  export * from "./undo.js";
6
+ export * from "./redo.js";
6
7
  import { CommandInterceptorError } from "./errors.js";
7
8
  import {
8
9
  runCrudCommandWrite
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/commands/index.ts"],
4
- "sourcesContent": ["export * from './types'\nexport * from './registry'\nexport { CommandBus } from './command-bus'\nexport * from './customFieldSnapshots'\nexport * from './undo'\nexport { CommandInterceptorError } from './errors'\nexport {\n runCrudCommandWrite,\n type RunCrudCommandWriteOptions,\n type RunCrudCommandWriteResult,\n type CrudCommandWritePhase,\n type CrudCommandWriteScope,\n type CrudCommandWriteSideEffectTarget,\n} from './runCrudCommandWrite'\n"],
5
- "mappings": "AAAA,cAAc;AACd,cAAc;AACd,SAAS,kBAAkB;AAC3B,cAAc;AACd,cAAc;AACd,SAAS,+BAA+B;AACxC;AAAA,EACE;AAAA,OAMK;",
4
+ "sourcesContent": ["export * from './types'\nexport * from './registry'\nexport { CommandBus } from './command-bus'\nexport * from './customFieldSnapshots'\nexport * from './undo'\nexport * from './redo'\nexport { CommandInterceptorError } from './errors'\nexport {\n runCrudCommandWrite,\n type RunCrudCommandWriteOptions,\n type RunCrudCommandWriteResult,\n type CrudCommandWritePhase,\n type CrudCommandWriteScope,\n type CrudCommandWriteSideEffectTarget,\n} from './runCrudCommandWrite'\n"],
5
+ "mappings": "AAAA,cAAc;AACd,cAAc;AACd,SAAS,kBAAkB;AAC3B,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,+BAA+B;AACxC;AAAA,EACE;AAAA,OAMK;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,106 @@
1
+ import { CrudHttpError, conflict, isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
2
+ import { extractUndoPayload } from "./undo.js";
3
+ import { emitCrudSideEffects } from "./helpers.js";
4
+ import { withAtomicFlush } from "./flush.js";
5
+ const DEFAULT_SNAPSHOT_DATE_FIELDS = ["createdAt", "updatedAt", "deletedAt"];
6
+ function reviveSnapshotSeed(snapshot, dateFields = DEFAULT_SNAPSHOT_DATE_FIELDS) {
7
+ const seed = { ...snapshot };
8
+ for (const field of dateFields) {
9
+ const value = seed[field];
10
+ if (typeof value === "string") seed[field] = new Date(value);
11
+ }
12
+ return seed;
13
+ }
14
+ function serializeRowSnapshot(entity, fields, dateFields = DEFAULT_SNAPSHOT_DATE_FIELDS) {
15
+ const snapshot = {};
16
+ const dateFieldSet = new Set(dateFields);
17
+ for (const field of fields) {
18
+ const value = entity[field];
19
+ if (dateFieldSet.has(field)) {
20
+ snapshot[field] = value instanceof Date ? value.toISOString() : value ?? null;
21
+ } else {
22
+ snapshot[field] = value ?? null;
23
+ }
24
+ }
25
+ return snapshot;
26
+ }
27
+ function resolveRedoSnapshot(logEntry) {
28
+ if (!logEntry) return null;
29
+ const undo = extractUndoPayload(logEntry);
30
+ if (undo && undo.after != null) return undo.after;
31
+ if (logEntry.snapshotAfter != null) return logEntry.snapshotAfter;
32
+ return null;
33
+ }
34
+ async function restoreCreatedRow(em, entityClass, id, seedFromSnapshot, findRow) {
35
+ const existing = findRow ? await findRow(em, id) : await em.findOne(entityClass, { id });
36
+ if (existing) {
37
+ existing.deletedAt = null;
38
+ const seed = seedFromSnapshot();
39
+ if (typeof seed.isActive === "boolean") existing.isActive = seed.isActive;
40
+ return existing;
41
+ }
42
+ const record = em.create(entityClass, { ...seedFromSnapshot(), deletedAt: null });
43
+ em.persist(record);
44
+ return record;
45
+ }
46
+ function makeCreateRedo(config) {
47
+ const getSnapshotId = config.getSnapshotId ?? ((snapshot) => snapshot.id ?? null);
48
+ const seedFromSnapshot = config.seedFromSnapshot ?? ((snapshot) => reviveSnapshotSeed(snapshot, config.dateFields));
49
+ return async ({ ctx, logEntry }) => {
50
+ const snapshot = resolveRedoSnapshot(logEntry);
51
+ const id = snapshot ? getSnapshotId(snapshot) : logEntry.resourceId ?? null;
52
+ if (!snapshot || !id) {
53
+ throw new CrudHttpError(400, { error: "[internal] redo snapshot unavailable for create command" });
54
+ }
55
+ const em = ctx.container.resolve("em").fork();
56
+ const overrides = config.beforeRestore ? await config.beforeRestore({ em, ctx, snapshot }) : void 0;
57
+ const buildSeed = () => overrides ? { ...seedFromSnapshot(snapshot), ...overrides } : seedFromSnapshot(snapshot);
58
+ const findRow = config.findRow ? (forkedEm, rowId) => config.findRow({ em: forkedEm, ctx, id: rowId, snapshot }) : void 0;
59
+ let entity;
60
+ const restorePhase = async () => {
61
+ entity = await restoreCreatedRow(em, config.entityClass, id, buildSeed, findRow);
62
+ };
63
+ const afterRestorePhase = async () => {
64
+ if (config.afterRestore) {
65
+ await config.afterRestore({ em, ctx, entity, snapshot, logEntry });
66
+ }
67
+ };
68
+ try {
69
+ if (config.transaction) {
70
+ const phases = config.afterRestore ? [restorePhase, afterRestorePhase] : [restorePhase];
71
+ await withAtomicFlush(em, phases, { transaction: true });
72
+ } else {
73
+ await restorePhase();
74
+ await em.flush();
75
+ await afterRestorePhase();
76
+ }
77
+ } catch (err) {
78
+ if (isUniqueViolation(err)) {
79
+ throw conflict("[internal] Cannot redo: a record with the same unique key already exists.");
80
+ }
81
+ throw err;
82
+ }
83
+ const dataEngine = ctx.container.resolve("dataEngine");
84
+ await emitCrudSideEffects({
85
+ dataEngine,
86
+ action: "created",
87
+ entity,
88
+ identifiers: {
89
+ id,
90
+ organizationId: entity.organizationId ?? null,
91
+ tenantId: entity.tenantId ?? null
92
+ },
93
+ events: config.events,
94
+ indexer: config.indexer
95
+ });
96
+ return config.buildResult(entity, snapshot);
97
+ };
98
+ }
99
+ export {
100
+ makeCreateRedo,
101
+ resolveRedoSnapshot,
102
+ restoreCreatedRow,
103
+ reviveSnapshotSeed,
104
+ serializeRowSnapshot
105
+ };
106
+ //# sourceMappingURL=redo.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/commands/redo.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandRuntimeContext, CommandUndoLogEntry } from './types'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport { CrudHttpError, conflict, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { extractUndoPayload, type UndoPayload } from './undo'\nimport { emitCrudSideEffects } from './helpers'\nimport { withAtomicFlush } from './flush'\n\ntype EntityClass<T> = abstract new (...args: never[]) => T\n\ntype ScopedSoftDeletable = {\n id: string\n organizationId?: string | null\n tenantId?: string | null\n deletedAt?: Date | null\n isActive?: boolean\n}\n\n/** Snapshot keys revived from ISO strings to `Date` when no explicit `dateFields` is given. */\nconst DEFAULT_SNAPSHOT_DATE_FIELDS = ['createdAt', 'updatedAt', 'deletedAt'] as const\n\n/**\n * Turn an after-snapshot into a create seed by shallow-cloning it and reviving the\n * declared date fields from ISO strings back to `Date`. Single-row snapshots are a\n * faithful serialized row whose keys already equal entity property names, so the\n * snapshot doubles as the seed once dates are revived \u2014 no per-command mapping needed.\n */\nexport function reviveSnapshotSeed(\n snapshot: Record<string, unknown>,\n dateFields: readonly string[] = DEFAULT_SNAPSHOT_DATE_FIELDS,\n): Record<string, unknown> {\n const seed: Record<string, unknown> = { ...snapshot }\n for (const field of dateFields) {\n const value = seed[field]\n if (typeof value === 'string') seed[field] = new Date(value)\n }\n return seed\n}\n\n/**\n * Serialize a persisted row into a plain after-snapshot object: pick `fields` from\n * the entity and convert the declared `dateFields` from `Date` to ISO strings. Use\n * for single-row snapshot loaders that are a clean 1:1 column copy; loaders that\n * shape nested/related data keep their bespoke mapping.\n */\nexport function serializeRowSnapshot<TEntity extends Record<string, unknown>>(\n entity: TEntity,\n fields: readonly string[],\n dateFields: readonly string[] = DEFAULT_SNAPSHOT_DATE_FIELDS,\n): Record<string, unknown> {\n const snapshot: Record<string, unknown> = {}\n const dateFieldSet = new Set(dateFields)\n for (const field of fields) {\n const value = entity[field]\n if (dateFieldSet.has(field)) {\n snapshot[field] = value instanceof Date ? value.toISOString() : (value ?? null)\n } else {\n snapshot[field] = value ?? null\n }\n }\n return snapshot\n}\n\n/**\n * Resolve the after-snapshot a create command persisted for its action log, so a\n * `redo` handler can re-materialize the original record. Reads the `undo.after`\n * snapshot via {@link extractUndoPayload} and falls back to `snapshotAfter`.\n */\nexport function resolveRedoSnapshot<T>(logEntry: CommandUndoLogEntry | null | undefined): T | null {\n if (!logEntry) return null\n const undo = extractUndoPayload<UndoPayload<T>>(logEntry)\n if (undo && undo.after != null) return undo.after as T\n if (logEntry.snapshotAfter != null) return logEntry.snapshotAfter as T\n return null\n}\n\n/**\n * Re-materialize a single row that a create command produced, reusing its original\n * id. If the row still exists (it was soft-deleted by undo), clears `deletedAt` and\n * restores `isActive` from the seed. If it was hard-deleted, re-creates it from the\n * seed (which MUST include the original `id`). This keeps redo idempotent on id so\n * undo/redo snapshots and cross-references stay stable (issue #2506, invariant I6).\n */\nexport async function restoreCreatedRow<TEntity extends ScopedSoftDeletable>(\n em: EntityManager,\n entityClass: EntityClass<TEntity>,\n id: string,\n seedFromSnapshot: () => Record<string, unknown>,\n findRow?: (em: EntityManager, id: string) => Promise<TEntity | null>,\n): Promise<TEntity> {\n const existing = findRow\n ? await findRow(em, id)\n : ((await em.findOne(entityClass as never, { id } as never)) as TEntity | null)\n if (existing) {\n existing.deletedAt = null\n const seed = seedFromSnapshot()\n if (typeof seed.isActive === 'boolean') existing.isActive = seed.isActive\n return existing\n }\n const record = em.create(entityClass as never, { ...seedFromSnapshot(), deletedAt: null } as never) as TEntity\n em.persist(record as never)\n return record\n}\n\nexport type CreateRedoConfig<TEntity extends ScopedSoftDeletable, TSnapshot, TResult> = {\n entityClass: EntityClass<TEntity>\n /**\n * Pulls the original primary id out of the after-snapshot. Defaults to\n * `(snapshot) => snapshot.id`, which fits single-row snapshots whose top-level\n * `id` is the row's primary key. Override only when the id lives elsewhere.\n */\n getSnapshotId?: (snapshot: TSnapshot) => string | null | undefined\n /**\n * Maps the after-snapshot back to a create seed; MUST include the original id.\n * Defaults to {@link reviveSnapshotSeed} \u2014 the snapshot itself with `dateFields`\n * revived to `Date`. Override only when the snapshot keys diverge from entity\n * columns (e.g. nested shapes or derived columns).\n */\n seedFromSnapshot?: (snapshot: TSnapshot) => Record<string, unknown>\n /**\n * Snapshot keys to revive from ISO string to `Date` for the default seed.\n * Defaults to `['createdAt', 'updatedAt', 'deletedAt']`; list additional date\n * columns (e.g. `effectiveAt`, `returnedAt`) when the entity has them.\n */\n dateFields?: readonly string[]\n /** Builds the command result (mirrors `execute`'s return), e.g. `(e) => ({ currencyId: e.id })`. */\n buildResult: (entity: TEntity, snapshot: TSnapshot) => TResult\n events?: CrudEventsConfig<any>\n indexer?: CrudIndexerConfig<any>\n /**\n * Override how the existing row is looked up before restore. Defaults to\n * `em.findOne(entityClass, { id })`. Pass a decryption-aware finder\n * (`findOneWithDecryption`) for encrypted entities so the revive-in-place path\n * sees the same row the rest of the module does.\n */\n findRow?: (args: { em: EntityManager; ctx: CommandRuntimeContext; id: string; snapshot: TSnapshot }) => Promise<TEntity | null>\n /**\n * Runs after the em fork, before the row is restored. Use to validate\n * referenced relations (throw `CrudHttpError` to fail the redo) or resolve\n * relation entities. Anything it returns is shallow-merged into the create\n * seed (e.g. `{ entity: resolvedEntity }`), letting the seed reference a live\n * relation instead of a raw id.\n */\n beforeRestore?: (args: {\n em: EntityManager\n ctx: CommandRuntimeContext\n snapshot: TSnapshot\n }) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void\n /** Optional extra side effects after the row is restored (e.g. query-index upserts, custom-field restore). */\n afterRestore?: (args: {\n em: EntityManager\n ctx: CommandRuntimeContext\n entity: TEntity\n snapshot: TSnapshot\n logEntry: CommandUndoLogEntry\n }) => Promise<void> | void\n /**\n * Wrap the restore (create/revive + flush) in a single transaction via\n * {@link withAtomicFlush}. Use when the create participated in a multi-phase\n * atomic flush in `execute` and partial commits must be impossible.\n */\n transaction?: boolean\n}\n\n/**\n * Build a create command's `redo` handler that restores the original row in place\n * (reusing its id) instead of replaying `execute` and minting a new id. Wires the\n * `created` side effects (events + query index) exactly like `execute`. Use this for\n * single-row create commands; multi-entity creates implement `redo` by hand.\n */\nexport function makeCreateRedo<\n TEntity extends ScopedSoftDeletable,\n TSnapshot,\n TInput = unknown,\n TResult = unknown,\n>(config: CreateRedoConfig<TEntity, TSnapshot, TResult>) {\n const getSnapshotId = config.getSnapshotId ?? ((snapshot: TSnapshot) => (snapshot as { id?: string | null }).id ?? null)\n const seedFromSnapshot =\n config.seedFromSnapshot ?? ((snapshot: TSnapshot) => reviveSnapshotSeed(snapshot as Record<string, unknown>, config.dateFields))\n return async ({ ctx, logEntry }: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<TResult> => {\n const snapshot = resolveRedoSnapshot<TSnapshot>(logEntry)\n const id = snapshot ? getSnapshotId(snapshot) : (logEntry.resourceId ?? null)\n if (!snapshot || !id) {\n throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for create command' })\n }\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const overrides = config.beforeRestore ? await config.beforeRestore({ em, ctx, snapshot }) : undefined\n const buildSeed = () => (overrides ? { ...seedFromSnapshot(snapshot), ...overrides } : seedFromSnapshot(snapshot))\n const findRow = config.findRow\n ? (forkedEm: EntityManager, rowId: string) => config.findRow!({ em: forkedEm, ctx, id: rowId, snapshot })\n : undefined\n let entity!: TEntity\n const restorePhase = async () => {\n entity = await restoreCreatedRow(em, config.entityClass, id, buildSeed, findRow)\n }\n const afterRestorePhase = async () => {\n if (config.afterRestore) {\n await config.afterRestore({ em, ctx, entity, snapshot, logEntry })\n }\n }\n try {\n if (config.transaction) {\n const phases = config.afterRestore ? [restorePhase, afterRestorePhase] : [restorePhase]\n await withAtomicFlush(em, phases, { transaction: true })\n } else {\n await restorePhase()\n await em.flush()\n await afterRestorePhase()\n }\n } catch (err) {\n if (isUniqueViolation(err)) {\n // [internal] prefix: this shared-lib helper has no `t(...)` context; the\n // redo unique-collision is a rare developer-facing edge (the after-snapshot's\n // unique key was re-taken since undo), surfaced via normal error handling.\n throw conflict('[internal] Cannot redo: a record with the same unique key already exists.')\n }\n throw err\n }\n const dataEngine = ctx.container.resolve('dataEngine') as DataEngine\n await emitCrudSideEffects({\n dataEngine,\n action: 'created',\n entity,\n identifiers: {\n id,\n organizationId: entity.organizationId ?? null,\n tenantId: entity.tenantId ?? null,\n },\n events: config.events,\n indexer: config.indexer,\n })\n return config.buildResult(entity, snapshot)\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/commands/types.ts"],
4
- "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { randomUUID } from 'crypto'\nimport type { AuthContext } from '../auth/server'\nimport type { OrganizationScope } from '@open-mercato/core/modules/directory/utils/organizationScope'\n\nexport type CommandRuntimeContext = {\n container: AwilixContainer\n auth: AuthContext | null\n organizationScope: OrganizationScope | null\n selectedOrganizationId: string | null\n organizationIds: string[] | null\n request?: Request\n syncOrigin?: string | null\n /**\n * Marks a trusted server-side invocation (CLI seeding, tenant setup) that runs\n * without an authenticated end-user actor. Commands that gate writes behind a\n * privileged actor (e.g. super-admin-only platform tables) may treat this as\n * an explicit system grant. HTTP request paths MUST NOT set this \u2014 they always\n * carry a real `auth` actor, so a present-but-unprivileged actor stays denied.\n */\n systemActor?: boolean\n /**\n * When set, command handlers that support it MUST run their writes within this\n * existing transactional EntityManager (reusing its row locks) instead of\n * opening their own transaction. Lets a caller compose a command with its own\n * surrounding work as a single atomic, single-locked operation.\n */\n transactionalEm?: EntityManager\n}\n\nexport type CommandLogMetadata = {\n skipLog?: boolean\n tenantId?: string | null\n organizationId?: string | null\n actorUserId?: string | null\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n parentResourceKind?: string | null\n parentResourceId?: string | null\n undoToken?: string | null\n payload?: unknown\n snapshotBefore?: unknown\n snapshotAfter?: unknown\n relatedResourceKind?: string | null\n relatedResourceId?: string | null\n changes?: Record<string, unknown> | null\n context?: Record<string, unknown> | null\n}\n\nexport type CommandExecuteResult<TResult> = {\n result: TResult\n logEntry: any | null\n}\n\n/**\n * Shape of the persisted action log handed to a command's `undo()` handler.\n *\n * IMPORTANT: there is intentionally **no `payload` field**. `buildLog()` returns\n * a `payload` in its metadata, but the command bus persists that under\n * `commandPayload` (column `command_payload`, wrapped in a redo envelope) \u2014 the\n * stored row never has a top-level `payload`. Reading `logEntry.payload` in an\n * undo handler is therefore always `undefined` and silently no-ops the undo\n * (issue #2504). Always read the undo snapshot through\n * `extractUndoPayload(logEntry)` from `@open-mercato/shared/lib/commands/undo`,\n * which unwraps `commandPayload`/snapshots correctly. Omitting `payload` here\n * makes the footgun a compile-time error instead of a runtime silent failure.\n */\nexport type CommandUndoLogEntry = {\n id?: string\n commandId?: string\n commandPayload?: unknown | null\n snapshotBefore?: unknown | null\n snapshotAfter?: unknown | null\n resourceKind?: string | null\n resourceId?: string | null\n undoToken?: string | null\n actionLabel?: string | null\n tenantId?: string | null\n organizationId?: string | null\n actorUserId?: string | null\n changesJson?: Record<string, unknown> | null\n contextJson?: Record<string, unknown> | null\n createdAt?: Date | string\n updatedAt?: Date | string\n}\n\nexport type CommandLogBuilderArgs<TInput, TResult> = {\n input: TInput\n result: TResult\n ctx: CommandRuntimeContext\n snapshots: {\n before?: unknown\n after?: unknown\n }\n}\n\nexport interface CommandHandler<TInput = unknown, TResult = unknown> {\n readonly id: string\n readonly isUndoable?: boolean\n prepare?(input: TInput, ctx: CommandRuntimeContext): Promise<{ before?: unknown } | null> | { before?: unknown } | null\n execute(input: TInput, ctx: CommandRuntimeContext): Promise<TResult> | TResult\n buildLog?(args: CommandLogBuilderArgs<TInput, TResult>): Promise<CommandLogMetadata | null | undefined> | CommandLogMetadata | null | undefined\n captureAfter?(input: TInput, result: TResult, ctx: CommandRuntimeContext): Promise<unknown> | unknown\n undo?(params: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<void> | void\n}\n\nexport type CommandExecutionOptions<TInput> = {\n input: TInput\n ctx: CommandRuntimeContext\n metadata?: CommandLogMetadata | null\n skipCacheInvalidation?: boolean\n}\n\nexport function defaultUndoToken(): string {\n return randomUUID()\n}\n"],
5
- "mappings": "AAEA,SAAS,kBAAkB;AAiHpB,SAAS,mBAA2B;AACzC,SAAO,WAAW;AACpB;",
4
+ "sourcesContent": ["import type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { randomUUID } from 'crypto'\nimport type { AuthContext } from '../auth/server'\nimport type { OrganizationScope } from '@open-mercato/core/modules/directory/utils/organizationScope'\n\nexport type CommandRuntimeContext = {\n container: AwilixContainer\n auth: AuthContext | null\n organizationScope: OrganizationScope | null\n selectedOrganizationId: string | null\n organizationIds: string[] | null\n request?: Request\n syncOrigin?: string | null\n /**\n * Marks a trusted server-side invocation (CLI seeding, tenant setup) that runs\n * without an authenticated end-user actor. Commands that gate writes behind a\n * privileged actor (e.g. super-admin-only platform tables) may treat this as\n * an explicit system grant. HTTP request paths MUST NOT set this \u2014 they always\n * carry a real `auth` actor, so a present-but-unprivileged actor stays denied.\n */\n systemActor?: boolean\n /**\n * When set, command handlers that support it MUST run their writes within this\n * existing transactional EntityManager (reusing its row locks) instead of\n * opening their own transaction. Lets a caller compose a command with its own\n * surrounding work as a single atomic, single-locked operation.\n */\n transactionalEm?: EntityManager\n}\n\nexport type CommandLogMetadata = {\n skipLog?: boolean\n tenantId?: string | null\n organizationId?: string | null\n actorUserId?: string | null\n actionLabel?: string | null\n resourceKind?: string | null\n resourceId?: string | null\n parentResourceKind?: string | null\n parentResourceId?: string | null\n undoToken?: string | null\n payload?: unknown\n snapshotBefore?: unknown\n snapshotAfter?: unknown\n relatedResourceKind?: string | null\n relatedResourceId?: string | null\n changes?: Record<string, unknown> | null\n context?: Record<string, unknown> | null\n}\n\nexport type CommandExecuteResult<TResult> = {\n result: TResult\n logEntry: any | null\n}\n\n/**\n * Shape of the persisted action log handed to a command's `undo()` handler.\n *\n * IMPORTANT: there is intentionally **no `payload` field**. `buildLog()` returns\n * a `payload` in its metadata, but the command bus persists that under\n * `commandPayload` (column `command_payload`, wrapped in a redo envelope) \u2014 the\n * stored row never has a top-level `payload`. Reading `logEntry.payload` in an\n * undo handler is therefore always `undefined` and silently no-ops the undo\n * (issue #2504). Always read the undo snapshot through\n * `extractUndoPayload(logEntry)` from `@open-mercato/shared/lib/commands/undo`,\n * which unwraps `commandPayload`/snapshots correctly. Omitting `payload` here\n * makes the footgun a compile-time error instead of a runtime silent failure.\n */\nexport type CommandUndoLogEntry = {\n id?: string\n commandId?: string\n commandPayload?: unknown | null\n snapshotBefore?: unknown | null\n snapshotAfter?: unknown | null\n resourceKind?: string | null\n resourceId?: string | null\n undoToken?: string | null\n actionLabel?: string | null\n tenantId?: string | null\n organizationId?: string | null\n actorUserId?: string | null\n changesJson?: Record<string, unknown> | null\n contextJson?: Record<string, unknown> | null\n createdAt?: Date | string\n updatedAt?: Date | string\n}\n\nexport type CommandLogBuilderArgs<TInput, TResult> = {\n input: TInput\n result: TResult\n ctx: CommandRuntimeContext\n snapshots: {\n before?: unknown\n after?: unknown\n }\n}\n\nexport interface CommandHandler<TInput = unknown, TResult = unknown> {\n readonly id: string\n readonly isUndoable?: boolean\n prepare?(input: TInput, ctx: CommandRuntimeContext): Promise<{ before?: unknown } | null> | { before?: unknown } | null\n execute(input: TInput, ctx: CommandRuntimeContext): Promise<TResult> | TResult\n buildLog?(args: CommandLogBuilderArgs<TInput, TResult>): Promise<CommandLogMetadata | null | undefined> | CommandLogMetadata | null | undefined\n captureAfter?(input: TInput, result: TResult, ctx: CommandRuntimeContext): Promise<unknown> | unknown\n undo?(params: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<void> | void\n /**\n * Optional redo handler. When defined, the command bus calls this instead of\n * `execute()` while replaying a previously undone action (the redo route passes\n * `redoLogEntry` in the execution options). It receives the source action log so\n * it can re-materialize the original record **reusing its id** \u2014 for a create\n * command this restores the soft-deleted row (or re-creates it from the\n * `snapshotAfter`) instead of minting a new id, keeping undo/redo snapshots and\n * references stable (issue #2506, invariant I6). Handlers without `redo` keep the\n * legacy behavior of replaying `execute(__redoInput)`.\n */\n redo?(params: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<TResult> | TResult\n}\n\nexport type CommandExecutionOptions<TInput> = {\n input: TInput\n ctx: CommandRuntimeContext\n metadata?: CommandLogMetadata | null\n skipCacheInvalidation?: boolean\n /**\n * When set, marks this execution as a redo of a previously undone action. If the\n * resolved handler defines a `redo` method, the command bus calls\n * `handler.redo({ input, ctx, logEntry })` instead of `handler.execute(...)`. The\n * rest of the pipeline (snapshots, buildLog, undo-token minting, persistence,\n * cache invalidation, side effects) is identical, so the fresh log entry \u2014 and\n * the `x-om-operation` header derived from it \u2014 automatically carry the restored\n * resourceId. Ignored when the handler has no `redo` method (legacy replay path).\n */\n redoLogEntry?: CommandUndoLogEntry | null\n}\n\nexport function defaultUndoToken(): string {\n return randomUUID()\n}\n"],
5
+ "mappings": "AAEA,SAAS,kBAAkB;AAsIpB,SAAS,mBAA2B;AACzC,SAAO,WAAW;AACpB;",
6
6
  "names": []
7
7
  }
@@ -21,6 +21,26 @@ function forbidden(message = "Forbidden") {
21
21
  function notFound(message = "Not found") {
22
22
  return new CrudHttpError(404, { error: message });
23
23
  }
24
+ function conflict(message) {
25
+ return new CrudHttpError(409, { error: message });
26
+ }
27
+ const POSTGRES_UNIQUE_VIOLATION = "23505";
28
+ function isUniqueViolation(err, constraintName) {
29
+ if (!err || typeof err !== "object") return false;
30
+ const candidates = [err, err.cause, err.previous];
31
+ for (const candidate of candidates) {
32
+ if (!candidate || typeof candidate !== "object") continue;
33
+ const record = candidate;
34
+ if (record.code === POSTGRES_UNIQUE_VIOLATION) {
35
+ if (!constraintName) return true;
36
+ const constraint = typeof record.constraint === "string" ? record.constraint : "";
37
+ const detail = typeof record.detail === "string" ? record.detail : "";
38
+ const message = typeof record.message === "string" ? record.message : "";
39
+ return constraint === constraintName || detail.includes(constraintName) || message.includes(constraintName);
40
+ }
41
+ }
42
+ return false;
43
+ }
24
44
  function assertFound(value, message) {
25
45
  if (!value) throw notFound(message);
26
46
  return value;
@@ -29,8 +49,10 @@ export {
29
49
  CrudHttpError,
30
50
  assertFound,
31
51
  badRequest,
52
+ conflict,
32
53
  forbidden,
33
54
  isCrudHttpError,
55
+ isUniqueViolation,
34
56
  notFound
35
57
  };
36
58
  //# sourceMappingURL=errors.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/crud/errors.ts"],
4
- "sourcesContent": ["// Use Symbol.for so the marker survives module duplication across bundle boundaries\n// (same behaviour as globalThis-based registries used for DI registrars)\nconst CRUD_HTTP_ERROR_MARKER = Symbol.for('@open-mercato/CrudHttpError')\n\nexport class CrudHttpError extends Error {\n readonly [CRUD_HTTP_ERROR_MARKER] = true\n status: number\n body: Record<string, any>\n\n constructor(\n status: number,\n body?: Record<string, any> | string,\n options?: { cause?: unknown },\n ) {\n const normalizedBody = typeof body === 'string' ? { error: body } : body ?? {}\n super(typeof body === 'string' ? body : normalizedBody.error ?? 'Request failed', options)\n this.status = status\n this.body = normalizedBody\n }\n}\n\n/**\n * Type-safe check for CrudHttpError that works across module/bundle boundaries.\n * Prefer this over `instanceof CrudHttpError` whenever the error may originate\n * from a different module bundle (e.g. enterprise packages, dynamic imports).\n */\nexport function isCrudHttpError(err: unknown): err is CrudHttpError {\n return !!err && typeof err === 'object' && (err as Record<symbol, unknown>)[CRUD_HTTP_ERROR_MARKER] === true\n}\n\nexport function badRequest(message: string): CrudHttpError {\n return new CrudHttpError(400, { error: message })\n}\n\nexport function forbidden(message = 'Forbidden'): CrudHttpError {\n return new CrudHttpError(403, { error: message })\n}\n\nexport function notFound(message = 'Not found'): CrudHttpError {\n return new CrudHttpError(404, { error: message })\n}\n\nexport function assertFound<T>(value: T | null | undefined, message: string): T {\n if (!value) throw notFound(message)\n return value\n}\n"],
5
- "mappings": "AAAA;AAEA,MAAM,yBAAyB,uBAAO,IAAI,6BAA6B;AAEhE,MAAM,uBAAsB,YACvB,6BADuB,IAAM;AAAA,EAKvC,YACE,QACA,MACA,SACA;AACA,UAAM,iBAAiB,OAAO,SAAS,WAAW,EAAE,OAAO,KAAK,IAAI,QAAQ,CAAC;AAC7E,UAAM,OAAO,SAAS,WAAW,OAAO,eAAe,SAAS,kBAAkB,OAAO;AAV3F,SAAU,MAA0B;AAWlC,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AACF;AAOO,SAAS,gBAAgB,KAAoC;AAClE,SAAO,CAAC,CAAC,OAAO,OAAO,QAAQ,YAAa,IAAgC,sBAAsB,MAAM;AAC1G;AAEO,SAAS,WAAW,SAAgC;AACzD,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,CAAC;AAClD;AAEO,SAAS,UAAU,UAAU,aAA4B;AAC9D,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,CAAC;AAClD;AAEO,SAAS,SAAS,UAAU,aAA4B;AAC7D,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,CAAC;AAClD;AAEO,SAAS,YAAe,OAA6B,SAAoB;AAC9E,MAAI,CAAC,MAAO,OAAM,SAAS,OAAO;AAClC,SAAO;AACT;",
4
+ "sourcesContent": ["// Use Symbol.for so the marker survives module duplication across bundle boundaries\n// (same behaviour as globalThis-based registries used for DI registrars)\nconst CRUD_HTTP_ERROR_MARKER = Symbol.for('@open-mercato/CrudHttpError')\n\nexport class CrudHttpError extends Error {\n readonly [CRUD_HTTP_ERROR_MARKER] = true\n status: number\n body: Record<string, any>\n\n constructor(\n status: number,\n body?: Record<string, any> | string,\n options?: { cause?: unknown },\n ) {\n const normalizedBody = typeof body === 'string' ? { error: body } : body ?? {}\n super(typeof body === 'string' ? body : normalizedBody.error ?? 'Request failed', options)\n this.status = status\n this.body = normalizedBody\n }\n}\n\n/**\n * Type-safe check for CrudHttpError that works across module/bundle boundaries.\n * Prefer this over `instanceof CrudHttpError` whenever the error may originate\n * from a different module bundle (e.g. enterprise packages, dynamic imports).\n */\nexport function isCrudHttpError(err: unknown): err is CrudHttpError {\n return !!err && typeof err === 'object' && (err as Record<symbol, unknown>)[CRUD_HTTP_ERROR_MARKER] === true\n}\n\nexport function badRequest(message: string): CrudHttpError {\n return new CrudHttpError(400, { error: message })\n}\n\nexport function forbidden(message = 'Forbidden'): CrudHttpError {\n return new CrudHttpError(403, { error: message })\n}\n\nexport function notFound(message = 'Not found'): CrudHttpError {\n return new CrudHttpError(404, { error: message })\n}\n\nexport function conflict(message: string): CrudHttpError {\n return new CrudHttpError(409, { error: message })\n}\n\nconst POSTGRES_UNIQUE_VIOLATION = '23505'\n\n/**\n * Detects a Postgres unique-constraint violation (SQLSTATE 23505) on a thrown\n * error, looking through MikroORM's driver-error wrapping. Use this to map a DB\n * uniqueness race (e.g. a soft-deleted row the in-app check missed, or a\n * timestamp-precision mismatch) onto a clean 409 instead of a generic 500.\n */\nexport function isUniqueViolation(err: unknown, constraintName?: string): boolean {\n if (!err || typeof err !== 'object') return false\n const candidates: unknown[] = [err, (err as { cause?: unknown }).cause, (err as { previous?: unknown }).previous]\n for (const candidate of candidates) {\n if (!candidate || typeof candidate !== 'object') continue\n const record = candidate as Record<string, unknown>\n if (record.code === POSTGRES_UNIQUE_VIOLATION) {\n if (!constraintName) return true\n const constraint = typeof record.constraint === 'string' ? record.constraint : ''\n const detail = typeof record.detail === 'string' ? record.detail : ''\n const message = typeof record.message === 'string' ? record.message : ''\n return constraint === constraintName || detail.includes(constraintName) || message.includes(constraintName)\n }\n }\n return false\n}\n\nexport function assertFound<T>(value: T | null | undefined, message: string): T {\n if (!value) throw notFound(message)\n return value\n}\n"],
5
+ "mappings": "AAAA;AAEA,MAAM,yBAAyB,uBAAO,IAAI,6BAA6B;AAEhE,MAAM,uBAAsB,YACvB,6BADuB,IAAM;AAAA,EAKvC,YACE,QACA,MACA,SACA;AACA,UAAM,iBAAiB,OAAO,SAAS,WAAW,EAAE,OAAO,KAAK,IAAI,QAAQ,CAAC;AAC7E,UAAM,OAAO,SAAS,WAAW,OAAO,eAAe,SAAS,kBAAkB,OAAO;AAV3F,SAAU,MAA0B;AAWlC,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AACF;AAOO,SAAS,gBAAgB,KAAoC;AAClE,SAAO,CAAC,CAAC,OAAO,OAAO,QAAQ,YAAa,IAAgC,sBAAsB,MAAM;AAC1G;AAEO,SAAS,WAAW,SAAgC;AACzD,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,CAAC;AAClD;AAEO,SAAS,UAAU,UAAU,aAA4B;AAC9D,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,CAAC;AAClD;AAEO,SAAS,SAAS,UAAU,aAA4B;AAC7D,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,CAAC;AAClD;AAEO,SAAS,SAAS,SAAgC;AACvD,SAAO,IAAI,cAAc,KAAK,EAAE,OAAO,QAAQ,CAAC;AAClD;AAEA,MAAM,4BAA4B;AAQ3B,SAAS,kBAAkB,KAAc,gBAAkC;AAChF,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,aAAwB,CAAC,KAAM,IAA4B,OAAQ,IAA+B,QAAQ;AAChH,aAAW,aAAa,YAAY;AAClC,QAAI,CAAC,aAAa,OAAO,cAAc,SAAU;AACjD,UAAM,SAAS;AACf,QAAI,OAAO,SAAS,2BAA2B;AAC7C,UAAI,CAAC,eAAgB,QAAO;AAC5B,YAAM,aAAa,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;AAC/E,YAAM,SAAS,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AACnE,YAAM,UAAU,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU;AACtE,aAAO,eAAe,kBAAkB,OAAO,SAAS,cAAc,KAAK,QAAQ,SAAS,cAAc;AAAA,IAC5G;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,YAAe,OAA6B,SAAoB;AAC9E,MAAI,CAAC,MAAO,OAAM,SAAS,OAAO;AAClC,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.5-develop.4718.1.56d834bb34";
1
+ const APP_VERSION = "0.6.5-develop.4732.1.732f6c67a5";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.5-develop.4718.1.56d834bb34'\nexport const appVersion = APP_VERSION\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.5-develop.4732.1.732f6c67a5'\nexport const appVersion = APP_VERSION\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.5-develop.4718.1.56d834bb34",
3
+ "version": "0.6.5-develop.4732.1.732f6c67a5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -92,7 +92,7 @@
92
92
  "@mikro-orm/core": "^7.1.3",
93
93
  "@mikro-orm/decorators": "^7.1.3",
94
94
  "@mikro-orm/postgresql": "^7.1.3",
95
- "@open-mercato/cache": "0.6.5-develop.4718.1.56d834bb34",
95
+ "@open-mercato/cache": "0.6.5-develop.4732.1.732f6c67a5",
96
96
  "dotenv": "^17.4.2",
97
97
  "rate-limiter-flexible": "^11.1.0",
98
98
  "re2js": "2.8.3",
@@ -0,0 +1,265 @@
1
+ import { reviveSnapshotSeed, serializeRowSnapshot, makeCreateRedo } from '../redo'
2
+
3
+ describe('reviveSnapshotSeed', () => {
4
+ it('revives the default date fields from ISO strings to Date', () => {
5
+ const seed = reviveSnapshotSeed({
6
+ id: 'row-1',
7
+ code: 'USD',
8
+ createdAt: '2026-01-02T03:04:05.000Z',
9
+ updatedAt: '2026-01-02T03:04:05.000Z',
10
+ deletedAt: null,
11
+ })
12
+ expect(seed.id).toBe('row-1')
13
+ expect(seed.code).toBe('USD')
14
+ expect(seed.createdAt).toBeInstanceOf(Date)
15
+ expect((seed.createdAt as Date).toISOString()).toBe('2026-01-02T03:04:05.000Z')
16
+ expect(seed.updatedAt).toBeInstanceOf(Date)
17
+ expect(seed.deletedAt).toBeNull()
18
+ })
19
+
20
+ it('revives explicitly declared extra date fields', () => {
21
+ const seed = reviveSnapshotSeed(
22
+ { id: 'row-1', effectiveAt: '2026-02-03T00:00:00.000Z', updatedAt: '2026-02-03T00:00:00.000Z' },
23
+ ['createdAt', 'updatedAt', 'deletedAt', 'effectiveAt'],
24
+ )
25
+ expect(seed.effectiveAt).toBeInstanceOf(Date)
26
+ expect(seed.updatedAt).toBeInstanceOf(Date)
27
+ })
28
+
29
+ it('leaves non-date values untouched and clones the input', () => {
30
+ const snapshot = { id: 'row-1', symbol: null, decimalPlaces: 2 }
31
+ const seed = reviveSnapshotSeed(snapshot)
32
+ expect(seed).not.toBe(snapshot)
33
+ expect(seed.symbol).toBeNull()
34
+ expect(seed.decimalPlaces).toBe(2)
35
+ })
36
+ })
37
+
38
+ describe('serializeRowSnapshot', () => {
39
+ it('picks the requested fields and converts dates to ISO strings', () => {
40
+ const entity = {
41
+ id: 'row-1',
42
+ code: 'USD',
43
+ symbol: undefined as unknown as string,
44
+ createdAt: new Date('2026-01-02T03:04:05.000Z'),
45
+ updatedAt: new Date('2026-01-02T03:04:05.000Z'),
46
+ ignored: 'nope',
47
+ }
48
+ const snapshot = serializeRowSnapshot(entity, ['id', 'code', 'symbol', 'createdAt', 'updatedAt'])
49
+ expect(snapshot).toEqual({
50
+ id: 'row-1',
51
+ code: 'USD',
52
+ symbol: null,
53
+ createdAt: '2026-01-02T03:04:05.000Z',
54
+ updatedAt: '2026-01-02T03:04:05.000Z',
55
+ })
56
+ })
57
+ })
58
+
59
+ describe('makeCreateRedo defaults', () => {
60
+ function buildContext(em: { fork: () => unknown }) {
61
+ const dataEngine = { markOrmEntityChange: () => undefined }
62
+ return {
63
+ container: {
64
+ resolve: (name: string) => (name === 'em' ? em : dataEngine),
65
+ },
66
+ } as never
67
+ }
68
+
69
+ it('uses the snapshot as the seed (with date revival) when no seedFromSnapshot is given', async () => {
70
+ const created: Array<Record<string, unknown>> = []
71
+ const forked = {
72
+ findOne: async () => null,
73
+ create: (_cls: unknown, data: Record<string, unknown>) => {
74
+ created.push(data)
75
+ return { ...data }
76
+ },
77
+ persist: () => undefined,
78
+ flush: async () => undefined,
79
+ }
80
+ const em = { fork: () => forked }
81
+ const redo = makeCreateRedo<{ id: string; organizationId?: string | null; tenantId?: string | null }, { id: string; code: string; createdAt: string; updatedAt: string }>({
82
+ entityClass: class {} as never,
83
+ buildResult: (entity) => ({ id: entity.id }),
84
+ })
85
+ const logEntry = {
86
+ snapshotAfter: { id: 'row-1', code: 'USD', createdAt: '2026-01-02T03:04:05.000Z', updatedAt: '2026-01-02T03:04:05.000Z' },
87
+ }
88
+ const result = await redo({ input: {}, ctx: buildContext(em), logEntry: logEntry as never })
89
+ expect(result).toEqual({ id: 'row-1' })
90
+ expect(created).toHaveLength(1)
91
+ expect(created[0].id).toBe('row-1')
92
+ expect(created[0].code).toBe('USD')
93
+ expect(created[0].createdAt).toBeInstanceOf(Date)
94
+ })
95
+
96
+ it('merges beforeRestore overrides into the create seed and runs before restore', async () => {
97
+ const created: Array<Record<string, unknown>> = []
98
+ const calls: string[] = []
99
+ const forked = {
100
+ findOne: async () => { calls.push('find'); return null },
101
+ create: (_cls: unknown, data: Record<string, unknown>) => { created.push(data); return { ...data } },
102
+ persist: () => undefined,
103
+ flush: async () => undefined,
104
+ }
105
+ const em = { fork: () => forked }
106
+ const resolvedRelation = { id: 'rel-1' }
107
+ const redo = makeCreateRedo<{ id: string }, { id: string; relationId: string }>({
108
+ entityClass: class {} as never,
109
+ buildResult: (entity) => ({ id: entity.id }),
110
+ beforeRestore: async ({ snapshot }) => { calls.push('before'); return { relation: resolvedRelation, relationId: undefined } },
111
+ })
112
+ const logEntry = { snapshotAfter: { id: 'row-1', relationId: 'rel-1' } }
113
+ await redo({ input: {}, ctx: buildContext(em), logEntry: logEntry as never })
114
+ expect(calls).toEqual(['before', 'find'])
115
+ expect(created[0].relation).toBe(resolvedRelation)
116
+ expect(created[0].relationId).toBeUndefined()
117
+ })
118
+
119
+ it('uses the findRow override instead of em.findOne', async () => {
120
+ const surviving: Record<string, unknown> = { id: 'row-1', deletedAt: new Date() }
121
+ let findRowCalled = false
122
+ const forked = {
123
+ findOne: async () => { throw new Error('default findOne must not run when findRow is set') },
124
+ create: () => { throw new Error('should not create when findRow returns a row') },
125
+ persist: () => undefined,
126
+ flush: async () => undefined,
127
+ }
128
+ const em = { fork: () => forked }
129
+ const redo = makeCreateRedo<{ id: string; deletedAt?: Date | null }, { id: string }>({
130
+ entityClass: class {} as never,
131
+ buildResult: (entity) => ({ id: entity.id }),
132
+ findRow: async ({ id }) => { findRowCalled = true; return id === 'row-1' ? (surviving as never) : null },
133
+ })
134
+ await redo({ input: {}, ctx: buildContext(em), logEntry: { snapshotAfter: { id: 'row-1' } } as never })
135
+ expect(findRowCalled).toBe(true)
136
+ expect(surviving.deletedAt).toBeNull()
137
+ })
138
+
139
+ it('passes logEntry to afterRestore', async () => {
140
+ let seenLogEntry: unknown = null
141
+ const forked = {
142
+ findOne: async () => null,
143
+ create: (_cls: unknown, data: Record<string, unknown>) => ({ ...data }),
144
+ persist: () => undefined,
145
+ flush: async () => undefined,
146
+ }
147
+ const em = { fork: () => forked }
148
+ const redo = makeCreateRedo<{ id: string }, { id: string }>({
149
+ entityClass: class {} as never,
150
+ buildResult: (entity) => ({ id: entity.id }),
151
+ afterRestore: async ({ logEntry }) => { seenLogEntry = logEntry },
152
+ })
153
+ const logEntry = { snapshotAfter: { id: 'row-1' }, resourceId: 'row-1' }
154
+ await redo({ input: {}, ctx: buildContext(em), logEntry: logEntry as never })
155
+ expect(seenLogEntry).toBe(logEntry)
156
+ })
157
+
158
+ it('wraps the restore in a transaction when transaction is true', async () => {
159
+ const order: string[] = []
160
+ const forked = {
161
+ isInTransaction: () => false,
162
+ begin: async () => { order.push('begin') },
163
+ commit: async () => { order.push('commit') },
164
+ rollback: async () => { order.push('rollback') },
165
+ getUnitOfWork: () => ({ getChangeSets: () => [] }),
166
+ findOne: async () => null,
167
+ create: (_cls: unknown, data: Record<string, unknown>) => { order.push('create'); return { ...data } },
168
+ persist: () => undefined,
169
+ flush: async () => { order.push('flush') },
170
+ }
171
+ const em = { fork: () => forked }
172
+ const redo = makeCreateRedo<{ id: string }, { id: string }>({
173
+ entityClass: class {} as never,
174
+ buildResult: (entity) => ({ id: entity.id }),
175
+ transaction: true,
176
+ })
177
+ await redo({ input: {}, ctx: buildContext(em), logEntry: { snapshotAfter: { id: 'row-1' } } as never })
178
+ expect(order[0]).toBe('begin')
179
+ expect(order).toContain('create')
180
+ expect(order[order.length - 1]).toBe('commit')
181
+ expect(order).not.toContain('rollback')
182
+ })
183
+
184
+ it('runs afterRestore inside the transaction before commit when transaction is true', async () => {
185
+ const order: string[] = []
186
+ const forked = {
187
+ isInTransaction: () => false,
188
+ begin: async () => { order.push('begin') },
189
+ commit: async () => { order.push('commit') },
190
+ rollback: async () => { order.push('rollback') },
191
+ getUnitOfWork: () => ({ getChangeSets: () => [] }),
192
+ findOne: async () => null,
193
+ create: (_cls: unknown, data: Record<string, unknown>) => { order.push('create'); return { ...data } },
194
+ persist: () => undefined,
195
+ flush: async () => { order.push('flush') },
196
+ }
197
+ const em = { fork: () => forked }
198
+ const redo = makeCreateRedo<{ id: string }, { id: string }>({
199
+ entityClass: class {} as never,
200
+ buildResult: (entity) => ({ id: entity.id }),
201
+ transaction: true,
202
+ afterRestore: async () => { order.push('afterRestore') },
203
+ })
204
+ await redo({ input: {}, ctx: buildContext(em), logEntry: { snapshotAfter: { id: 'row-1' } } as never })
205
+ expect(order).toEqual(['begin', 'create', 'flush', 'afterRestore', 'flush', 'commit'])
206
+ })
207
+
208
+ it('maps a Postgres unique-constraint violation thrown during flush to a 409 conflict', async () => {
209
+ const uniqueError = Object.assign(new Error('duplicate key value violates unique constraint'), { code: '23505' })
210
+ const forked = {
211
+ findOne: async () => null,
212
+ create: (_cls: unknown, data: Record<string, unknown>) => ({ ...data }),
213
+ persist: () => undefined,
214
+ flush: async () => { throw uniqueError },
215
+ }
216
+ const em = { fork: () => forked }
217
+ const redo = makeCreateRedo<{ id: string }, { id: string }>({
218
+ entityClass: class {} as never,
219
+ buildResult: (entity) => ({ id: entity.id }),
220
+ })
221
+ const logEntry = { snapshotAfter: { id: 'row-1' } }
222
+ await expect(redo({ input: {}, ctx: buildContext(em), logEntry: logEntry as never })).rejects.toMatchObject({
223
+ status: 409,
224
+ })
225
+ })
226
+
227
+ it('propagates a non-unique flush error unchanged', async () => {
228
+ const otherError = Object.assign(new Error('some other failure'), { code: '23503' })
229
+ const forked = {
230
+ findOne: async () => null,
231
+ create: (_cls: unknown, data: Record<string, unknown>) => ({ ...data }),
232
+ persist: () => undefined,
233
+ flush: async () => { throw otherError },
234
+ }
235
+ const em = { fork: () => forked }
236
+ const redo = makeCreateRedo<{ id: string }, { id: string }>({
237
+ entityClass: class {} as never,
238
+ buildResult: (entity) => ({ id: entity.id }),
239
+ })
240
+ const logEntry = { snapshotAfter: { id: 'row-1' } }
241
+ await expect(redo({ input: {}, ctx: buildContext(em), logEntry: logEntry as never })).rejects.toBe(otherError)
242
+ })
243
+
244
+ it('defaults getSnapshotId to snapshot.id and restores a surviving row in place', async () => {
245
+ const surviving: Record<string, unknown> = { id: 'row-1', deletedAt: new Date(), isActive: false }
246
+ const forked = {
247
+ findOne: async () => surviving,
248
+ create: () => {
249
+ throw new Error('should not create when row survives')
250
+ },
251
+ persist: () => undefined,
252
+ flush: async () => undefined,
253
+ }
254
+ const em = { fork: () => forked }
255
+ const redo = makeCreateRedo<{ id: string; deletedAt?: Date | null; isActive?: boolean }, { id: string; isActive: boolean }>({
256
+ entityClass: class {} as never,
257
+ buildResult: (entity) => ({ id: entity.id, isActive: entity.isActive }),
258
+ })
259
+ const logEntry = { snapshotAfter: { id: 'row-1', isActive: true } }
260
+ const result = await redo({ input: {}, ctx: buildContext(em), logEntry: logEntry as never })
261
+ expect(result).toEqual({ id: 'row-1', isActive: true })
262
+ expect(surviving.deletedAt).toBeNull()
263
+ expect(surviving.isActive).toBe(true)
264
+ })
265
+ })
@@ -230,7 +230,11 @@ export class CommandBus {
230
230
  }
231
231
 
232
232
  const snapshots = await this.prepareSnapshots(handler, effectiveOptions)
233
- const result = await handler.execute(effectiveOptions.input, effectiveOptions.ctx)
233
+ const redoLogEntry = effectiveOptions.redoLogEntry ?? null
234
+ const result =
235
+ redoLogEntry && typeof handler.redo === 'function'
236
+ ? await handler.redo({ input: effectiveOptions.input, ctx: effectiveOptions.ctx, logEntry: redoLogEntry })
237
+ : await handler.execute(effectiveOptions.input, effectiveOptions.ctx)
234
238
  const afterSnapshot = await this.captureAfter(handler, effectiveOptions, result)
235
239
  const snapshotsWithAfter = { ...snapshots, after: afterSnapshot }
236
240
  const logMeta = await this.buildLog(handler, effectiveOptions, result, snapshotsWithAfter)
@@ -3,6 +3,7 @@ export * from './registry'
3
3
  export { CommandBus } from './command-bus'
4
4
  export * from './customFieldSnapshots'
5
5
  export * from './undo'
6
+ export * from './redo'
6
7
  export { CommandInterceptorError } from './errors'
7
8
  export {
8
9
  runCrudCommandWrite,
@@ -0,0 +1,235 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import type { CommandRuntimeContext, CommandUndoLogEntry } from './types'
3
+ import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
4
+ import type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
5
+ import { CrudHttpError, conflict, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
6
+ import { extractUndoPayload, type UndoPayload } from './undo'
7
+ import { emitCrudSideEffects } from './helpers'
8
+ import { withAtomicFlush } from './flush'
9
+
10
+ type EntityClass<T> = abstract new (...args: never[]) => T
11
+
12
+ type ScopedSoftDeletable = {
13
+ id: string
14
+ organizationId?: string | null
15
+ tenantId?: string | null
16
+ deletedAt?: Date | null
17
+ isActive?: boolean
18
+ }
19
+
20
+ /** Snapshot keys revived from ISO strings to `Date` when no explicit `dateFields` is given. */
21
+ const DEFAULT_SNAPSHOT_DATE_FIELDS = ['createdAt', 'updatedAt', 'deletedAt'] as const
22
+
23
+ /**
24
+ * Turn an after-snapshot into a create seed by shallow-cloning it and reviving the
25
+ * declared date fields from ISO strings back to `Date`. Single-row snapshots are a
26
+ * faithful serialized row whose keys already equal entity property names, so the
27
+ * snapshot doubles as the seed once dates are revived — no per-command mapping needed.
28
+ */
29
+ export function reviveSnapshotSeed(
30
+ snapshot: Record<string, unknown>,
31
+ dateFields: readonly string[] = DEFAULT_SNAPSHOT_DATE_FIELDS,
32
+ ): Record<string, unknown> {
33
+ const seed: Record<string, unknown> = { ...snapshot }
34
+ for (const field of dateFields) {
35
+ const value = seed[field]
36
+ if (typeof value === 'string') seed[field] = new Date(value)
37
+ }
38
+ return seed
39
+ }
40
+
41
+ /**
42
+ * Serialize a persisted row into a plain after-snapshot object: pick `fields` from
43
+ * the entity and convert the declared `dateFields` from `Date` to ISO strings. Use
44
+ * for single-row snapshot loaders that are a clean 1:1 column copy; loaders that
45
+ * shape nested/related data keep their bespoke mapping.
46
+ */
47
+ export function serializeRowSnapshot<TEntity extends Record<string, unknown>>(
48
+ entity: TEntity,
49
+ fields: readonly string[],
50
+ dateFields: readonly string[] = DEFAULT_SNAPSHOT_DATE_FIELDS,
51
+ ): Record<string, unknown> {
52
+ const snapshot: Record<string, unknown> = {}
53
+ const dateFieldSet = new Set(dateFields)
54
+ for (const field of fields) {
55
+ const value = entity[field]
56
+ if (dateFieldSet.has(field)) {
57
+ snapshot[field] = value instanceof Date ? value.toISOString() : (value ?? null)
58
+ } else {
59
+ snapshot[field] = value ?? null
60
+ }
61
+ }
62
+ return snapshot
63
+ }
64
+
65
+ /**
66
+ * Resolve the after-snapshot a create command persisted for its action log, so a
67
+ * `redo` handler can re-materialize the original record. Reads the `undo.after`
68
+ * snapshot via {@link extractUndoPayload} and falls back to `snapshotAfter`.
69
+ */
70
+ export function resolveRedoSnapshot<T>(logEntry: CommandUndoLogEntry | null | undefined): T | null {
71
+ if (!logEntry) return null
72
+ const undo = extractUndoPayload<UndoPayload<T>>(logEntry)
73
+ if (undo && undo.after != null) return undo.after as T
74
+ if (logEntry.snapshotAfter != null) return logEntry.snapshotAfter as T
75
+ return null
76
+ }
77
+
78
+ /**
79
+ * Re-materialize a single row that a create command produced, reusing its original
80
+ * id. If the row still exists (it was soft-deleted by undo), clears `deletedAt` and
81
+ * restores `isActive` from the seed. If it was hard-deleted, re-creates it from the
82
+ * seed (which MUST include the original `id`). This keeps redo idempotent on id so
83
+ * undo/redo snapshots and cross-references stay stable (issue #2506, invariant I6).
84
+ */
85
+ export async function restoreCreatedRow<TEntity extends ScopedSoftDeletable>(
86
+ em: EntityManager,
87
+ entityClass: EntityClass<TEntity>,
88
+ id: string,
89
+ seedFromSnapshot: () => Record<string, unknown>,
90
+ findRow?: (em: EntityManager, id: string) => Promise<TEntity | null>,
91
+ ): Promise<TEntity> {
92
+ const existing = findRow
93
+ ? await findRow(em, id)
94
+ : ((await em.findOne(entityClass as never, { id } as never)) as TEntity | null)
95
+ if (existing) {
96
+ existing.deletedAt = null
97
+ const seed = seedFromSnapshot()
98
+ if (typeof seed.isActive === 'boolean') existing.isActive = seed.isActive
99
+ return existing
100
+ }
101
+ const record = em.create(entityClass as never, { ...seedFromSnapshot(), deletedAt: null } as never) as TEntity
102
+ em.persist(record as never)
103
+ return record
104
+ }
105
+
106
+ export type CreateRedoConfig<TEntity extends ScopedSoftDeletable, TSnapshot, TResult> = {
107
+ entityClass: EntityClass<TEntity>
108
+ /**
109
+ * Pulls the original primary id out of the after-snapshot. Defaults to
110
+ * `(snapshot) => snapshot.id`, which fits single-row snapshots whose top-level
111
+ * `id` is the row's primary key. Override only when the id lives elsewhere.
112
+ */
113
+ getSnapshotId?: (snapshot: TSnapshot) => string | null | undefined
114
+ /**
115
+ * Maps the after-snapshot back to a create seed; MUST include the original id.
116
+ * Defaults to {@link reviveSnapshotSeed} — the snapshot itself with `dateFields`
117
+ * revived to `Date`. Override only when the snapshot keys diverge from entity
118
+ * columns (e.g. nested shapes or derived columns).
119
+ */
120
+ seedFromSnapshot?: (snapshot: TSnapshot) => Record<string, unknown>
121
+ /**
122
+ * Snapshot keys to revive from ISO string to `Date` for the default seed.
123
+ * Defaults to `['createdAt', 'updatedAt', 'deletedAt']`; list additional date
124
+ * columns (e.g. `effectiveAt`, `returnedAt`) when the entity has them.
125
+ */
126
+ dateFields?: readonly string[]
127
+ /** Builds the command result (mirrors `execute`'s return), e.g. `(e) => ({ currencyId: e.id })`. */
128
+ buildResult: (entity: TEntity, snapshot: TSnapshot) => TResult
129
+ events?: CrudEventsConfig<any>
130
+ indexer?: CrudIndexerConfig<any>
131
+ /**
132
+ * Override how the existing row is looked up before restore. Defaults to
133
+ * `em.findOne(entityClass, { id })`. Pass a decryption-aware finder
134
+ * (`findOneWithDecryption`) for encrypted entities so the revive-in-place path
135
+ * sees the same row the rest of the module does.
136
+ */
137
+ findRow?: (args: { em: EntityManager; ctx: CommandRuntimeContext; id: string; snapshot: TSnapshot }) => Promise<TEntity | null>
138
+ /**
139
+ * Runs after the em fork, before the row is restored. Use to validate
140
+ * referenced relations (throw `CrudHttpError` to fail the redo) or resolve
141
+ * relation entities. Anything it returns is shallow-merged into the create
142
+ * seed (e.g. `{ entity: resolvedEntity }`), letting the seed reference a live
143
+ * relation instead of a raw id.
144
+ */
145
+ beforeRestore?: (args: {
146
+ em: EntityManager
147
+ ctx: CommandRuntimeContext
148
+ snapshot: TSnapshot
149
+ }) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void
150
+ /** Optional extra side effects after the row is restored (e.g. query-index upserts, custom-field restore). */
151
+ afterRestore?: (args: {
152
+ em: EntityManager
153
+ ctx: CommandRuntimeContext
154
+ entity: TEntity
155
+ snapshot: TSnapshot
156
+ logEntry: CommandUndoLogEntry
157
+ }) => Promise<void> | void
158
+ /**
159
+ * Wrap the restore (create/revive + flush) in a single transaction via
160
+ * {@link withAtomicFlush}. Use when the create participated in a multi-phase
161
+ * atomic flush in `execute` and partial commits must be impossible.
162
+ */
163
+ transaction?: boolean
164
+ }
165
+
166
+ /**
167
+ * Build a create command's `redo` handler that restores the original row in place
168
+ * (reusing its id) instead of replaying `execute` and minting a new id. Wires the
169
+ * `created` side effects (events + query index) exactly like `execute`. Use this for
170
+ * single-row create commands; multi-entity creates implement `redo` by hand.
171
+ */
172
+ export function makeCreateRedo<
173
+ TEntity extends ScopedSoftDeletable,
174
+ TSnapshot,
175
+ TInput = unknown,
176
+ TResult = unknown,
177
+ >(config: CreateRedoConfig<TEntity, TSnapshot, TResult>) {
178
+ const getSnapshotId = config.getSnapshotId ?? ((snapshot: TSnapshot) => (snapshot as { id?: string | null }).id ?? null)
179
+ const seedFromSnapshot =
180
+ config.seedFromSnapshot ?? ((snapshot: TSnapshot) => reviveSnapshotSeed(snapshot as Record<string, unknown>, config.dateFields))
181
+ return async ({ ctx, logEntry }: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<TResult> => {
182
+ const snapshot = resolveRedoSnapshot<TSnapshot>(logEntry)
183
+ const id = snapshot ? getSnapshotId(snapshot) : (logEntry.resourceId ?? null)
184
+ if (!snapshot || !id) {
185
+ throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for create command' })
186
+ }
187
+ const em = (ctx.container.resolve('em') as EntityManager).fork()
188
+ const overrides = config.beforeRestore ? await config.beforeRestore({ em, ctx, snapshot }) : undefined
189
+ const buildSeed = () => (overrides ? { ...seedFromSnapshot(snapshot), ...overrides } : seedFromSnapshot(snapshot))
190
+ const findRow = config.findRow
191
+ ? (forkedEm: EntityManager, rowId: string) => config.findRow!({ em: forkedEm, ctx, id: rowId, snapshot })
192
+ : undefined
193
+ let entity!: TEntity
194
+ const restorePhase = async () => {
195
+ entity = await restoreCreatedRow(em, config.entityClass, id, buildSeed, findRow)
196
+ }
197
+ const afterRestorePhase = async () => {
198
+ if (config.afterRestore) {
199
+ await config.afterRestore({ em, ctx, entity, snapshot, logEntry })
200
+ }
201
+ }
202
+ try {
203
+ if (config.transaction) {
204
+ const phases = config.afterRestore ? [restorePhase, afterRestorePhase] : [restorePhase]
205
+ await withAtomicFlush(em, phases, { transaction: true })
206
+ } else {
207
+ await restorePhase()
208
+ await em.flush()
209
+ await afterRestorePhase()
210
+ }
211
+ } catch (err) {
212
+ if (isUniqueViolation(err)) {
213
+ // [internal] prefix: this shared-lib helper has no `t(...)` context; the
214
+ // redo unique-collision is a rare developer-facing edge (the after-snapshot's
215
+ // unique key was re-taken since undo), surfaced via normal error handling.
216
+ throw conflict('[internal] Cannot redo: a record with the same unique key already exists.')
217
+ }
218
+ throw err
219
+ }
220
+ const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
221
+ await emitCrudSideEffects({
222
+ dataEngine,
223
+ action: 'created',
224
+ entity,
225
+ identifiers: {
226
+ id,
227
+ organizationId: entity.organizationId ?? null,
228
+ tenantId: entity.tenantId ?? null,
229
+ },
230
+ events: config.events,
231
+ indexer: config.indexer,
232
+ })
233
+ return config.buildResult(entity, snapshot)
234
+ }
235
+ }
@@ -104,6 +104,17 @@ export interface CommandHandler<TInput = unknown, TResult = unknown> {
104
104
  buildLog?(args: CommandLogBuilderArgs<TInput, TResult>): Promise<CommandLogMetadata | null | undefined> | CommandLogMetadata | null | undefined
105
105
  captureAfter?(input: TInput, result: TResult, ctx: CommandRuntimeContext): Promise<unknown> | unknown
106
106
  undo?(params: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<void> | void
107
+ /**
108
+ * Optional redo handler. When defined, the command bus calls this instead of
109
+ * `execute()` while replaying a previously undone action (the redo route passes
110
+ * `redoLogEntry` in the execution options). It receives the source action log so
111
+ * it can re-materialize the original record **reusing its id** — for a create
112
+ * command this restores the soft-deleted row (or re-creates it from the
113
+ * `snapshotAfter`) instead of minting a new id, keeping undo/redo snapshots and
114
+ * references stable (issue #2506, invariant I6). Handlers without `redo` keep the
115
+ * legacy behavior of replaying `execute(__redoInput)`.
116
+ */
117
+ redo?(params: { input: TInput; ctx: CommandRuntimeContext; logEntry: CommandUndoLogEntry }): Promise<TResult> | TResult
107
118
  }
108
119
 
109
120
  export type CommandExecutionOptions<TInput> = {
@@ -111,6 +122,16 @@ export type CommandExecutionOptions<TInput> = {
111
122
  ctx: CommandRuntimeContext
112
123
  metadata?: CommandLogMetadata | null
113
124
  skipCacheInvalidation?: boolean
125
+ /**
126
+ * When set, marks this execution as a redo of a previously undone action. If the
127
+ * resolved handler defines a `redo` method, the command bus calls
128
+ * `handler.redo({ input, ctx, logEntry })` instead of `handler.execute(...)`. The
129
+ * rest of the pipeline (snapshots, buildLog, undo-token minting, persistence,
130
+ * cache invalidation, side effects) is identical, so the fresh log entry — and
131
+ * the `x-om-operation` header derived from it — automatically carry the restored
132
+ * resourceId. Ignored when the handler has no `redo` method (legacy replay path).
133
+ */
134
+ redoLogEntry?: CommandUndoLogEntry | null
114
135
  }
115
136
 
116
137
  export function defaultUndoToken(): string {
@@ -40,6 +40,35 @@ export function notFound(message = 'Not found'): CrudHttpError {
40
40
  return new CrudHttpError(404, { error: message })
41
41
  }
42
42
 
43
+ export function conflict(message: string): CrudHttpError {
44
+ return new CrudHttpError(409, { error: message })
45
+ }
46
+
47
+ const POSTGRES_UNIQUE_VIOLATION = '23505'
48
+
49
+ /**
50
+ * Detects a Postgres unique-constraint violation (SQLSTATE 23505) on a thrown
51
+ * error, looking through MikroORM's driver-error wrapping. Use this to map a DB
52
+ * uniqueness race (e.g. a soft-deleted row the in-app check missed, or a
53
+ * timestamp-precision mismatch) onto a clean 409 instead of a generic 500.
54
+ */
55
+ export function isUniqueViolation(err: unknown, constraintName?: string): boolean {
56
+ if (!err || typeof err !== 'object') return false
57
+ const candidates: unknown[] = [err, (err as { cause?: unknown }).cause, (err as { previous?: unknown }).previous]
58
+ for (const candidate of candidates) {
59
+ if (!candidate || typeof candidate !== 'object') continue
60
+ const record = candidate as Record<string, unknown>
61
+ if (record.code === POSTGRES_UNIQUE_VIOLATION) {
62
+ if (!constraintName) return true
63
+ const constraint = typeof record.constraint === 'string' ? record.constraint : ''
64
+ const detail = typeof record.detail === 'string' ? record.detail : ''
65
+ const message = typeof record.message === 'string' ? record.message : ''
66
+ return constraint === constraintName || detail.includes(constraintName) || message.includes(constraintName)
67
+ }
68
+ }
69
+ return false
70
+ }
71
+
43
72
  export function assertFound<T>(value: T | null | undefined, message: string): T {
44
73
  if (!value) throw notFound(message)
45
74
  return value