@open-mercato/shared 0.8.1-develop.7297.1.67605e74f9 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +2 -0
- package/dist/lib/auth/jwt.js +4 -0
- package/dist/lib/auth/jwt.js.map +2 -2
- package/dist/lib/auth/organizationScope.js +15 -1
- package/dist/lib/auth/organizationScope.js.map +2 -2
- package/dist/lib/availability/catalogOnlyProvider.js +77 -0
- package/dist/lib/availability/catalogOnlyProvider.js.map +7 -0
- package/dist/lib/availability/index.js +8 -0
- package/dist/lib/availability/index.js.map +7 -0
- package/dist/lib/availability/registry.js +47 -0
- package/dist/lib/availability/registry.js.map +7 -0
- package/dist/lib/availability/types.js +7 -0
- package/dist/lib/availability/types.js.map +7 -0
- package/dist/lib/catalog-visibility/index.js +11 -0
- package/dist/lib/catalog-visibility/index.js.map +7 -0
- package/dist/lib/catalog-visibility/intersectScopes.js +49 -0
- package/dist/lib/catalog-visibility/intersectScopes.js.map +7 -0
- package/dist/lib/catalog-visibility/matchesOne.js +27 -0
- package/dist/lib/catalog-visibility/matchesOne.js.map +7 -0
- package/dist/lib/catalog-visibility/matchesScope.js +9 -0
- package/dist/lib/catalog-visibility/matchesScope.js.map +7 -0
- package/dist/lib/catalog-visibility/types.js +1 -0
- package/dist/lib/catalog-visibility/types.js.map +7 -0
- package/dist/lib/catalog-visibility/unionScopes.js +9 -0
- package/dist/lib/catalog-visibility/unionScopes.js.map +7 -0
- package/dist/lib/commands/command-bus.js +383 -125
- package/dist/lib/commands/command-bus.js.map +3 -3
- package/dist/lib/commands/flush.js +16 -0
- package/dist/lib/commands/flush.js.map +2 -2
- package/dist/lib/commands/index.js +8 -0
- package/dist/lib/commands/index.js.map +2 -2
- package/dist/lib/commands/redo.js +2 -7
- package/dist/lib/commands/redo.js.map +2 -2
- package/dist/lib/commands/transaction-lifetime.js +67 -0
- package/dist/lib/commands/transaction-lifetime.js.map +7 -0
- package/dist/lib/commands/types.js.map +2 -2
- package/dist/lib/commands/undo.js +54 -2
- package/dist/lib/commands/undo.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +64 -9
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +42 -18
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/data/engine.js +12 -3
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/accentInsensitiveSearch.js +21 -0
- package/dist/lib/db/accentInsensitiveSearch.js.map +7 -0
- package/dist/lib/encryption/kms.js +1 -5
- package/dist/lib/encryption/kms.js.map +2 -2
- package/dist/lib/encryption/likeFilterWarning.js +3 -1
- package/dist/lib/encryption/likeFilterWarning.js.map +2 -2
- package/dist/lib/encryption/migration-backfill.js +69 -0
- package/dist/lib/encryption/migration-backfill.js.map +7 -0
- package/dist/lib/encryption/policyMemo.js +28 -0
- package/dist/lib/encryption/policyMemo.js.map +7 -0
- package/dist/lib/encryption/subscriber.js +30 -3
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +113 -200
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +3 -3
- package/dist/lib/modules/api-route-identity.js +102 -0
- package/dist/lib/modules/api-route-identity.js.map +7 -0
- package/dist/lib/telemetry/runtime.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/modules/events/factory.js +20 -0
- package/dist/modules/events/factory.js.map +2 -2
- package/dist/modules/registry.js +3 -1
- package/dist/modules/registry.js.map +2 -2
- package/package.json +12 -5
- package/src/lib/auth/__tests__/jwt.test.ts +12 -0
- package/src/lib/auth/__tests__/organizationScope.test.ts +27 -0
- package/src/lib/auth/jwt.ts +9 -0
- package/src/lib/auth/organizationScope.ts +39 -0
- package/src/lib/availability/__tests__/catalogOnlyProvider.test.ts +166 -0
- package/src/lib/availability/__tests__/registry.test.ts +130 -0
- package/src/lib/availability/catalogOnlyProvider.ts +152 -0
- package/src/lib/availability/index.ts +18 -0
- package/src/lib/availability/registry.ts +131 -0
- package/src/lib/availability/types.ts +86 -0
- package/src/lib/catalog-visibility/__tests__/intersectScopes.test.ts +168 -0
- package/src/lib/catalog-visibility/__tests__/matchesOne.test.ts +116 -0
- package/src/lib/catalog-visibility/__tests__/matchesScope.test.ts +35 -0
- package/src/lib/catalog-visibility/__tests__/testFixtures.ts +66 -0
- package/src/lib/catalog-visibility/__tests__/unionScopes.test.ts +62 -0
- package/src/lib/catalog-visibility/index.ts +5 -0
- package/src/lib/catalog-visibility/intersectScopes.ts +93 -0
- package/src/lib/catalog-visibility/matchesOne.ts +28 -0
- package/src/lib/catalog-visibility/matchesScope.ts +8 -0
- package/src/lib/catalog-visibility/types.ts +31 -0
- package/src/lib/catalog-visibility/unionScopes.ts +16 -0
- package/src/lib/commands/__tests__/command-bus.atomic-replay.test.ts +650 -0
- package/src/lib/commands/__tests__/command-bus.test.ts +175 -0
- package/src/lib/commands/__tests__/command-bus.undo-toctou.test.ts +1 -1
- package/src/lib/commands/__tests__/redo.test.ts +4 -0
- package/src/lib/commands/__tests__/transaction-lifetime.test.ts +118 -0
- package/src/lib/commands/__tests__/undo.test.ts +90 -1
- package/src/lib/commands/command-bus.ts +463 -148
- package/src/lib/commands/flush.ts +16 -0
- package/src/lib/commands/index.ts +7 -0
- package/src/lib/commands/redo.ts +4 -8
- package/src/lib/commands/transaction-lifetime.ts +88 -0
- package/src/lib/commands/types.ts +91 -0
- package/src/lib/commands/undo.ts +79 -1
- package/src/lib/crud/__tests__/crud-factory.cache-tenant-level.test.ts +149 -0
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +48 -6
- package/src/lib/crud/__tests__/crud-factory.test.ts +224 -27
- package/src/lib/crud/__tests__/enricher-runner.test.ts +270 -5
- package/src/lib/crud/enricher-runner.ts +83 -9
- package/src/lib/crud/factory.ts +46 -19
- package/src/lib/data/__tests__/engine.pin-organization-id.test.ts +102 -0
- package/src/lib/data/engine.ts +12 -0
- package/src/lib/db/__tests__/accentInsensitiveSearch.test.ts +51 -0
- package/src/lib/db/accentInsensitiveSearch.ts +70 -0
- package/src/lib/encryption/__tests__/dek-lifecycle.test.ts +11 -9
- package/src/lib/encryption/__tests__/fixtures/policy-process-worker.ts +96 -0
- package/src/lib/encryption/__tests__/kms.test.ts +45 -3
- package/src/lib/encryption/__tests__/likeFilterWarning.test.ts +43 -0
- package/src/lib/encryption/__tests__/migration-backfill.test.ts +57 -0
- package/src/lib/encryption/__tests__/process-policy-coherence.test.ts +128 -0
- package/src/lib/encryption/__tests__/subscriber.deep-decrypt-collections.test.ts +66 -1
- package/src/lib/encryption/__tests__/subscriber.test.ts +3 -0
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +412 -51
- package/src/lib/encryption/kms.ts +1 -6
- package/src/lib/encryption/likeFilterWarning.ts +10 -2
- package/src/lib/encryption/migration-backfill.ts +90 -0
- package/src/lib/encryption/policyMemo.ts +51 -0
- package/src/lib/encryption/subscriber.ts +41 -4
- package/src/lib/encryption/tenantDataEncryptionService.ts +153 -219
- package/src/lib/i18n/__tests__/detect-locale-narrowed.test.ts +20 -10
- package/src/lib/modules/api-route-identity.ts +116 -0
- package/src/lib/telemetry/runtime.ts +8 -0
- package/src/modules/__tests__/registry.test.ts +92 -0
- package/src/modules/events/__tests__/broadcast-coalescing-declaration.test.ts +53 -0
- package/src/modules/events/factory.ts +31 -0
- package/src/modules/events/types.ts +13 -0
- package/src/modules/notifications/types.ts +1 -0
- package/src/modules/registry.ts +4 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/catalog-visibility/unionScopes.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AssortmentScope, EffectiveAssortmentScope } from './types'\n\n/**\n * Combines N independent sources' own scopes into one OR-list.\n * THE CRITICAL CORRECTNESS REQUIREMENT (a first draft got this wrong \u2014 R2 defect):\n * do NOT merge every source's categoryIds/tagIds into one flat AssortmentScope object.\n * That computes AND across sources' dimensions instead of OR. Each source becomes its\n * own OR-branch in the returned list, UNMODIFIED \u2014 nothing about one source's scope\n * changes because another source exists. The returned list is a fresh array, never the\n * caller's own.\n */\nexport function unionScopes(scopes: Array<AssortmentScope | null>): EffectiveAssortmentScope {\n if (scopes.length === 0) return null\n if (scopes.some((s) => s === null)) return null\n return scopes.filter((scope): scope is AssortmentScope => scope !== null)\n}\n"],
|
|
5
|
+
"mappings": "AAWO,SAAS,YAAY,QAAiE;AAC3F,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,MAAI,OAAO,KAAK,CAAC,MAAM,MAAM,IAAI,EAAG,QAAO;AAC3C,SAAO,OAAO,OAAO,CAAC,UAAoC,UAAU,IAAI;AAC1E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -18,7 +18,32 @@ import {
|
|
|
18
18
|
import { CommandInterceptorError } from "./errors.js";
|
|
19
19
|
import { isReadProjectionAlwaysConsistent } from "@open-mercato/shared/lib/data/consistency";
|
|
20
20
|
import { createLogger } from "../logger/index.js";
|
|
21
|
+
import { withAtomicFlush } from "./flush.js";
|
|
22
|
+
import {
|
|
23
|
+
getTransactionLifetime,
|
|
24
|
+
onTransactionLifetimeComplete,
|
|
25
|
+
ownsTransactionLifetime
|
|
26
|
+
} from "./transaction-lifetime.js";
|
|
21
27
|
const logger = createLogger("shared").child({ component: "commands" });
|
|
28
|
+
function isInTransaction(em) {
|
|
29
|
+
const probe = em.isInTransaction;
|
|
30
|
+
return typeof probe === "function" && probe.call(em);
|
|
31
|
+
}
|
|
32
|
+
function resolveAtomicReplayTransaction(ctx) {
|
|
33
|
+
const em = ctx.transactionalEm ?? ctx.container.resolve("em");
|
|
34
|
+
const joinsAmbient = isInTransaction(em);
|
|
35
|
+
if (joinsAmbient && !ownsTransactionLifetime(em, ctx.transactionLifetime)) {
|
|
36
|
+
throw new Error("[internal] Ambient atomic replay requires its outer transaction lifetime owner");
|
|
37
|
+
}
|
|
38
|
+
return { em, joinsAmbient };
|
|
39
|
+
}
|
|
40
|
+
function bindReplayTransactionContext(ctx, em, expectedLifetime) {
|
|
41
|
+
const transactionLifetime = getTransactionLifetime(em);
|
|
42
|
+
if (!transactionLifetime || expectedLifetime && transactionLifetime !== expectedLifetime) {
|
|
43
|
+
throw new Error("[internal] Atomic replay transaction lifetime owner changed before replay");
|
|
44
|
+
}
|
|
45
|
+
return { ...ctx, transactionalEm: em, transactionLifetime };
|
|
46
|
+
}
|
|
22
47
|
const SKIPPED_ACTION_LOG_RESOURCE_KINDS = /* @__PURE__ */ new Set([
|
|
23
48
|
"audit_logs.access",
|
|
24
49
|
"audit_logs.action",
|
|
@@ -176,117 +201,227 @@ function extractAliasList(source) {
|
|
|
176
201
|
class CommandBus {
|
|
177
202
|
async execute(commandId, options) {
|
|
178
203
|
const handler = await this.resolveHandler(commandId);
|
|
204
|
+
const replayLogEntry = options.redoLogEntry ?? null;
|
|
205
|
+
const isAtomicReplay = replayLogEntry !== null && handler.atomicReplay === true;
|
|
179
206
|
const allInterceptors = getAllCommandInterceptorInstances();
|
|
180
207
|
let interceptorMetadata = /* @__PURE__ */ new Map();
|
|
181
208
|
let effectiveOptions = options;
|
|
182
|
-
|
|
183
|
-
|
|
209
|
+
let userFeatures = [];
|
|
210
|
+
const runBeforeExecuteInterceptors = async (baseOptions) => {
|
|
211
|
+
const resolvedUserFeatures = allInterceptors.length ? await this.resolveUserFeaturesForInterceptors(baseOptions.ctx) : [];
|
|
212
|
+
if (!allInterceptors.length) {
|
|
213
|
+
return {
|
|
214
|
+
effectiveOptions: baseOptions,
|
|
215
|
+
metadata: /* @__PURE__ */ new Map(),
|
|
216
|
+
userFeatures: resolvedUserFeatures
|
|
217
|
+
};
|
|
218
|
+
}
|
|
184
219
|
const interceptorCtx = {
|
|
185
220
|
commandId,
|
|
186
|
-
auth:
|
|
187
|
-
selectedOrganizationId:
|
|
188
|
-
container:
|
|
221
|
+
auth: baseOptions.ctx.auth ?? null,
|
|
222
|
+
selectedOrganizationId: baseOptions.ctx.selectedOrganizationId ?? baseOptions.ctx.auth?.orgId ?? null,
|
|
223
|
+
container: baseOptions.ctx.container
|
|
189
224
|
};
|
|
190
225
|
const beforeResult = await runCommandInterceptorsBefore(
|
|
191
226
|
allInterceptors,
|
|
192
227
|
commandId,
|
|
193
|
-
|
|
228
|
+
baseOptions.input,
|
|
194
229
|
interceptorCtx,
|
|
195
|
-
|
|
230
|
+
resolvedUserFeatures
|
|
196
231
|
);
|
|
197
232
|
if (!beforeResult.ok) {
|
|
198
233
|
const blocked = beforeResult.error;
|
|
199
234
|
throw new CommandInterceptorError(blocked.message, { status: blocked.status, body: blocked.body });
|
|
200
235
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const snapshots = await this.prepareSnapshots(handler, effectiveOptions);
|
|
210
|
-
const redoLogEntry = effectiveOptions.redoLogEntry ?? null;
|
|
211
|
-
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);
|
|
212
|
-
const afterSnapshot = await this.captureAfter(handler, effectiveOptions, result);
|
|
213
|
-
const snapshotsWithAfter = { ...snapshots, after: afterSnapshot };
|
|
214
|
-
const logMeta = await this.buildLog(handler, effectiveOptions, result, snapshotsWithAfter);
|
|
215
|
-
let mergedMeta = this.mergeMetadata(effectiveOptions.metadata, logMeta);
|
|
216
|
-
let interceptorContextMerged = {};
|
|
217
|
-
for (const meta of interceptorMetadata.values()) {
|
|
218
|
-
const logContextRecord = asRecord(asRecord(meta)?.logContext);
|
|
219
|
-
if (!logContextRecord) continue;
|
|
220
|
-
interceptorContextMerged = {
|
|
221
|
-
...interceptorContextMerged,
|
|
222
|
-
...logContextRecord
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
const baseContext = asRecord(effectiveOptions.metadata?.context) ?? {};
|
|
226
|
-
const logMetaContext = asRecord(logMeta?.context) ?? {};
|
|
227
|
-
if (Object.keys(interceptorContextMerged).length > 0 || Object.keys(baseContext).length > 0 || Object.keys(logMetaContext).length > 0) {
|
|
228
|
-
mergedMeta = mergedMeta ?? {};
|
|
229
|
-
mergedMeta.context = {
|
|
230
|
-
...baseContext,
|
|
231
|
-
...interceptorContextMerged,
|
|
232
|
-
...logMetaContext
|
|
236
|
+
const preparedOptions = beforeResult.modifiedInput ? {
|
|
237
|
+
...baseOptions,
|
|
238
|
+
input: { ...baseOptions.input, ...beforeResult.modifiedInput }
|
|
239
|
+
} : baseOptions;
|
|
240
|
+
return {
|
|
241
|
+
effectiveOptions: preparedOptions,
|
|
242
|
+
metadata: beforeResult.metadataByInterceptor,
|
|
243
|
+
userFeatures: resolvedUserFeatures
|
|
233
244
|
};
|
|
245
|
+
};
|
|
246
|
+
if (replayLogEntry && !isAtomicReplay) {
|
|
247
|
+
await this.runReplayTransactionGuard(options.ctx, "redo", replayLogEntry);
|
|
248
|
+
if (handler.authorizeReplay) {
|
|
249
|
+
await handler.authorizeReplay({
|
|
250
|
+
operation: "redo",
|
|
251
|
+
input: options.input,
|
|
252
|
+
ctx: options.ctx,
|
|
253
|
+
logEntry: replayLogEntry
|
|
254
|
+
});
|
|
255
|
+
}
|
|
234
256
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
if (!isAtomicReplay) {
|
|
258
|
+
const prepared = await runBeforeExecuteInterceptors(options);
|
|
259
|
+
effectiveOptions = prepared.effectiveOptions;
|
|
260
|
+
interceptorMetadata = prepared.metadata;
|
|
261
|
+
userFeatures = prepared.userFeatures;
|
|
240
262
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
263
|
+
const executeCore = async (coreOptions) => {
|
|
264
|
+
const sourceLog = coreOptions.redoLogEntry ?? null;
|
|
265
|
+
const snapshots = await this.prepareSnapshots(handler, coreOptions);
|
|
266
|
+
const result2 = sourceLog && typeof handler.redo === "function" ? await handler.redo({ input: coreOptions.input, ctx: coreOptions.ctx, logEntry: sourceLog }) : await handler.execute(coreOptions.input, coreOptions.ctx);
|
|
267
|
+
const afterSnapshot = await this.captureAfter(handler, coreOptions, result2);
|
|
268
|
+
const snapshotsWithAfter = { ...snapshots, after: afterSnapshot };
|
|
269
|
+
const logMeta = await this.buildLog(handler, coreOptions, result2, snapshotsWithAfter);
|
|
270
|
+
let mergedMeta2 = this.mergeMetadata(coreOptions.metadata, logMeta);
|
|
271
|
+
let interceptorContextMerged = {};
|
|
272
|
+
for (const meta of interceptorMetadata.values()) {
|
|
273
|
+
const logContextRecord = asRecord(asRecord(meta)?.logContext);
|
|
274
|
+
if (!logContextRecord) continue;
|
|
275
|
+
interceptorContextMerged = {
|
|
276
|
+
...interceptorContextMerged,
|
|
277
|
+
...logContextRecord
|
|
278
|
+
};
|
|
246
279
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
280
|
+
const baseContext = asRecord(coreOptions.metadata?.context) ?? {};
|
|
281
|
+
const logMetaContext = asRecord(logMeta?.context) ?? {};
|
|
282
|
+
if (Object.keys(interceptorContextMerged).length > 0 || Object.keys(baseContext).length > 0 || Object.keys(logMetaContext).length > 0) {
|
|
283
|
+
mergedMeta2 = mergedMeta2 ?? {};
|
|
284
|
+
mergedMeta2.context = {
|
|
285
|
+
...baseContext,
|
|
286
|
+
...interceptorContextMerged,
|
|
287
|
+
...logMetaContext
|
|
288
|
+
};
|
|
253
289
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
290
|
+
const undoable = this.isUndoable(handler);
|
|
291
|
+
if (undoable && mergedMeta2?.replayable !== false) {
|
|
292
|
+
mergedMeta2 = mergedMeta2 ?? {};
|
|
293
|
+
if (!mergedMeta2.undoToken) mergedMeta2.undoToken = defaultUndoToken();
|
|
294
|
+
if (mergedMeta2.actorUserId === void 0) mergedMeta2.actorUserId = coreOptions.ctx.auth?.sub ?? null;
|
|
295
|
+
} else if (mergedMeta2?.replayable === false) {
|
|
296
|
+
mergedMeta2.undoToken = null;
|
|
261
297
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
effectiveOptions.input,
|
|
276
|
-
result,
|
|
277
|
-
interceptorCtx,
|
|
278
|
-
userFeatures,
|
|
279
|
-
interceptorMetadata
|
|
280
|
-
);
|
|
281
|
-
if (afterResult.modifiedResult && typeof result === "object" && result) {
|
|
282
|
-
finalResult = { ...result, ...afterResult.modifiedResult };
|
|
298
|
+
if (afterSnapshot !== void 0 && afterSnapshot !== null) {
|
|
299
|
+
if (!mergedMeta2) {
|
|
300
|
+
mergedMeta2 = { snapshotAfter: afterSnapshot };
|
|
301
|
+
} else if (!mergedMeta2.snapshotAfter) {
|
|
302
|
+
mergedMeta2.snapshotAfter = afterSnapshot;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (snapshots.before) {
|
|
306
|
+
if (!mergedMeta2) {
|
|
307
|
+
mergedMeta2 = { snapshotBefore: snapshots.before };
|
|
308
|
+
} else if (!mergedMeta2.snapshotBefore) {
|
|
309
|
+
mergedMeta2.snapshotBefore = snapshots.before;
|
|
310
|
+
}
|
|
283
311
|
}
|
|
312
|
+
if (mergedMeta2?.snapshotBefore !== void 0 && mergedMeta2?.snapshotAfter !== void 0) {
|
|
313
|
+
const currentChanges = mergedMeta2.changes;
|
|
314
|
+
const shouldInfer = currentChanges === void 0 || currentChanges === null || typeof currentChanges === "object" && !Array.isArray(currentChanges) && Object.keys(currentChanges).length === 0;
|
|
315
|
+
if (shouldInfer) {
|
|
316
|
+
const inferred = deriveChangesFromSnapshots(mergedMeta2.snapshotBefore, mergedMeta2.snapshotAfter);
|
|
317
|
+
if (inferred) mergedMeta2.changes = inferred;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const logEntry2 = await this.persistLog(commandId, coreOptions, mergedMeta2);
|
|
321
|
+
return { result: result2, mergedMeta: mergedMeta2, logEntry: logEntry2 };
|
|
322
|
+
};
|
|
323
|
+
let replaySourceFinalized = false;
|
|
324
|
+
let coreResult;
|
|
325
|
+
const atomicSourceLog = options.redoLogEntry ?? null;
|
|
326
|
+
let ambientReplay = null;
|
|
327
|
+
let replayTransactionLifetime = null;
|
|
328
|
+
if (atomicSourceLog && isAtomicReplay) {
|
|
329
|
+
ambientReplay = resolveAtomicReplayTransaction(options.ctx);
|
|
330
|
+
const replayEm = ambientReplay.em;
|
|
331
|
+
await withAtomicFlush(replayEm, [async () => {
|
|
332
|
+
const replayCtx = bindReplayTransactionContext(
|
|
333
|
+
options.ctx,
|
|
334
|
+
replayEm,
|
|
335
|
+
ambientReplay?.joinsAmbient ? options.ctx.transactionLifetime : void 0
|
|
336
|
+
);
|
|
337
|
+
replayTransactionLifetime = replayCtx.transactionLifetime ?? null;
|
|
338
|
+
const transactionalBaseOptions = {
|
|
339
|
+
...options,
|
|
340
|
+
ctx: replayCtx
|
|
341
|
+
};
|
|
342
|
+
if (!atomicSourceLog.id) throw new Error("[internal] Atomic redo source log id is required");
|
|
343
|
+
if (handler.stabilizeReplay) {
|
|
344
|
+
await handler.stabilizeReplay({
|
|
345
|
+
operation: "redo",
|
|
346
|
+
input: transactionalBaseOptions.input,
|
|
347
|
+
ctx: transactionalBaseOptions.ctx,
|
|
348
|
+
logEntry: atomicSourceLog
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
await this.runReplayTransactionGuard(
|
|
352
|
+
transactionalBaseOptions.ctx,
|
|
353
|
+
"redo",
|
|
354
|
+
atomicSourceLog,
|
|
355
|
+
replayEm
|
|
356
|
+
);
|
|
357
|
+
const prepared = await runBeforeExecuteInterceptors(transactionalBaseOptions);
|
|
358
|
+
effectiveOptions = prepared.effectiveOptions;
|
|
359
|
+
interceptorMetadata = prepared.metadata;
|
|
360
|
+
userFeatures = prepared.userFeatures;
|
|
361
|
+
const service = effectiveOptions.ctx.container.resolve("actionLogService");
|
|
362
|
+
if (handler.authorizeReplay) {
|
|
363
|
+
await handler.authorizeReplay({
|
|
364
|
+
operation: "redo",
|
|
365
|
+
input: effectiveOptions.input,
|
|
366
|
+
ctx: effectiveOptions.ctx,
|
|
367
|
+
logEntry: atomicSourceLog
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const claimed = await service.claimForRedo(atomicSourceLog.id, replayEm);
|
|
371
|
+
if (!claimed) throw new Error("[internal] Redo source already consumed");
|
|
372
|
+
coreResult = await executeCore(effectiveOptions);
|
|
373
|
+
}], { transaction: true, label: `${commandId}.redo` });
|
|
374
|
+
replaySourceFinalized = !ambientReplay.joinsAmbient;
|
|
375
|
+
} else {
|
|
376
|
+
coreResult = await executeCore(effectiveOptions);
|
|
284
377
|
}
|
|
285
|
-
|
|
286
|
-
|
|
378
|
+
const { result, mergedMeta, logEntry } = coreResult;
|
|
379
|
+
const runPostCommit = async () => {
|
|
380
|
+
let finalResult = result;
|
|
381
|
+
if (allInterceptors.length) {
|
|
382
|
+
const interceptorCtx = {
|
|
383
|
+
commandId,
|
|
384
|
+
auth: effectiveOptions.ctx.auth ?? null,
|
|
385
|
+
selectedOrganizationId: effectiveOptions.ctx.selectedOrganizationId ?? effectiveOptions.ctx.auth?.orgId ?? null,
|
|
386
|
+
container: effectiveOptions.ctx.container
|
|
387
|
+
};
|
|
388
|
+
const afterResult = await runCommandInterceptorsAfter(
|
|
389
|
+
allInterceptors,
|
|
390
|
+
commandId,
|
|
391
|
+
effectiveOptions.input,
|
|
392
|
+
result,
|
|
393
|
+
interceptorCtx,
|
|
394
|
+
userFeatures,
|
|
395
|
+
interceptorMetadata
|
|
396
|
+
);
|
|
397
|
+
if (afterResult.modifiedResult && typeof result === "object" && result) {
|
|
398
|
+
finalResult = { ...result, ...afterResult.modifiedResult };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (!effectiveOptions.skipCacheInvalidation) {
|
|
402
|
+
await this.invalidateCacheAfterExecute(commandId, effectiveOptions, finalResult, mergedMeta);
|
|
403
|
+
}
|
|
404
|
+
await this.flushCrudSideEffects(effectiveOptions.ctx.container, effectiveOptions.ctx?.bulkImport);
|
|
405
|
+
return finalResult;
|
|
406
|
+
};
|
|
407
|
+
const executionResult = {
|
|
408
|
+
result,
|
|
409
|
+
logEntry,
|
|
410
|
+
replaySourceFinalized
|
|
411
|
+
};
|
|
412
|
+
if (ambientReplay?.joinsAmbient) {
|
|
413
|
+
if (!replayTransactionLifetime || !ownsTransactionLifetime(ambientReplay.em, replayTransactionLifetime)) {
|
|
414
|
+
throw new Error("[internal] Ambient atomic replay lost its outer transaction lifetime owner");
|
|
415
|
+
}
|
|
416
|
+
onTransactionLifetimeComplete(ambientReplay.em, async (outcome) => {
|
|
417
|
+
if (outcome !== "committed") return;
|
|
418
|
+
executionResult.result = await runPostCommit();
|
|
419
|
+
executionResult.replaySourceFinalized = true;
|
|
420
|
+
});
|
|
421
|
+
return executionResult;
|
|
287
422
|
}
|
|
288
|
-
await
|
|
289
|
-
return
|
|
423
|
+
executionResult.result = await runPostCommit();
|
|
424
|
+
return executionResult;
|
|
290
425
|
}
|
|
291
426
|
async undo(undoToken, ctx) {
|
|
292
427
|
const service = ctx.container.resolve("actionLogService");
|
|
@@ -296,39 +431,121 @@ class CommandBus {
|
|
|
296
431
|
if (!handler.undo || this.isUndoable(handler) === false) {
|
|
297
432
|
throw new Error(`Command ${log.commandId} is not undoable`);
|
|
298
433
|
}
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
if (allInterceptors.length) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
auth: ctx.auth ?? null,
|
|
310
|
-
selectedOrganizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
311
|
-
container: ctx.container
|
|
434
|
+
const isAtomicReplay = handler.atomicReplay === true;
|
|
435
|
+
const allInterceptors = getAllCommandInterceptorInstances();
|
|
436
|
+
let undoInterceptorMetadata = /* @__PURE__ */ new Map();
|
|
437
|
+
let userFeatures = [];
|
|
438
|
+
const runBeforeUndoInterceptors = async (replayCtx) => {
|
|
439
|
+
const resolvedUserFeatures = allInterceptors.length ? await this.resolveUserFeaturesForInterceptors(replayCtx) : [];
|
|
440
|
+
if (!allInterceptors.length) {
|
|
441
|
+
return {
|
|
442
|
+
metadata: /* @__PURE__ */ new Map(),
|
|
443
|
+
userFeatures: resolvedUserFeatures
|
|
312
444
|
};
|
|
313
|
-
const beforeResult = await runCommandInterceptorsBeforeUndo(
|
|
314
|
-
allInterceptors,
|
|
315
|
-
log.commandId,
|
|
316
|
-
undoCtx,
|
|
317
|
-
interceptorCtx,
|
|
318
|
-
userFeatures
|
|
319
|
-
);
|
|
320
|
-
if (!beforeResult.ok) {
|
|
321
|
-
const blocked = beforeResult.error;
|
|
322
|
-
throw new CommandInterceptorError(blocked.message, { status: blocked.status, body: blocked.body });
|
|
323
|
-
}
|
|
324
|
-
undoInterceptorMetadata = beforeResult.metadataByInterceptor;
|
|
325
445
|
}
|
|
446
|
+
const undoCtx = { input: log.commandPayload, logEntry: log, undoToken };
|
|
447
|
+
const interceptorCtx = {
|
|
448
|
+
commandId: log.commandId,
|
|
449
|
+
auth: replayCtx.auth ?? null,
|
|
450
|
+
selectedOrganizationId: replayCtx.selectedOrganizationId ?? replayCtx.auth?.orgId ?? null,
|
|
451
|
+
container: replayCtx.container
|
|
452
|
+
};
|
|
453
|
+
const beforeResult = await runCommandInterceptorsBeforeUndo(
|
|
454
|
+
allInterceptors,
|
|
455
|
+
log.commandId,
|
|
456
|
+
undoCtx,
|
|
457
|
+
interceptorCtx,
|
|
458
|
+
resolvedUserFeatures
|
|
459
|
+
);
|
|
460
|
+
if (!beforeResult.ok) {
|
|
461
|
+
const blocked = beforeResult.error;
|
|
462
|
+
throw new CommandInterceptorError(blocked.message, { status: blocked.status, body: blocked.body });
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
metadata: beforeResult.metadataByInterceptor,
|
|
466
|
+
userFeatures: resolvedUserFeatures
|
|
467
|
+
};
|
|
468
|
+
};
|
|
469
|
+
if (!isAtomicReplay) {
|
|
470
|
+
await this.runReplayTransactionGuard(ctx, "undo", log);
|
|
471
|
+
if (handler.authorizeReplay) {
|
|
472
|
+
await handler.authorizeReplay({
|
|
473
|
+
operation: "undo",
|
|
474
|
+
input: log.commandPayload,
|
|
475
|
+
ctx,
|
|
476
|
+
logEntry: log
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const claimUndo = async (replayEm) => {
|
|
481
|
+
const claimed = replayEm ? await service.claimForUndo(log.id, replayEm) : await service.claimForUndo(log.id);
|
|
482
|
+
if (!claimed) throw new Error("[internal] Undo token already consumed");
|
|
483
|
+
};
|
|
484
|
+
const finishUndo = async (replayCtx, replayEm) => {
|
|
326
485
|
await handler.undo({
|
|
327
486
|
input: log.commandPayload,
|
|
328
|
-
ctx,
|
|
487
|
+
ctx: replayCtx,
|
|
329
488
|
logEntry: log
|
|
330
489
|
});
|
|
331
|
-
|
|
490
|
+
const traceLog = this.buildUndoTraceLog(log, replayCtx);
|
|
491
|
+
if (replayEm) {
|
|
492
|
+
await service.markUndone(log.id, traceLog, replayEm);
|
|
493
|
+
} else {
|
|
494
|
+
await service.markUndone(log.id, traceLog);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
let ambientReplay = null;
|
|
498
|
+
let replayTransactionLifetime = null;
|
|
499
|
+
if (isAtomicReplay) {
|
|
500
|
+
ambientReplay = resolveAtomicReplayTransaction(ctx);
|
|
501
|
+
const replayEm = ambientReplay.em;
|
|
502
|
+
await withAtomicFlush(replayEm, [async () => {
|
|
503
|
+
const replayCtx = bindReplayTransactionContext(
|
|
504
|
+
ctx,
|
|
505
|
+
replayEm,
|
|
506
|
+
ambientReplay?.joinsAmbient ? ctx.transactionLifetime : void 0
|
|
507
|
+
);
|
|
508
|
+
replayTransactionLifetime = replayCtx.transactionLifetime ?? null;
|
|
509
|
+
if (handler.stabilizeReplay) {
|
|
510
|
+
await handler.stabilizeReplay({
|
|
511
|
+
operation: "undo",
|
|
512
|
+
input: log.commandPayload,
|
|
513
|
+
ctx: replayCtx,
|
|
514
|
+
logEntry: log
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
await this.runReplayTransactionGuard(replayCtx, "undo", log, replayEm);
|
|
518
|
+
const prepared = await runBeforeUndoInterceptors(replayCtx);
|
|
519
|
+
undoInterceptorMetadata = prepared.metadata;
|
|
520
|
+
userFeatures = prepared.userFeatures;
|
|
521
|
+
if (handler.authorizeReplay) {
|
|
522
|
+
await handler.authorizeReplay({
|
|
523
|
+
operation: "undo",
|
|
524
|
+
input: log.commandPayload,
|
|
525
|
+
ctx: replayCtx,
|
|
526
|
+
logEntry: log
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
await claimUndo(replayEm);
|
|
530
|
+
await finishUndo(replayCtx, replayEm);
|
|
531
|
+
}], {
|
|
532
|
+
transaction: true,
|
|
533
|
+
label: `${log.commandId}.undo`
|
|
534
|
+
});
|
|
535
|
+
} else {
|
|
536
|
+
try {
|
|
537
|
+
await claimUndo();
|
|
538
|
+
const prepared = await runBeforeUndoInterceptors(ctx);
|
|
539
|
+
undoInterceptorMetadata = prepared.metadata;
|
|
540
|
+
userFeatures = prepared.userFeatures;
|
|
541
|
+
await finishUndo(ctx);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
await service.releaseUndoClaim(log.id).catch(() => {
|
|
544
|
+
});
|
|
545
|
+
throw err;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const runPostCommit = async () => {
|
|
332
549
|
if (allInterceptors.length) {
|
|
333
550
|
const undoCtx = { input: log.commandPayload, logEntry: log, undoToken };
|
|
334
551
|
const interceptorCtx = {
|
|
@@ -348,11 +565,17 @@ class CommandBus {
|
|
|
348
565
|
}
|
|
349
566
|
await this.invalidateCacheAfterUndo(log, ctx);
|
|
350
567
|
await this.flushCrudSideEffects(ctx.container);
|
|
351
|
-
}
|
|
352
|
-
|
|
568
|
+
};
|
|
569
|
+
if (ambientReplay?.joinsAmbient) {
|
|
570
|
+
if (!replayTransactionLifetime || !ownsTransactionLifetime(ambientReplay.em, replayTransactionLifetime)) {
|
|
571
|
+
throw new Error("[internal] Ambient atomic replay lost its outer transaction lifetime owner");
|
|
572
|
+
}
|
|
573
|
+
onTransactionLifetimeComplete(ambientReplay.em, async (outcome) => {
|
|
574
|
+
if (outcome === "committed") await runPostCommit();
|
|
353
575
|
});
|
|
354
|
-
|
|
576
|
+
return;
|
|
355
577
|
}
|
|
578
|
+
await runPostCommit();
|
|
356
579
|
}
|
|
357
580
|
buildUndoTraceLog(log, ctx) {
|
|
358
581
|
const snapshotBefore = log.snapshotAfter ?? null;
|
|
@@ -385,18 +608,44 @@ class CommandBus {
|
|
|
385
608
|
}
|
|
386
609
|
async resolveUserFeaturesForInterceptors(ctx) {
|
|
387
610
|
if (!ctx.auth) return [];
|
|
611
|
+
let rbac;
|
|
612
|
+
try {
|
|
613
|
+
rbac = ctx.container.resolve("rbacService");
|
|
614
|
+
} catch {
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
const scope = {
|
|
618
|
+
tenantId: ctx.auth.tenantId,
|
|
619
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId
|
|
620
|
+
};
|
|
621
|
+
if (ctx.transactionalEm) {
|
|
622
|
+
if (!rbac?.getGrantedFeaturesWithEntityManager) return [];
|
|
623
|
+
return await rbac.getGrantedFeaturesWithEntityManager(
|
|
624
|
+
ctx.transactionalEm,
|
|
625
|
+
ctx.auth.sub,
|
|
626
|
+
scope
|
|
627
|
+
);
|
|
628
|
+
}
|
|
388
629
|
try {
|
|
389
|
-
const rbac = ctx.container.resolve("rbacService");
|
|
390
630
|
if (rbac?.getGrantedFeatures) {
|
|
391
|
-
return await rbac.getGrantedFeatures(ctx.auth.sub,
|
|
392
|
-
tenantId: ctx.auth.tenantId,
|
|
393
|
-
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId
|
|
394
|
-
});
|
|
631
|
+
return await rbac.getGrantedFeatures(ctx.auth.sub, scope);
|
|
395
632
|
}
|
|
396
633
|
} catch {
|
|
397
634
|
}
|
|
398
635
|
return [];
|
|
399
636
|
}
|
|
637
|
+
async runReplayTransactionGuard(ctx, operation, logEntry, transactionalEm) {
|
|
638
|
+
if (!ctx.replayTransactionGuard) return;
|
|
639
|
+
if (transactionalEm) {
|
|
640
|
+
await ctx.replayTransactionGuard({ operation, logEntry, transactionalEm });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const requestEm = ctx.container.resolve("em");
|
|
644
|
+
const guardEm = typeof requestEm.fork === "function" ? requestEm.fork() : requestEm;
|
|
645
|
+
await withAtomicFlush(guardEm, [async () => {
|
|
646
|
+
await ctx.replayTransactionGuard({ operation, logEntry, transactionalEm: guardEm });
|
|
647
|
+
}], { transaction: true, label: `${logEntry.commandId}.${operation}.request-guard` });
|
|
648
|
+
}
|
|
400
649
|
async resolveHandler(commandId) {
|
|
401
650
|
const handler = commandRegistry.get(commandId) ?? await commandRegistry.load(commandId);
|
|
402
651
|
if (!handler) {
|
|
@@ -436,6 +685,7 @@ class CommandBus {
|
|
|
436
685
|
if (!primary && !secondary) return null;
|
|
437
686
|
return {
|
|
438
687
|
skipLog: secondary?.skipLog ?? primary?.skipLog ?? false,
|
|
688
|
+
replayable: secondary?.replayable === false || primary?.replayable === false ? false : secondary?.replayable ?? primary?.replayable,
|
|
439
689
|
tenantId: secondary?.tenantId ?? primary?.tenantId ?? null,
|
|
440
690
|
organizationId: secondary?.organizationId ?? primary?.organizationId ?? null,
|
|
441
691
|
actorUserId: secondary?.actorUserId ?? primary?.actorUserId ?? null,
|
|
@@ -448,6 +698,9 @@ class CommandBus {
|
|
|
448
698
|
relatedResourceKind: secondary?.relatedResourceKind ?? primary?.relatedResourceKind ?? null,
|
|
449
699
|
relatedResourceId: secondary?.relatedResourceId ?? primary?.relatedResourceId ?? null,
|
|
450
700
|
undoToken: secondary?.undoToken ?? primary?.undoToken ?? null,
|
|
701
|
+
// A redacted redo input always wins: it exists precisely because the raw
|
|
702
|
+
// input carries a secret that must not reach `action_logs`.
|
|
703
|
+
redoInput: secondary?.redoInput !== void 0 ? secondary.redoInput : primary?.redoInput,
|
|
451
704
|
payload: secondary?.payload ?? primary?.payload ?? null,
|
|
452
705
|
snapshotBefore: secondary?.snapshotBefore ?? primary?.snapshotBefore ?? null,
|
|
453
706
|
snapshotAfter: secondary?.snapshotAfter ?? primary?.snapshotAfter ?? null,
|
|
@@ -505,9 +758,14 @@ class CommandBus {
|
|
|
505
758
|
const baseContext = asRecord(payload.context) ?? {};
|
|
506
759
|
payload.context = { ...baseContext, source: runAs.source };
|
|
507
760
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
761
|
+
if (metadata.replayable === false) {
|
|
762
|
+
delete payload.commandPayload;
|
|
763
|
+
} else {
|
|
764
|
+
const redoInput = metadata.redoInput !== void 0 ? metadata.redoInput : options.input;
|
|
765
|
+
const redoEnvelope = wrapRedoPayload("commandPayload" in payload ? payload.commandPayload : void 0, redoInput);
|
|
766
|
+
payload.commandPayload = redoEnvelope;
|
|
767
|
+
}
|
|
768
|
+
return options.ctx.transactionalEm ? await service.log(payload, options.ctx.transactionalEm) : await service.log(payload);
|
|
511
769
|
}
|
|
512
770
|
isUndoable(handler) {
|
|
513
771
|
return handler.isUndoable !== false && typeof handler.undo === "function";
|