@stackra/pipeline 0.1.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.
@@ -0,0 +1,447 @@
1
+ import { DynamicModule, IApplication } from '@stackra/contracts';
2
+
3
+ /**
4
+ * @file pipeline.module.ts
5
+ * @module @stackra/pipeline
6
+ * @description DI module for the pipeline system.
7
+ * Registers the PipelineHub as a global singleton and provides a
8
+ * PIPELINE_FACTORY token for creating fresh Pipeline instances on demand.
9
+ *
10
+ * The Pipeline class itself is NOT a singleton — each usage creates a new
11
+ * instance via the factory. This ensures no state leaks between usages.
12
+ */
13
+
14
+ /**
15
+ * Pipeline DI module.
16
+ *
17
+ * Provides:
18
+ * - `PipelineHub` — global singleton for named pipeline presets
19
+ * - `PIPELINE_FACTORY` — factory function for creating fresh Pipeline instances
20
+ *
21
+ * The Pipeline is intentionally NOT registered as a singleton. Each call to
22
+ * the factory produces a new instance with clean state. This mirrors Laravel's
23
+ * pattern where `app(Pipeline::class)` always returns a fresh pipeline.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * import { PipelineModule } from '@stackra/pipeline';
28
+ *
29
+ * @Module({
30
+ * imports: [PipelineModule.forRoot()],
31
+ * })
32
+ * export class AppModule {}
33
+ * ```
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * // Injecting the factory in a service
38
+ * @Injectable()
39
+ * class OrderService {
40
+ * constructor(
41
+ * @Inject(PIPELINE_FACTORY) private readonly createPipeline: PipelineFactory,
42
+ * ) {}
43
+ *
44
+ * processOrder(order: Order): Order {
45
+ * return this.createPipeline<Order>()
46
+ * .send(order)
47
+ * .through([ValidateOrder, CalculateTax, ApplyDiscount])
48
+ * .thenReturn();
49
+ * }
50
+ * }
51
+ * ```
52
+ */
53
+ declare class PipelineModule {
54
+ /**
55
+ * Register the pipeline module globally.
56
+ *
57
+ * @returns Dynamic module definition with PipelineHub and factory
58
+ */
59
+ static forRoot(): DynamicModule;
60
+ }
61
+
62
+ /**
63
+ * @file pipeline-options.interface.ts
64
+ * @module @stackra/pipeline/interfaces
65
+ * @description Internal pipeline configuration interfaces.
66
+ * Defines the types used for pipe resolution and pipeline definition callbacks.
67
+ */
68
+ /**
69
+ * Function pipe — a closure that receives the passable and a next callback.
70
+ *
71
+ * @typeParam TPassable - The type of value flowing through the pipeline
72
+ * @typeParam TReturn - The return type of the pipeline
73
+ */
74
+ type PipeClosure<TPassable = unknown, TReturn = unknown> = (passable: TPassable, next: (passable: TPassable) => TReturn) => TReturn;
75
+ /**
76
+ * Tuple pipe — a class reference (or string) paired with additional parameters.
77
+ * Equivalent to Laravel's `'ClassName:param1,param2'` syntax.
78
+ *
79
+ * The first element is the pipe (class or string), and subsequent elements
80
+ * are parameters passed to the pipe's handler method after `passable` and `next`.
81
+ */
82
+ type PipeTuple = [pipe: PipeEntry, ...params: unknown[]];
83
+ /**
84
+ * A single pipe entry (without parameters).
85
+ * Can be a closure, a string for container resolution, or an object instance.
86
+ */
87
+ type PipeEntry = PipeClosure | string | object;
88
+ /**
89
+ * Union of all supported pipe types.
90
+ *
91
+ * - `PipeClosure` — function `(passable, next) => result`
92
+ * - `string` — class name resolved from DI container
93
+ * - `object` — pipe instance with a handler method (default: `handle`)
94
+ * - `PipeTuple` — `[pipe, ...params]` for parameterized pipes
95
+ */
96
+ type PipeType = PipeClosure | string | object | PipeTuple;
97
+ /**
98
+ * Pipeline definition callback used by the Hub.
99
+ *
100
+ * Receives a fresh Pipeline instance and the passable, configures the pipeline,
101
+ * and returns the result.
102
+ */
103
+ type PipelineDefinition = (pipeline: unknown, passable: unknown) => unknown;
104
+
105
+ /**
106
+ * @file pipeline.service.ts
107
+ * @module @stackra/pipeline/services
108
+ * @description Laravel-style middleware Pipeline with fluent API and DI container integration.
109
+ * Implements the classic array_reduce(reverse(pipes), carry(), destination) pattern
110
+ * for composing middleware chains with type safety.
111
+ */
112
+
113
+ /**
114
+ * Laravel-style middleware pipeline with fluent API.
115
+ *
116
+ * Composes a chain of pipes (middleware) that process a passable value
117
+ * sequentially. Each pipe can inspect, modify, or short-circuit the passable
118
+ * before passing it to the next pipe in the chain.
119
+ *
120
+ * The pipeline supports four pipe types:
121
+ * - **Closure** — `(passable, next) => result`
122
+ * - **String** — resolved from the DI container by name
123
+ * - **Object** — instance with a handler method (default: `handle`)
124
+ * - **Tuple** — `[pipe, ...params]` for parameterized pipes
125
+ *
126
+ * Core algorithm (mirrors Laravel):
127
+ * ```
128
+ * pipeline = array_reduce(reverse(pipes), carry(), prepareDestination(destination))
129
+ * return pipeline(passable)
130
+ * ```
131
+ *
132
+ * @typeParam TPassable - The type of value flowing through the pipeline
133
+ * @typeParam TReturn - The type returned by the pipeline's `then()` method
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const result = new Pipeline<Request, Response>(container)
138
+ * .send(request)
139
+ * .through([AuthMiddleware, LoggingMiddleware])
140
+ * .then((req) => handleRequest(req));
141
+ * ```
142
+ */
143
+ declare class Pipeline<TPassable = unknown, TReturn = TPassable> {
144
+ /** The value flowing through the pipeline. */
145
+ private passable;
146
+ /** The ordered list of pipes to execute. */
147
+ private pipes;
148
+ /** The method name to call on pipe objects. */
149
+ private method;
150
+ /** Optional callback invoked after pipeline execution. */
151
+ private finallyCallback?;
152
+ /** Optional DI container for resolving string pipes. */
153
+ private readonly container?;
154
+ /**
155
+ * @param container - Optional application context for resolving string-based pipes.
156
+ * When provided, string pipes are resolved via `container.get(pipeName)`.
157
+ * When omitted, string pipes will throw a PipelineError.
158
+ */
159
+ constructor(container?: IApplication);
160
+ /**
161
+ * Set the passable object being sent through the pipeline.
162
+ *
163
+ * @param passable - The value to send through the pipes
164
+ * @returns This pipeline instance for chaining
165
+ */
166
+ send(passable: TPassable): this;
167
+ /**
168
+ * Set the array of pipes (replaces any existing pipes).
169
+ *
170
+ * @param pipes - Array of pipe definitions to process the passable through
171
+ * @returns This pipeline instance for chaining
172
+ */
173
+ through(pipes: PipeType[]): this;
174
+ /**
175
+ * Append additional pipes to the pipeline.
176
+ *
177
+ * Unlike `through()` which replaces all pipes, `pipe()` adds to the
178
+ * existing chain. Useful for conditionally extending a pipeline.
179
+ *
180
+ * @param pipes - One or more pipe definitions to append
181
+ * @returns This pipeline instance for chaining
182
+ */
183
+ pipe(...pipes: PipeType[]): this;
184
+ /**
185
+ * Change the handler method name called on pipe objects.
186
+ *
187
+ * By default, the pipeline calls `handle(passable, next)` on pipe objects.
188
+ * Use `via()` to call a different method (e.g., `process`, `transform`).
189
+ *
190
+ * @param method - The method name to invoke on pipe objects
191
+ * @returns This pipeline instance for chaining
192
+ */
193
+ via(method: string): this;
194
+ /**
195
+ * Register a finally callback invoked after pipeline execution.
196
+ *
197
+ * The callback receives the passable after all pipes and the destination
198
+ * have processed it. Useful for cleanup, logging, or telemetry.
199
+ *
200
+ * @param callback - Function to call with the passable after execution
201
+ * @returns This pipeline instance for chaining
202
+ */
203
+ finally(callback: (passable: TPassable) => void): this;
204
+ /**
205
+ * Execute the pipeline with a final destination closure.
206
+ *
207
+ * The destination is the innermost function — it receives the passable
208
+ * after all pipes have processed it. The pipeline is built using
209
+ * `reduceRight` (equivalent to Laravel's `array_reduce(array_reverse(...))`).
210
+ *
211
+ * @typeParam R - The return type of the destination
212
+ * @param destination - Final handler that produces the pipeline's return value
213
+ * @returns The result of the destination function
214
+ * @throws {PipelineError} When a pipe cannot be resolved or executed
215
+ */
216
+ then<R = TReturn>(destination: (passable: TPassable) => R): R;
217
+ /**
218
+ * Execute the pipeline and return the passable as-is.
219
+ *
220
+ * Equivalent to `then((passable) => passable)`. Useful when the pipeline
221
+ * transforms the passable in place and no final destination is needed.
222
+ *
223
+ * @returns The passable after all pipes have processed it
224
+ */
225
+ thenReturn(): TPassable;
226
+ /**
227
+ * Wrap the destination in a closure matching the pipeline's signature.
228
+ *
229
+ * @param destination - The final handler
230
+ * @returns A closure compatible with the reduce chain
231
+ */
232
+ private prepareDestination;
233
+ /**
234
+ * Invoke a single pipe with the passable and next callback.
235
+ *
236
+ * Resolves the pipe type and delegates to the appropriate handler:
237
+ * - Function → called directly
238
+ * - String → resolved from container, method called
239
+ * - Array (tuple) → first element resolved, rest are params
240
+ * - Object → method called directly
241
+ *
242
+ * @param pipe - The pipe to invoke
243
+ * @param passable - The current passable value
244
+ * @param next - The next function in the chain
245
+ * @returns The result of invoking the pipe
246
+ * @throws {PipelineError} When the pipe cannot be resolved or invoked
247
+ */
248
+ private carry;
249
+ /**
250
+ * Handle a tuple pipe by extracting the pipe and its parameters.
251
+ *
252
+ * @param tuple - The [pipe, ...params] tuple
253
+ * @param passable - The current passable
254
+ * @param next - The next function
255
+ * @returns The result of invoking the pipe
256
+ */
257
+ private handleTuplePipe;
258
+ /**
259
+ * Resolve a string pipe from the container and invoke its handler method.
260
+ *
261
+ * @param name - The service name/token to resolve
262
+ * @param passable - The current passable
263
+ * @param next - The next function
264
+ * @param params - Additional parameters to pass to the handler
265
+ * @returns The result of invoking the resolved pipe
266
+ * @throws {PipelineError} When no container is available or resolution fails
267
+ */
268
+ private handleStringPipe;
269
+ /**
270
+ * Invoke a handler method on a pipe object instance.
271
+ *
272
+ * @param pipe - The pipe object instance
273
+ * @param passable - The current passable
274
+ * @param next - The next function
275
+ * @param params - Additional parameters to pass after passable and next
276
+ * @returns The result of invoking the method
277
+ * @throws {PipelineError} When the method does not exist on the pipe
278
+ */
279
+ private handleObjectPipe;
280
+ }
281
+
282
+ /**
283
+ * @file pipeline-hub.service.ts
284
+ * @module @stackra/pipeline/services
285
+ * @description Named pipeline presets registry (Hub).
286
+ * Allows registering named pipeline definitions and executing them by name.
287
+ * Equivalent to Laravel's Pipeline Hub concept for reusable pipeline configurations.
288
+ */
289
+
290
+ /**
291
+ * Named pipeline presets registry.
292
+ *
293
+ * The Hub stores named pipeline definitions that can be executed by name.
294
+ * This enables reusable pipeline configurations that multiple consumers
295
+ * can invoke without knowing the pipe chain details.
296
+ *
297
+ * A `defaults` pipeline is used as the fallback when no specific pipeline
298
+ * name is provided to `pipe()`.
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * const hub = new PipelineHub(container);
303
+ *
304
+ * hub.pipeline('order-validation', (pipeline, passable) =>
305
+ * pipeline
306
+ * .send(passable)
307
+ * .through([ValidateStock, ValidatePayment, ValidateAddress])
308
+ * .thenReturn()
309
+ * );
310
+ *
311
+ * const result = hub.pipe(orderData, 'order-validation');
312
+ * ```
313
+ */
314
+ declare class PipelineHub {
315
+ /** Registry of named pipeline definitions. */
316
+ private readonly pipelines;
317
+ /** Default pipeline definition (fallback when no name specified). */
318
+ private defaultPipeline?;
319
+ /** Optional DI container for creating Pipeline instances. */
320
+ private readonly container?;
321
+ /**
322
+ * @param container - Optional application context passed to created Pipeline instances
323
+ */
324
+ constructor(container?: IApplication);
325
+ /**
326
+ * Register the default pipeline definition.
327
+ *
328
+ * The default pipeline is used when `pipe()` is called without a name.
329
+ *
330
+ * @param callback - Pipeline definition that configures and executes a pipeline
331
+ */
332
+ defaults(callback: PipelineDefinition): void;
333
+ /**
334
+ * Register a named pipeline definition.
335
+ *
336
+ * @param name - Unique name for this pipeline preset
337
+ * @param callback - Pipeline definition that configures and executes a pipeline
338
+ * @throws {PipelineError} When the name is empty
339
+ */
340
+ pipeline(name: string, callback: PipelineDefinition): void;
341
+ /**
342
+ * Execute a named (or default) pipeline with the given passable.
343
+ *
344
+ * Creates a fresh Pipeline instance, passes it along with the passable
345
+ * to the registered definition, and returns the result.
346
+ *
347
+ * @typeParam T - The type of the passable value
348
+ * @param passable - The value to send through the pipeline
349
+ * @param pipelineName - Optional name of the pipeline preset to execute.
350
+ * Falls back to the default pipeline if omitted.
351
+ * @returns The result of the pipeline execution
352
+ * @throws {PipelineError} When the named pipeline or default is not registered
353
+ */
354
+ pipe<T>(passable: T, pipelineName?: string): unknown;
355
+ /**
356
+ * Check if a named pipeline definition exists in the registry.
357
+ *
358
+ * @param name - The pipeline name to check
359
+ * @returns True if the pipeline is registered
360
+ */
361
+ has(name: string): boolean;
362
+ /**
363
+ * Resolve a pipeline definition by name or fall back to the default.
364
+ *
365
+ * @param name - Optional pipeline name
366
+ * @returns The resolved pipeline definition
367
+ * @throws {PipelineError} When the definition cannot be found
368
+ */
369
+ private resolveDefinition;
370
+ }
371
+
372
+ /**
373
+ * @file pipeline.constant.ts
374
+ * @module @stackra/pipeline/constants
375
+ * @description DI tokens and constants for the pipeline module.
376
+ */
377
+ /**
378
+ * DI token for the pipeline factory function.
379
+ *
380
+ * Inject this token to obtain a factory that creates fresh Pipeline instances.
381
+ * Each call to the factory produces a new Pipeline with no residual state.
382
+ *
383
+ * @example
384
+ * ```typescript
385
+ * @Inject(PIPELINE_FACTORY) private readonly createPipeline: PipelineFactory
386
+ * ```
387
+ */
388
+ declare const PIPELINE_FACTORY: unique symbol;
389
+
390
+ /**
391
+ * @file pipeline-factory.type.ts
392
+ * @module @stackra/pipeline/types
393
+ * @description PipelineFactory type.
394
+ */
395
+ /**
396
+ * Factory function type for creating fresh Pipeline instances.
397
+ *
398
+ * Returns a new Pipeline typed with the specified passable type.
399
+ * The factory encapsulates container injection so consumers don't need
400
+ * to manage container references.
401
+ *
402
+ * @typeParam T - The passable type for the created pipeline
403
+ */
404
+ type PipelineFactory = <T = unknown>() => Pipeline<T>;
405
+
406
+ /**
407
+ * @file pipeline.error.ts
408
+ * @module @stackra/pipeline/errors
409
+ * @description Pipeline-specific error class for all pipeline operation failures.
410
+ * Thrown when a pipe cannot be resolved, the pipeline is misconfigured,
411
+ * or a pipe throws during execution.
412
+ */
413
+ /**
414
+ * Error thrown when a pipeline operation fails.
415
+ *
416
+ * Covers scenarios such as:
417
+ * - Pipe resolution failure (string pipe not found in container)
418
+ * - Invalid pipe type provided
419
+ * - Pipeline method not found on a pipe object
420
+ * - Pipe execution failure (wrapped original error)
421
+ *
422
+ * @example
423
+ * ```typescript
424
+ * throw new PipelineError(
425
+ * 'Pipe "ValidateInput" could not be resolved from the container.',
426
+ * 'PIPE_RESOLUTION_FAILED',
427
+ * );
428
+ * ```
429
+ */
430
+ declare class PipelineError extends Error {
431
+ /**
432
+ * Machine-readable error code for programmatic handling.
433
+ */
434
+ readonly code: string;
435
+ /**
436
+ * The original error that caused this pipeline error, if any.
437
+ */
438
+ readonly cause?: Error;
439
+ /**
440
+ * @param message - Human-readable description of what went wrong
441
+ * @param code - Machine-readable error code
442
+ * @param cause - The original error that triggered this pipeline error
443
+ */
444
+ constructor(message: string, code?: string, cause?: Error);
445
+ }
446
+
447
+ export { PIPELINE_FACTORY, type PipeClosure, type PipeEntry, type PipeTuple, type PipeType, Pipeline, type PipelineDefinition, PipelineError, type PipelineFactory, PipelineHub, PipelineModule };