@veloxts/router 0.7.9 → 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.
@@ -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 {};
@@ -0,0 +1,50 @@
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
+ /** @internal */
12
+ export function defineStep(nameOrOptions, handler) {
13
+ const resolved = typeof nameOrOptions === 'string'
14
+ ? { name: nameOrOptions, external: false }
15
+ : { name: nameOrOptions.name, external: nameOrOptions.external ?? false };
16
+ return createStep(resolved.name, resolved.external, handler, undefined);
17
+ }
18
+ // ============================================================================
19
+ // defineRevert
20
+ // ============================================================================
21
+ /**
22
+ * Define a revert action for compensating an external pipeline step
23
+ *
24
+ * Revert handlers receive the same `{ input, ctx }` shape but return void.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const refund = defineRevert('refundPayment', async ({ input, ctx }) => {
29
+ * await gateway.refund(input.chargeId);
30
+ * });
31
+ * ```
32
+ */
33
+ export function defineRevert(name, handler) {
34
+ return { name, handler };
35
+ }
36
+ // ============================================================================
37
+ // Internal helpers
38
+ // ============================================================================
39
+ /** @internal Create a PipelineStep object with onRevert method */
40
+ function createStep(name, external, handler, revertAction) {
41
+ return {
42
+ name,
43
+ external,
44
+ handler,
45
+ revertAction,
46
+ onRevert(revert) {
47
+ return createStep(name, external, handler, revert);
48
+ },
49
+ };
50
+ }