@veloxts/router 0.8.0 → 0.8.1
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/CHANGELOG.md +9 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/procedure/builder.d.ts +1 -1
- package/dist/procedure/builder.js +206 -17
- package/dist/procedure/pipeline-executor.d.ts +69 -0
- package/dist/procedure/pipeline-executor.js +134 -0
- package/dist/procedure/pipeline.d.ts +98 -0
- package/dist/procedure/pipeline.js +50 -0
- package/dist/procedure/types.d.ts +230 -19
- package/dist/types.d.ts +156 -10
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -39,14 +39,16 @@
|
|
|
39
39
|
*/
|
|
40
40
|
/** Router package version */
|
|
41
41
|
export declare const ROUTER_VERSION: string;
|
|
42
|
-
export type { CompiledProcedure, ContextExtensions, ContextFactory, ExtendedContext, GuardLike, HttpMethod, InferProcedureContext, InferProcedureInput, InferProcedureOutput, Middleware, MiddlewareArgs, MiddlewareFunction, MiddlewareNext, MiddlewareResult, ParentResourceChain, ParentResourceConfig, ProcedureCollection, ProcedureHandler, ProcedureHandlerArgs, ProcedureRecord, ProcedureType, RestRouteOverride, } from './types.js';
|
|
42
|
+
export type { AfterHandler, CompiledProcedure, ContextExtensions, ContextFactory, ExtendedContext, GuardLike, HttpMethod, InferProcedureContext, InferProcedureErrors, InferProcedureInput, InferProcedureOutput, Middleware, MiddlewareArgs, MiddlewareFunction, MiddlewareNext, MiddlewareResult, ParentResourceChain, ParentResourceConfig, PolicyActionLike, ProcedureCollection, ProcedureHandler, ProcedureHandlerArgs, ProcedureRecord, ProcedureType, RestRouteOverride, TransactionalOptions, } from './types.js';
|
|
43
43
|
export { PROCEDURE_METHOD_MAP, } from './types.js';
|
|
44
44
|
export type { GuardErrorResponse, RouterErrorCode } from './errors.js';
|
|
45
45
|
export { GuardError, isGuardError } from './errors.js';
|
|
46
46
|
export type { DefineProceduresOptions, } from './procedure/builder.js';
|
|
47
47
|
export { defineProcedures, executeProcedure, isCompiledProcedure, isProcedureCollection, procedure, procedures, } from './procedure/builder.js';
|
|
48
|
+
export type { PipelineStep, RevertAction, StepOptions } from './procedure/pipeline.js';
|
|
49
|
+
export { defineRevert, defineStep } from './procedure/pipeline.js';
|
|
48
50
|
export { createProcedure, typedProcedure } from './procedure/factory.js';
|
|
49
|
-
export type { BuilderRuntimeState, InferOutputSchema, InferProcedures, InferSchemaOutput, ProcedureBuilder, ProcedureBuilderState, ProcedureDefinitions, ValidOutputSchema, ValidSchema, } from './procedure/types.js';
|
|
51
|
+
export type { BuilderRuntimeState, InferOutputSchema, InferProcedures, InferSchemaOutput, PostHandlerBuilder, ProcedureBuilder, ProcedureBuilderState, ProcedureDefinitions, ValidOutputSchema, ValidSchema, } from './procedure/types.js';
|
|
50
52
|
export type { RouterResult } from './router-utils.js';
|
|
51
53
|
export { createRouter, toRouter } from './router-utils.js';
|
|
52
54
|
export type { NamingWarning, NamingWarningType, WarningConfig, WarningOption } from './warnings.js';
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,7 @@ export {
|
|
|
51
51
|
// Builder functions
|
|
52
52
|
defineProcedures, executeProcedure, isCompiledProcedure, isProcedureCollection, procedure, procedures, // Short alias for defineProcedures
|
|
53
53
|
} from './procedure/builder.js';
|
|
54
|
+
export { defineRevert, defineStep } from './procedure/pipeline.js';
|
|
54
55
|
// ============================================================================
|
|
55
56
|
// Router Utilities
|
|
56
57
|
// ============================================================================
|
|
@@ -41,7 +41,7 @@ import type { InferProcedures, ProcedureBuilder, ProcedureDefinitions } from './
|
|
|
41
41
|
* });
|
|
42
42
|
* ```
|
|
43
43
|
*/
|
|
44
|
-
export declare function procedure<TContext extends BaseContext = BaseContext>(): ProcedureBuilder<unknown, unknown, TContext>;
|
|
44
|
+
export declare function procedure<TContext extends BaseContext = BaseContext>(): ProcedureBuilder<unknown, unknown, TContext, never>;
|
|
45
45
|
/**
|
|
46
46
|
* Options for defining a procedure collection
|
|
47
47
|
*/
|
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module procedure/builder
|
|
9
9
|
*/
|
|
10
|
-
import { ConfigurationError, logWarning } from '@veloxts/core';
|
|
10
|
+
import { ConfigurationError, createLogger, ForbiddenError, logWarning, } from '@veloxts/core';
|
|
11
11
|
import { GuardError } from '../errors.js';
|
|
12
12
|
import { createMiddlewareExecutor, executeMiddlewareChain } from '../middleware/chain.js';
|
|
13
13
|
import { isResourceSchema, isTaggedResourceSchema, Resource, } from '../resource/index.js';
|
|
14
14
|
import { deriveParentParamName } from '../utils/pluralization.js';
|
|
15
15
|
import { analyzeNamingConvention, isDevelopment, normalizeWarningOption, } from '../warnings.js';
|
|
16
|
+
import { executeExternalSteps, executePipeline, splitPipelineSteps } from './pipeline-executor.js';
|
|
17
|
+
const log = createLogger('router');
|
|
16
18
|
// ============================================================================
|
|
17
19
|
// Builder Factory
|
|
18
20
|
// ============================================================================
|
|
@@ -158,6 +160,24 @@ function createBuilder(state) {
|
|
|
158
160
|
guards: [...state.guards, ...guardDefs],
|
|
159
161
|
});
|
|
160
162
|
},
|
|
163
|
+
/**
|
|
164
|
+
* Adds a policy action check to the procedure
|
|
165
|
+
*/
|
|
166
|
+
policy(action) {
|
|
167
|
+
return createBuilder({
|
|
168
|
+
...state,
|
|
169
|
+
policyAction: action,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
/**
|
|
173
|
+
* Declares domain error classes this procedure may throw
|
|
174
|
+
*/
|
|
175
|
+
throws(...errorClasses) {
|
|
176
|
+
return createBuilder({
|
|
177
|
+
...state,
|
|
178
|
+
errorClasses: [...(state.errorClasses ?? []), ...errorClasses],
|
|
179
|
+
});
|
|
180
|
+
},
|
|
161
181
|
/**
|
|
162
182
|
* Sets REST route override
|
|
163
183
|
*/
|
|
@@ -213,6 +233,37 @@ function createBuilder(state) {
|
|
|
213
233
|
parentResources: parentConfigs,
|
|
214
234
|
});
|
|
215
235
|
},
|
|
236
|
+
/**
|
|
237
|
+
* Declares a domain event to emit after successful handler execution
|
|
238
|
+
*/
|
|
239
|
+
emits(eventClass, mapper) {
|
|
240
|
+
// Cast is safe: the typed TEventData and TOutput are erased at runtime.
|
|
241
|
+
// BuilderRuntimeState stores the widened types for uniform handling.
|
|
242
|
+
const entry = { eventClass, mapper };
|
|
243
|
+
return createBuilder({
|
|
244
|
+
...state,
|
|
245
|
+
emittedEvents: [...(state.emittedEvents ?? []), entry],
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
/**
|
|
249
|
+
* Wraps the handler in a database transaction
|
|
250
|
+
*/
|
|
251
|
+
transactional(options) {
|
|
252
|
+
return createBuilder({
|
|
253
|
+
...state,
|
|
254
|
+
transactional: true,
|
|
255
|
+
transactionalOptions: options,
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
/**
|
|
259
|
+
* Adds pipeline steps that execute before the handler
|
|
260
|
+
*/
|
|
261
|
+
through(...steps) {
|
|
262
|
+
return createBuilder({
|
|
263
|
+
...state,
|
|
264
|
+
pipelineSteps: [...(state.pipelineSteps ?? []), ...steps],
|
|
265
|
+
});
|
|
266
|
+
},
|
|
216
267
|
/**
|
|
217
268
|
* Finalizes as a query procedure
|
|
218
269
|
*
|
|
@@ -278,8 +329,8 @@ function compileProcedure(type, handler, state) {
|
|
|
278
329
|
// Pre-compile the middleware chain executor if middlewares exist
|
|
279
330
|
// This avoids rebuilding the chain on every request
|
|
280
331
|
const precompiledExecutor = typedMiddlewares.length > 0 ? createMiddlewareExecutor(typedMiddlewares, handler) : undefined;
|
|
281
|
-
// Create the final procedure object
|
|
282
|
-
return {
|
|
332
|
+
// Create the final procedure object with .useAfter() support
|
|
333
|
+
return createPostHandlerBuilder({
|
|
283
334
|
type,
|
|
284
335
|
handler,
|
|
285
336
|
inputSchema: state.inputSchema,
|
|
@@ -298,7 +349,18 @@ function compileProcedure(type, handler, state) {
|
|
|
298
349
|
_resourceSchema: state.resourceSchema,
|
|
299
350
|
// Store explicit resource level from tagged schema (e.g., UserSchema.authenticated)
|
|
300
351
|
_resourceLevel: state.resourceLevel,
|
|
301
|
-
|
|
352
|
+
// Store error classes declared via .throws()
|
|
353
|
+
errorClasses: state.errorClasses,
|
|
354
|
+
// Store transactional configuration
|
|
355
|
+
transactional: state.transactional,
|
|
356
|
+
transactionalOptions: state.transactionalOptions,
|
|
357
|
+
// Store emitted events declared via .emits()
|
|
358
|
+
emittedEvents: state.emittedEvents,
|
|
359
|
+
// Store pipeline steps declared via .through()
|
|
360
|
+
pipelineSteps: state.pipelineSteps,
|
|
361
|
+
// Store policy action declared via .policy()
|
|
362
|
+
policyAction: state.policyAction,
|
|
363
|
+
});
|
|
302
364
|
}
|
|
303
365
|
/**
|
|
304
366
|
* Compiles a Level 3 branched procedure with a handler map
|
|
@@ -310,7 +372,7 @@ function compileProcedure(type, handler, state) {
|
|
|
310
372
|
*/
|
|
311
373
|
function compileProcedureWithHandlerMap(type, dispatchHandler, handlerMap, state) {
|
|
312
374
|
const typedMiddlewares = state.middlewares;
|
|
313
|
-
return {
|
|
375
|
+
return createPostHandlerBuilder({
|
|
314
376
|
type,
|
|
315
377
|
handler: dispatchHandler,
|
|
316
378
|
inputSchema: state.inputSchema,
|
|
@@ -327,6 +389,45 @@ function compileProcedureWithHandlerMap(type, dispatchHandler, handlerMap, state
|
|
|
327
389
|
_resourceSchema: state.resourceSchema,
|
|
328
390
|
_resourceLevel: undefined, // No fixed level — determined at runtime by branch selection
|
|
329
391
|
_handlerMap: handlerMap,
|
|
392
|
+
// Store error classes declared via .throws()
|
|
393
|
+
errorClasses: state.errorClasses,
|
|
394
|
+
// Store transactional configuration
|
|
395
|
+
transactional: state.transactional,
|
|
396
|
+
transactionalOptions: state.transactionalOptions,
|
|
397
|
+
// Store pipeline steps declared via .through()
|
|
398
|
+
pipelineSteps: state.pipelineSteps,
|
|
399
|
+
// Store policy action declared via .policy()
|
|
400
|
+
policyAction: state.policyAction,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// ============================================================================
|
|
404
|
+
// PostHandlerBuilder Factory
|
|
405
|
+
// ============================================================================
|
|
406
|
+
/**
|
|
407
|
+
* Creates a PostHandlerBuilder from a CompiledProcedure's fields
|
|
408
|
+
*
|
|
409
|
+
* Wraps a compiled procedure object with a `.useAfter()` method that
|
|
410
|
+
* appends after-handler hooks. Each call returns a new PostHandlerBuilder
|
|
411
|
+
* with the hook added, preserving immutability.
|
|
412
|
+
*
|
|
413
|
+
* The resulting object passes `isCompiledProcedure` because it retains
|
|
414
|
+
* all required fields (type, handler, middlewares, guards).
|
|
415
|
+
*
|
|
416
|
+
* @internal
|
|
417
|
+
*/
|
|
418
|
+
function createPostHandlerBuilder(compiled) {
|
|
419
|
+
return {
|
|
420
|
+
...compiled,
|
|
421
|
+
useAfter(handler) {
|
|
422
|
+
const afterHandlers = [
|
|
423
|
+
...(compiled.afterHandlers ?? []),
|
|
424
|
+
handler,
|
|
425
|
+
];
|
|
426
|
+
return createPostHandlerBuilder({
|
|
427
|
+
...compiled,
|
|
428
|
+
afterHandlers,
|
|
429
|
+
});
|
|
430
|
+
},
|
|
330
431
|
};
|
|
331
432
|
}
|
|
332
433
|
/**
|
|
@@ -510,25 +611,101 @@ export async function executeProcedure(procedure, rawInput, ctx) {
|
|
|
510
611
|
const input = procedure.inputSchema
|
|
511
612
|
? procedure.inputSchema.parse(rawInput)
|
|
512
613
|
: rawInput;
|
|
614
|
+
// Step 2.3: Policy check — if .policy() was used
|
|
615
|
+
if (procedure.policyAction) {
|
|
616
|
+
const ctxRecord = ctxWithLevel;
|
|
617
|
+
const user = ctxRecord.user;
|
|
618
|
+
if (user == null) {
|
|
619
|
+
throw new ForbiddenError('Policy check failed: no authenticated user');
|
|
620
|
+
}
|
|
621
|
+
const resourceNameRaw = procedure.policyAction.resourceName;
|
|
622
|
+
const resourceName = resourceNameRaw.charAt(0).toLowerCase() + resourceNameRaw.slice(1);
|
|
623
|
+
const policyResource = ctxRecord[resourceName];
|
|
624
|
+
const allowed = await procedure.policyAction.check(user, policyResource);
|
|
625
|
+
if (!allowed) {
|
|
626
|
+
throw new ForbiddenError(`Policy check failed: cannot ${procedure.policyAction.actionName} ${procedure.policyAction.resourceName}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
513
629
|
// Step 2.5: Level 3 branch selection — if handler map is present
|
|
514
630
|
if (procedure._handlerMap) {
|
|
515
631
|
return executeBranchedProcedure(procedure, input, ctxWithLevel);
|
|
516
632
|
}
|
|
633
|
+
// Step 2.7: Execute pipeline steps if .through() was used
|
|
634
|
+
// Pipeline transforms input before the handler. Runs inside transaction when applicable.
|
|
635
|
+
const pipelineSteps = procedure.pipelineSteps;
|
|
636
|
+
const hasPipeline = pipelineSteps !== undefined && pipelineSteps.length > 0;
|
|
517
637
|
// Step 3: Execute handler (with or without middleware)
|
|
638
|
+
// Helper that runs a given set of pipeline steps + middleware chain + handler
|
|
639
|
+
const executeWithSteps = async (steps, execCtx) => {
|
|
640
|
+
const hasSteps = steps !== undefined && steps.length > 0;
|
|
641
|
+
// Run pipeline to transform input before handler
|
|
642
|
+
const handlerInput = hasSteps
|
|
643
|
+
? (await executePipeline(steps, input, execCtx))
|
|
644
|
+
: input;
|
|
645
|
+
enrichedInput = handlerInput;
|
|
646
|
+
if (procedure._precompiledExecutor) {
|
|
647
|
+
// PERFORMANCE: Use pre-compiled middleware chain executor
|
|
648
|
+
return procedure._precompiledExecutor(handlerInput, execCtx);
|
|
649
|
+
}
|
|
650
|
+
if (procedure.middlewares.length === 0) {
|
|
651
|
+
// No middleware - execute handler directly
|
|
652
|
+
return procedure.handler({ input: handlerInput, ctx: execCtx });
|
|
653
|
+
}
|
|
654
|
+
// Fallback: Build middleware chain dynamically (should not normally happen)
|
|
655
|
+
return executeMiddlewareChain(procedure.middlewares, handlerInput, execCtx, async () => procedure.handler({ input: handlerInput, ctx: execCtx }));
|
|
656
|
+
};
|
|
518
657
|
let result;
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
658
|
+
let enrichedInput = input;
|
|
659
|
+
// Wrap in transaction if .transactional() was called and ctx.db.$transaction exists
|
|
660
|
+
const ctxRecord = ctxWithLevel;
|
|
661
|
+
const db = ctxRecord.db;
|
|
662
|
+
const isTransactional = procedure.transactional && db && typeof db.$transaction === 'function';
|
|
663
|
+
if (isTransactional) {
|
|
664
|
+
const $transaction = db.$transaction;
|
|
665
|
+
if (hasPipeline) {
|
|
666
|
+
// Two-phase model: split steps into DB (inside tx) and external (after commit)
|
|
667
|
+
const { dbSteps, externalSteps } = splitPipelineSteps(pipelineSteps);
|
|
668
|
+
// Phase A: Run DB steps + handler inside transaction
|
|
669
|
+
result = await $transaction(async (tx) => {
|
|
670
|
+
const txCtx = { ...ctxWithLevel, db: tx };
|
|
671
|
+
return executeWithSteps(dbSteps.length > 0 ? dbSteps : undefined, txCtx);
|
|
672
|
+
}, procedure.transactionalOptions);
|
|
673
|
+
// Phase B: Run external steps after commit (outside transaction)
|
|
674
|
+
if (externalSteps.length > 0) {
|
|
675
|
+
await executeExternalSteps(externalSteps, input, ctxWithLevel);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
// Transactional, no pipeline — wrap handler in $transaction (current behavior)
|
|
680
|
+
result = await $transaction(async (tx) => {
|
|
681
|
+
const txCtx = { ...ctxWithLevel, db: tx };
|
|
682
|
+
return executeWithSteps(undefined, txCtx);
|
|
683
|
+
}, procedure.transactionalOptions);
|
|
684
|
+
}
|
|
526
685
|
}
|
|
527
686
|
else {
|
|
528
|
-
//
|
|
529
|
-
result = await
|
|
687
|
+
// Not transactional — run all steps in declaration order (regardless of external flag)
|
|
688
|
+
result = await executeWithSteps(hasPipeline ? pipelineSteps : undefined, ctxWithLevel);
|
|
530
689
|
}
|
|
531
|
-
// Step 4:
|
|
690
|
+
// Step 4: Emit domain events if .emits() was used
|
|
691
|
+
if (procedure.emittedEvents) {
|
|
692
|
+
const ctxEvents = ctxRecord
|
|
693
|
+
.events;
|
|
694
|
+
if (ctxEvents?.emit) {
|
|
695
|
+
for (const { eventClass, mapper } of procedure.emittedEvents) {
|
|
696
|
+
try {
|
|
697
|
+
const eventData = mapper
|
|
698
|
+
? mapper(result)
|
|
699
|
+
: result;
|
|
700
|
+
await ctxEvents.emit(new eventClass(eventData));
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
log.error('Event emission error:', error);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// Step 5: Auto-project if resource schema is set
|
|
532
709
|
if (procedure._resourceSchema) {
|
|
533
710
|
const schema = procedure._resourceSchema;
|
|
534
711
|
// Prefer explicit level from tagged schema over guard-derived level
|
|
@@ -543,10 +720,22 @@ export async function executeProcedure(procedure, rawInput, ctx) {
|
|
|
543
720
|
result = projectOne(result);
|
|
544
721
|
}
|
|
545
722
|
}
|
|
546
|
-
// Step
|
|
723
|
+
// Step 6: Validate output if schema provided
|
|
547
724
|
if (procedure.outputSchema) {
|
|
548
|
-
|
|
725
|
+
result = procedure.outputSchema.parse(result);
|
|
726
|
+
}
|
|
727
|
+
// Step 7: Execute after-handler hooks if .useAfter() was used
|
|
728
|
+
if (procedure.afterHandlers?.length) {
|
|
729
|
+
for (const afterHandler of procedure.afterHandlers) {
|
|
730
|
+
try {
|
|
731
|
+
await afterHandler({ input: enrichedInput, result, ctx: ctxWithLevel });
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
log.error('useAfter hook error:', error);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
549
737
|
}
|
|
738
|
+
// Return the ORIGINAL result (hooks cannot modify it)
|
|
550
739
|
return result;
|
|
551
740
|
}
|
|
552
741
|
// ============================================================================
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline executor for .through() steps
|
|
3
|
+
*
|
|
4
|
+
* Executes pipeline steps in declaration order. Each step's output becomes
|
|
5
|
+
* the next step's input. On failure, runs revert actions for completed
|
|
6
|
+
* steps in reverse order (compensation pattern).
|
|
7
|
+
*
|
|
8
|
+
* When `.transactional()` is combined with `.through()`, the executor
|
|
9
|
+
* implements a two-phase model:
|
|
10
|
+
* - Phase A: DB steps (non-external) run inside the transaction
|
|
11
|
+
* - Phase B: External steps run after the transaction commits
|
|
12
|
+
*
|
|
13
|
+
* @module procedure/pipeline-executor
|
|
14
|
+
*/
|
|
15
|
+
import type { BaseContext } from '@veloxts/core';
|
|
16
|
+
import type { PipelineStep } from './pipeline.js';
|
|
17
|
+
/**
|
|
18
|
+
* Result of splitting pipeline steps into DB and external phases
|
|
19
|
+
*/
|
|
20
|
+
export interface SplitPipelineSteps {
|
|
21
|
+
/** Steps that run inside the DB transaction (external === false) */
|
|
22
|
+
readonly dbSteps: ReadonlyArray<PipelineStep>;
|
|
23
|
+
/** Steps that run after transaction commit (external === true) */
|
|
24
|
+
readonly externalSteps: ReadonlyArray<PipelineStep>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Splits pipeline steps into DB (non-external) and external phases
|
|
28
|
+
*
|
|
29
|
+
* Validates the ordering constraint: when transactional, all external steps
|
|
30
|
+
* must come AFTER all DB steps. If an external step appears before a DB step,
|
|
31
|
+
* a ConfigurationError is thrown.
|
|
32
|
+
*
|
|
33
|
+
* @param steps - Pipeline steps to split
|
|
34
|
+
* @returns Object with `dbSteps` and `externalSteps` arrays
|
|
35
|
+
* @throws ConfigurationError if an external step precedes a DB step
|
|
36
|
+
*/
|
|
37
|
+
export declare function splitPipelineSteps(steps: ReadonlyArray<PipelineStep>): SplitPipelineSteps;
|
|
38
|
+
/**
|
|
39
|
+
* Executes a sequence of pipeline steps
|
|
40
|
+
*
|
|
41
|
+
* Steps run in declaration order. Each step receives the previous step's
|
|
42
|
+
* output as its input (the first step receives the original validated input).
|
|
43
|
+
*
|
|
44
|
+
* If a step fails:
|
|
45
|
+
* 1. Collects all completed steps that have a revertAction
|
|
46
|
+
* 2. Runs their revert handlers in REVERSE order
|
|
47
|
+
* 3. Each revert receives the output of the step being reverted
|
|
48
|
+
* 4. Revert errors are logged but do not suppress the original error
|
|
49
|
+
* 5. Rethrows the original error
|
|
50
|
+
*
|
|
51
|
+
* @param steps - Pipeline steps to execute in order
|
|
52
|
+
* @param input - Initial input (typically the validated procedure input)
|
|
53
|
+
* @param ctx - Request context
|
|
54
|
+
* @returns The output of the last step
|
|
55
|
+
*/
|
|
56
|
+
export declare function executePipeline(steps: ReadonlyArray<PipelineStep>, input: unknown, ctx: BaseContext): Promise<unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Executes external pipeline steps after a transaction has committed
|
|
59
|
+
*
|
|
60
|
+
* External steps run in declaration order. If a step fails, revert actions
|
|
61
|
+
* are run for all previously completed EXTERNAL steps in reverse order.
|
|
62
|
+
* The DB transaction is already committed — DB changes persist.
|
|
63
|
+
*
|
|
64
|
+
* @param steps - External pipeline steps to execute in order
|
|
65
|
+
* @param input - Input for the first external step (output of last DB step, or original input)
|
|
66
|
+
* @param ctx - Request context (with the ORIGINAL db, not the transactional client)
|
|
67
|
+
* @returns The output of the last external step (ignored by the caller, since handler already ran)
|
|
68
|
+
*/
|
|
69
|
+
export declare function executeExternalSteps(steps: ReadonlyArray<PipelineStep>, input: unknown, ctx: BaseContext): Promise<void>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline executor for .through() steps
|
|
3
|
+
*
|
|
4
|
+
* Executes pipeline steps in declaration order. Each step's output becomes
|
|
5
|
+
* the next step's input. On failure, runs revert actions for completed
|
|
6
|
+
* steps in reverse order (compensation pattern).
|
|
7
|
+
*
|
|
8
|
+
* When `.transactional()` is combined with `.through()`, the executor
|
|
9
|
+
* implements a two-phase model:
|
|
10
|
+
* - Phase A: DB steps (non-external) run inside the transaction
|
|
11
|
+
* - Phase B: External steps run after the transaction commits
|
|
12
|
+
*
|
|
13
|
+
* @module procedure/pipeline-executor
|
|
14
|
+
*/
|
|
15
|
+
import { ConfigurationError, createLogger } from '@veloxts/core';
|
|
16
|
+
const log = createLogger('router');
|
|
17
|
+
/**
|
|
18
|
+
* Splits pipeline steps into DB (non-external) and external phases
|
|
19
|
+
*
|
|
20
|
+
* Validates the ordering constraint: when transactional, all external steps
|
|
21
|
+
* must come AFTER all DB steps. If an external step appears before a DB step,
|
|
22
|
+
* a ConfigurationError is thrown.
|
|
23
|
+
*
|
|
24
|
+
* @param steps - Pipeline steps to split
|
|
25
|
+
* @returns Object with `dbSteps` and `externalSteps` arrays
|
|
26
|
+
* @throws ConfigurationError if an external step precedes a DB step
|
|
27
|
+
*/
|
|
28
|
+
export function splitPipelineSteps(steps) {
|
|
29
|
+
const dbSteps = [];
|
|
30
|
+
const externalSteps = [];
|
|
31
|
+
let seenExternal = false;
|
|
32
|
+
for (const step of steps) {
|
|
33
|
+
if (step.external) {
|
|
34
|
+
seenExternal = true;
|
|
35
|
+
externalSteps.push(step);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
if (seenExternal) {
|
|
39
|
+
throw new ConfigurationError(`Pipeline step "${step.name}" (DB) is declared after external step "${externalSteps[externalSteps.length - 1].name}". ` +
|
|
40
|
+
'When using .transactional(), all external steps must come after all DB steps in the .through() declaration.');
|
|
41
|
+
}
|
|
42
|
+
dbSteps.push(step);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { dbSteps, externalSteps };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Executes a sequence of pipeline steps
|
|
49
|
+
*
|
|
50
|
+
* Steps run in declaration order. Each step receives the previous step's
|
|
51
|
+
* output as its input (the first step receives the original validated input).
|
|
52
|
+
*
|
|
53
|
+
* If a step fails:
|
|
54
|
+
* 1. Collects all completed steps that have a revertAction
|
|
55
|
+
* 2. Runs their revert handlers in REVERSE order
|
|
56
|
+
* 3. Each revert receives the output of the step being reverted
|
|
57
|
+
* 4. Revert errors are logged but do not suppress the original error
|
|
58
|
+
* 5. Rethrows the original error
|
|
59
|
+
*
|
|
60
|
+
* @param steps - Pipeline steps to execute in order
|
|
61
|
+
* @param input - Initial input (typically the validated procedure input)
|
|
62
|
+
* @param ctx - Request context
|
|
63
|
+
* @returns The output of the last step
|
|
64
|
+
*/
|
|
65
|
+
export async function executePipeline(steps, input, ctx) {
|
|
66
|
+
const completedSteps = [];
|
|
67
|
+
let currentInput = input;
|
|
68
|
+
for (const step of steps) {
|
|
69
|
+
try {
|
|
70
|
+
const output = await step.handler({ input: currentInput, ctx });
|
|
71
|
+
completedSteps.push({ step, output });
|
|
72
|
+
currentInput = output;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// Step failed — run reverts for completed steps in reverse order
|
|
76
|
+
await runReverts(completedSteps, ctx);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return currentInput;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Executes external pipeline steps after a transaction has committed
|
|
84
|
+
*
|
|
85
|
+
* External steps run in declaration order. If a step fails, revert actions
|
|
86
|
+
* are run for all previously completed EXTERNAL steps in reverse order.
|
|
87
|
+
* The DB transaction is already committed — DB changes persist.
|
|
88
|
+
*
|
|
89
|
+
* @param steps - External pipeline steps to execute in order
|
|
90
|
+
* @param input - Input for the first external step (output of last DB step, or original input)
|
|
91
|
+
* @param ctx - Request context (with the ORIGINAL db, not the transactional client)
|
|
92
|
+
* @returns The output of the last external step (ignored by the caller, since handler already ran)
|
|
93
|
+
*/
|
|
94
|
+
export async function executeExternalSteps(steps, input, ctx) {
|
|
95
|
+
const completedSteps = [];
|
|
96
|
+
let currentInput = input;
|
|
97
|
+
for (const step of steps) {
|
|
98
|
+
try {
|
|
99
|
+
const output = await step.handler({ input: currentInput, ctx });
|
|
100
|
+
completedSteps.push({ step, output });
|
|
101
|
+
currentInput = output;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// External step failed — revert only completed external steps
|
|
105
|
+
await runReverts(completedSteps, ctx);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Runs revert actions for completed steps in reverse order
|
|
112
|
+
*
|
|
113
|
+
* Each revert receives the output of the step it is reverting.
|
|
114
|
+
* Revert errors are logged but never suppress the original error.
|
|
115
|
+
*
|
|
116
|
+
* @param completedSteps - Steps that completed successfully before the failure
|
|
117
|
+
* @param ctx - Request context (forwarded to revert handlers)
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
async function runReverts(completedSteps, ctx) {
|
|
121
|
+
// Process in reverse order
|
|
122
|
+
for (let i = completedSteps.length - 1; i >= 0; i--) {
|
|
123
|
+
const { step, output } = completedSteps[i];
|
|
124
|
+
if (!step.revertAction) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
await step.revertAction.handler({ input: output, ctx });
|
|
129
|
+
}
|
|
130
|
+
catch (revertError) {
|
|
131
|
+
log.error(`Revert "${step.revertAction.name}" for step "${step.name}" failed:`, revertError);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline step and revert action factories
|
|
3
|
+
*
|
|
4
|
+
* Provides `defineStep` and `defineRevert` for building pipeline steps
|
|
5
|
+
* that execute in sequence via `.through()` on the procedure builder.
|
|
6
|
+
* Each step's output becomes the next step's input. External steps run
|
|
7
|
+
* outside DB transactions and can have revert actions for compensation.
|
|
8
|
+
*
|
|
9
|
+
* @module procedure/pipeline
|
|
10
|
+
*/
|
|
11
|
+
import type { BaseContext } from '@veloxts/core';
|
|
12
|
+
/**
|
|
13
|
+
* Options for configuring a pipeline step
|
|
14
|
+
*/
|
|
15
|
+
export interface StepOptions {
|
|
16
|
+
/** Step name used for logging and error reporting */
|
|
17
|
+
readonly name: string;
|
|
18
|
+
/** Whether this step runs outside the DB transaction (default: false) */
|
|
19
|
+
readonly external?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A revert action that undoes the effect of an external step
|
|
23
|
+
*
|
|
24
|
+
* Revert actions are invoked when a later step in the pipeline fails,
|
|
25
|
+
* allowing compensation for steps that ran outside the DB transaction.
|
|
26
|
+
*/
|
|
27
|
+
export interface RevertAction<TInput = unknown> {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly handler: (params: {
|
|
30
|
+
input: TInput;
|
|
31
|
+
ctx: BaseContext;
|
|
32
|
+
}) => void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A single step in a procedure pipeline
|
|
36
|
+
*
|
|
37
|
+
* Steps execute in order, each one's output becoming the next one's input.
|
|
38
|
+
* External steps run outside DB transactions and may have revert actions
|
|
39
|
+
* for compensation when a later step fails.
|
|
40
|
+
*/
|
|
41
|
+
export interface PipelineStep<TInput = unknown, TOutput = unknown> {
|
|
42
|
+
readonly name: string;
|
|
43
|
+
readonly external: boolean;
|
|
44
|
+
readonly handler: (params: {
|
|
45
|
+
input: TInput;
|
|
46
|
+
ctx: BaseContext;
|
|
47
|
+
}) => TOutput | Promise<TOutput>;
|
|
48
|
+
readonly revertAction?: RevertAction;
|
|
49
|
+
/** Attach a revert action to this step, returning a new step (immutable) */
|
|
50
|
+
onRevert(revert: RevertAction): PipelineStep<TInput, TOutput>;
|
|
51
|
+
}
|
|
52
|
+
/** Handler function signature for pipeline steps */
|
|
53
|
+
type StepHandler<TInput, TOutput> = (params: {
|
|
54
|
+
input: TInput;
|
|
55
|
+
ctx: BaseContext;
|
|
56
|
+
}) => TOutput | Promise<TOutput>;
|
|
57
|
+
/**
|
|
58
|
+
* Define a pipeline step with a string name (external defaults to false)
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const validate = defineStep('validateInventory', async ({ input, ctx }) => {
|
|
63
|
+
* return { ...input, validated: true };
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare function defineStep<TInput = unknown, TOutput = unknown>(name: string, handler: StepHandler<TInput, TOutput>): PipelineStep<TInput, TOutput>;
|
|
68
|
+
/**
|
|
69
|
+
* Define a pipeline step with options (name + external flag)
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const charge = defineStep(
|
|
74
|
+
* { name: 'chargePayment', external: true },
|
|
75
|
+
* async ({ input, ctx }) => {
|
|
76
|
+
* return { ...input, chargeId: 'ch_123' };
|
|
77
|
+
* },
|
|
78
|
+
* );
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function defineStep<TInput = unknown, TOutput = unknown>(options: StepOptions, handler: StepHandler<TInput, TOutput>): PipelineStep<TInput, TOutput>;
|
|
82
|
+
/**
|
|
83
|
+
* Define a revert action for compensating an external pipeline step
|
|
84
|
+
*
|
|
85
|
+
* Revert handlers receive the same `{ input, ctx }` shape but return void.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const refund = defineRevert('refundPayment', async ({ input, ctx }) => {
|
|
90
|
+
* await gateway.refund(input.chargeId);
|
|
91
|
+
* });
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export declare function defineRevert<TInput = unknown>(name: string, handler: (params: {
|
|
95
|
+
input: TInput;
|
|
96
|
+
ctx: BaseContext;
|
|
97
|
+
}) => void | Promise<void>): RevertAction<TInput>;
|
|
98
|
+
export {};
|