@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.
@@ -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
- import { isTaggedResourceSchema, Resource, } from '../resource/index.js';
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
  // ============================================================================
@@ -85,24 +87,41 @@ function createBuilder(state) {
85
87
  });
86
88
  },
87
89
  /**
88
- * Sets the output validation schema (Zod-only)
90
+ * Sets the output schema.
91
+ *
92
+ * Accepts either:
93
+ * - A Zod schema (Level 1) — validates output after handler
94
+ * - A tagged resource view (Level 2) — auto-projects handler result
95
+ * through the tagged level's field visibility
89
96
  */
90
97
  output(schema) {
98
+ // Level 2: tagged resource view — set up auto-projection
99
+ if (isTaggedResourceSchema(schema)) {
100
+ return createBuilder({
101
+ ...state,
102
+ resourceSchema: schema,
103
+ resourceLevel: schema._level,
104
+ outputSchema: undefined,
105
+ branchingMode: undefined,
106
+ });
107
+ }
108
+ // Level 3: untagged resource schema — enables branching mode
109
+ if (isResourceSchema(schema)) {
110
+ return createBuilder({
111
+ ...state,
112
+ resourceSchema: schema,
113
+ resourceLevel: undefined,
114
+ outputSchema: undefined,
115
+ branchingMode: true,
116
+ });
117
+ }
118
+ // Level 1: plain Zod schema — validate output
91
119
  return createBuilder({
92
120
  ...state,
93
121
  outputSchema: schema,
94
- });
95
- },
96
- /**
97
- * Sets field-level visibility via a resource schema
98
- */
99
- expose(schema) {
100
- const level = isTaggedResourceSchema(schema) ? schema._level : undefined;
101
- return createBuilder({
102
- ...state,
103
- resourceSchema: schema,
104
- resourceLevel: level,
105
- outputSchema: undefined,
122
+ resourceSchema: undefined,
123
+ resourceLevel: undefined,
124
+ branchingMode: undefined,
106
125
  });
107
126
  },
108
127
  /**
@@ -130,27 +149,33 @@ function createBuilder(state) {
130
149
  });
131
150
  },
132
151
  /**
133
- * Adds an authorization guard with type narrowing (EXPERIMENTAL)
152
+ * Adds multiple authorization guards at once
134
153
  *
135
- * Unlike `guard()`, this method narrows the context type based on
136
- * what the guard guarantees after it passes.
154
+ * Convenience method equivalent to chaining multiple `.guard()` calls.
155
+ * Guards execute left-to-right. All must pass for the procedure to execute.
137
156
  */
138
- guardNarrow(guardDef) {
157
+ guards(...guardDefs) {
139
158
  return createBuilder({
140
159
  ...state,
141
- guards: [...state.guards, guardDef],
160
+ guards: [...state.guards, ...guardDefs],
142
161
  });
143
162
  },
144
163
  /**
145
- * Adds multiple authorization guards at once
146
- *
147
- * Convenience method equivalent to chaining multiple `.guard()` calls.
148
- * Guards execute left-to-right. All must pass for the procedure to execute.
164
+ * Adds a policy action check to the procedure
149
165
  */
150
- guards(...guardDefs) {
166
+ policy(action) {
151
167
  return createBuilder({
152
168
  ...state,
153
- guards: [...state.guards, ...guardDefs],
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],
154
179
  });
155
180
  },
156
181
  /**
@@ -209,31 +234,87 @@ function createBuilder(state) {
209
234
  });
210
235
  },
211
236
  /**
212
- * Finalizes as a query procedure
237
+ * Declares a domain event to emit after successful handler execution
213
238
  */
214
- query(handler) {
215
- return compileProcedure('query', handler, state);
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
+ });
216
247
  },
217
248
  /**
218
- * Finalizes as a mutation procedure
249
+ * Wraps the handler in a database transaction
219
250
  */
220
- mutation(handler) {
221
- return compileProcedure('mutation', handler, state);
251
+ transactional(options) {
252
+ return createBuilder({
253
+ ...state,
254
+ transactional: true,
255
+ transactionalOptions: options,
256
+ });
222
257
  },
223
258
  /**
224
- * @deprecated Use `.expose()` instead. `.resource()` will be removed in v1.0.
259
+ * Adds pipeline steps that execute before the handler
225
260
  */
226
- resource(schema) {
227
- const level = isTaggedResourceSchema(schema) ? schema._level : undefined;
261
+ through(...steps) {
228
262
  return createBuilder({
229
263
  ...state,
230
- resourceSchema: schema,
231
- resourceLevel: level,
232
- outputSchema: undefined,
264
+ pipelineSteps: [...(state.pipelineSteps ?? []), ...steps],
233
265
  });
234
266
  },
267
+ /**
268
+ * Finalizes as a query procedure
269
+ *
270
+ * In branching mode (Level 3), accepts a handler map keyed by schema level keys.
271
+ */
272
+ query(handlerOrMap) {
273
+ return compileProcedureOrBranching('query', handlerOrMap, state);
274
+ },
275
+ /**
276
+ * Finalizes as a mutation procedure
277
+ *
278
+ * In branching mode (Level 3), accepts a handler map keyed by schema level keys.
279
+ */
280
+ mutation(handlerOrMap) {
281
+ return compileProcedureOrBranching('mutation', handlerOrMap, state);
282
+ },
235
283
  };
236
284
  }
285
+ /**
286
+ * Routes to the correct compile strategy based on branching mode
287
+ *
288
+ * In branching mode (Level 3), validates the handler map and synthesizes
289
+ * a dispatch handler. In normal mode, delegates to compileProcedure.
290
+ *
291
+ * @internal
292
+ */
293
+ function compileProcedureOrBranching(type, handlerOrMap, state) {
294
+ if (state.branchingMode) {
295
+ // Level 3: handler map required
296
+ if (typeof handlerOrMap === 'function') {
297
+ throw new ConfigurationError('Handler map required when .output() receives a resource schema. ' +
298
+ 'Use { [Schema.level.key]: handler } syntax.');
299
+ }
300
+ if (state.guards.length > 0) {
301
+ throw new ConfigurationError('Cannot use handler map with .guard(). ' +
302
+ 'Guards come from defineAccessLevels() in Level 3 branching mode.');
303
+ }
304
+ // Synthesize a dispatch handler so CompiledProcedure.handler is always defined.
305
+ // The real branch selection happens in executeProcedure.
306
+ const dispatchHandler = async () => {
307
+ throw new ConfigurationError('Level 3 branched procedures must be executed via executeProcedure(). ' +
308
+ 'Direct handler invocation is not supported.');
309
+ };
310
+ return compileProcedureWithHandlerMap(type, dispatchHandler, handlerOrMap, state);
311
+ }
312
+ // Not in branching mode: handler map is not allowed
313
+ if (typeof handlerOrMap !== 'function') {
314
+ throw new ConfigurationError('Handler map can only be used when .output() receives an untagged resource schema.');
315
+ }
316
+ return compileProcedure(type, handlerOrMap, state);
317
+ }
237
318
  /**
238
319
  * Compiles a procedure from the builder state
239
320
  *
@@ -248,8 +329,8 @@ function compileProcedure(type, handler, state) {
248
329
  // Pre-compile the middleware chain executor if middlewares exist
249
330
  // This avoids rebuilding the chain on every request
250
331
  const precompiledExecutor = typedMiddlewares.length > 0 ? createMiddlewareExecutor(typedMiddlewares, handler) : undefined;
251
- // Create the final procedure object
252
- return {
332
+ // Create the final procedure object with .useAfter() support
333
+ return createPostHandlerBuilder({
253
334
  type,
254
335
  handler,
255
336
  inputSchema: state.inputSchema,
@@ -268,6 +349,85 @@ function compileProcedure(type, handler, state) {
268
349
  _resourceSchema: state.resourceSchema,
269
350
  // Store explicit resource level from tagged schema (e.g., UserSchema.authenticated)
270
351
  _resourceLevel: state.resourceLevel,
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
+ });
364
+ }
365
+ /**
366
+ * Compiles a Level 3 branched procedure with a handler map
367
+ *
368
+ * Creates a CompiledProcedure with both a synthesized dispatch handler
369
+ * (so .handler is always defined) and the _handlerMap for branch selection.
370
+ *
371
+ * @internal
372
+ */
373
+ function compileProcedureWithHandlerMap(type, dispatchHandler, handlerMap, state) {
374
+ const typedMiddlewares = state.middlewares;
375
+ return createPostHandlerBuilder({
376
+ type,
377
+ handler: dispatchHandler,
378
+ inputSchema: state.inputSchema,
379
+ outputSchema: undefined, // Level 3 uses resource schema projection, not Zod output validation
380
+ middlewares: typedMiddlewares,
381
+ guards: [], // Guards come from access level config
382
+ restOverride: state.restOverride,
383
+ deprecated: state.deprecated,
384
+ deprecationMessage: state.deprecationMessage,
385
+ isWebhook: state.isWebhook,
386
+ parentResource: state.parentResource,
387
+ parentResources: state.parentResources,
388
+ _precompiledExecutor: undefined, // Branched procedures don't use precompiled chains
389
+ _resourceSchema: state.resourceSchema,
390
+ _resourceLevel: undefined, // No fixed level — determined at runtime by branch selection
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
+ },
271
431
  };
272
432
  }
273
433
  /**
@@ -437,7 +597,7 @@ export async function executeProcedure(procedure, rawInput, ctx) {
437
597
  // IMPORTANT: last guard's accessLevel wins. With custom levels that
438
598
  // have no inherent hierarchy, ordering of guards matters.
439
599
  // Guards without accessLevel (e.g. plain `authenticated`) do NOT
440
- // update the level — it stays at 'public' for .expose() projection.
600
+ // update the level — it stays at 'public' for .output() projection.
441
601
  const guardWithLevel = guard;
442
602
  if (guardWithLevel.accessLevel) {
443
603
  accessLevel = guardWithLevel.accessLevel;
@@ -451,21 +611,101 @@ export async function executeProcedure(procedure, rawInput, ctx) {
451
611
  const input = procedure.inputSchema
452
612
  ? procedure.inputSchema.parse(rawInput)
453
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
+ }
629
+ // Step 2.5: Level 3 branch selection — if handler map is present
630
+ if (procedure._handlerMap) {
631
+ return executeBranchedProcedure(procedure, input, ctxWithLevel);
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;
454
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
+ };
455
657
  let result;
456
- if (procedure._precompiledExecutor) {
457
- // PERFORMANCE: Use pre-compiled middleware chain executor
458
- result = await procedure._precompiledExecutor(input, ctxWithLevel);
459
- }
460
- else if (procedure.middlewares.length === 0) {
461
- // No middleware - execute handler directly
462
- result = await procedure.handler({ input, ctx: ctxWithLevel });
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
+ }
463
685
  }
464
686
  else {
465
- // Fallback: Build middleware chain dynamically (should not normally happen)
466
- result = await executeMiddlewareChain(procedure.middlewares, input, ctxWithLevel, async () => procedure.handler({ input, ctx: ctxWithLevel }));
687
+ // Not transactional run all steps in declaration order (regardless of external flag)
688
+ result = await executeWithSteps(hasPipeline ? pipelineSteps : undefined, ctxWithLevel);
689
+ }
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
+ }
467
707
  }
468
- // Step 4: Auto-project if resource schema is set
708
+ // Step 5: Auto-project if resource schema is set
469
709
  if (procedure._resourceSchema) {
470
710
  const schema = procedure._resourceSchema;
471
711
  // Prefer explicit level from tagged schema over guard-derived level
@@ -480,9 +720,99 @@ export async function executeProcedure(procedure, rawInput, ctx) {
480
720
  result = projectOne(result);
481
721
  }
482
722
  }
483
- // Step 5: Validate output if schema provided
723
+ // Step 6: Validate output if schema provided
484
724
  if (procedure.outputSchema) {
485
- return procedure.outputSchema.parse(result);
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
+ }
737
+ }
738
+ // Return the ORIGINAL result (hooks cannot modify it)
739
+ return result;
740
+ }
741
+ // ============================================================================
742
+ // Level 3 Branch Selection
743
+ // ============================================================================
744
+ /**
745
+ * Executes a Level 3 branched procedure
746
+ *
747
+ * Evaluates guards from the resource schema's access level config
748
+ * most-privileged-first to select the matching branch handler, then
749
+ * auto-projects the result through that level's field visibility.
750
+ *
751
+ * @internal
752
+ */
753
+ async function executeBranchedProcedure(procedure, input, ctx) {
754
+ const handlerMap = procedure._handlerMap;
755
+ const schema = procedure._resourceSchema;
756
+ const levelConfig = schema?._levelConfig;
757
+ if (!levelConfig) {
758
+ throw new ConfigurationError('Resource schema must be built with defineAccessLevels() containing guards for Level 3 branching.');
759
+ }
760
+ // Evaluate guards most-privileged-first (reverse level order)
761
+ const levels = [...levelConfig.levels];
762
+ const reversedLevels = [...levels].reverse();
763
+ let selectedLevel;
764
+ let selectedHandler;
765
+ for (const level of reversedLevels) {
766
+ const guard = levelConfig.guards[level];
767
+ if (!guard) {
768
+ // No guard = public/fallback level — only select if handler exists
769
+ const key = `__velox_level_${level}`;
770
+ if (handlerMap[key]) {
771
+ // Don't select yet — keep looking for a higher-privilege match.
772
+ // This fallback is used if no guarded level matches.
773
+ if (!selectedHandler) {
774
+ selectedLevel = level;
775
+ selectedHandler = handlerMap[key];
776
+ }
777
+ }
778
+ continue;
779
+ }
780
+ const passed = await guard(ctx);
781
+ if (passed) {
782
+ // Guard passed — find the best handler at or below this level
783
+ const levelIndex = levels.indexOf(level);
784
+ for (let i = levelIndex; i >= 0; i--) {
785
+ const candidateLevel = levels[i];
786
+ const key = `__velox_level_${candidateLevel}`;
787
+ if (handlerMap[key]) {
788
+ selectedHandler = handlerMap[key];
789
+ selectedLevel = candidateLevel;
790
+ break;
791
+ }
792
+ }
793
+ break;
794
+ }
795
+ }
796
+ if (!selectedHandler || !selectedLevel) {
797
+ throw new GuardError('access', 'No matching branch for this access level', 403);
798
+ }
799
+ // Execute middleware chain if any, then the selected handler
800
+ let result;
801
+ if (procedure.middlewares.length > 0) {
802
+ result = await executeMiddlewareChain(procedure.middlewares, input, ctx, async () => selectedHandler({ input, ctx }));
803
+ }
804
+ else {
805
+ result = await selectedHandler({ input, ctx });
806
+ }
807
+ // Auto-project through the selected level's visibility
808
+ const projectOne = (item) => {
809
+ return new Resource(item, schema).forLevel(selectedLevel);
810
+ };
811
+ if (Array.isArray(result)) {
812
+ result = result.map((item) => projectOne(item));
813
+ }
814
+ else {
815
+ result = projectOne(result);
486
816
  }
487
817
  return result;
488
818
  }
@@ -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>;