@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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/dist/index.d.mts +447 -0
- package/dist/index.d.ts +447 -0
- package/dist/index.js +491 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +488 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stackra L.L.C
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# @stackra/pipeline
|
|
2
|
+
|
|
3
|
+
Laravel-style middleware pipeline with a fluent API, DI-container integration, and named preset registration via `PipelineHub`. Framework-agnostic, zero server dependency, works in every JavaScript runtime.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @stackra/pipeline @stackra/container @stackra/contracts reflect-metadata
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer requirements: `@stackra/container`, `@stackra/contracts`, `reflect-metadata`.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import 'reflect-metadata';
|
|
17
|
+
import { ApplicationFactory, Module } from '@stackra/container';
|
|
18
|
+
import { PipelineModule, Pipeline } from '@stackra/pipeline';
|
|
19
|
+
|
|
20
|
+
@Module({
|
|
21
|
+
imports: [PipelineModule.forRoot()],
|
|
22
|
+
})
|
|
23
|
+
class AppModule {}
|
|
24
|
+
|
|
25
|
+
const app = await ApplicationFactory.create(AppModule);
|
|
26
|
+
|
|
27
|
+
const result = new Pipeline<number>(app)
|
|
28
|
+
.send(1)
|
|
29
|
+
.through([(n, next) => next(n + 1), (n, next) => next(n * 10)])
|
|
30
|
+
.thenReturn();
|
|
31
|
+
|
|
32
|
+
console.log(result); // 20
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Public API
|
|
36
|
+
|
|
37
|
+
### `Pipeline<TPassable, TReturn>`
|
|
38
|
+
|
|
39
|
+
The fluent pipeline class. Composes a chain of pipes that transform a passable value.
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
new Pipeline(container)
|
|
43
|
+
.send(input) // set the passable
|
|
44
|
+
.through(pipes) // replace all pipes
|
|
45
|
+
.pipe(extra) // append additional pipes
|
|
46
|
+
.via('process') // override handler method name (default: 'handle')
|
|
47
|
+
.finally((v) => log(v)) // callback after execution
|
|
48
|
+
.then((v) => destination) // execute with destination
|
|
49
|
+
// — or —
|
|
50
|
+
.thenReturn(); // execute and return the passable as-is
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `PipelineHub`
|
|
54
|
+
|
|
55
|
+
Named pipeline preset registry — reusable pipeline configurations invoked by name.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { PipelineHub } from '@stackra/pipeline';
|
|
59
|
+
|
|
60
|
+
@Injectable()
|
|
61
|
+
class OrderService {
|
|
62
|
+
constructor(private hub: PipelineHub) {}
|
|
63
|
+
|
|
64
|
+
onModuleInit() {
|
|
65
|
+
this.hub.defaults((pipeline, passable) =>
|
|
66
|
+
pipeline.send(passable).through([Sanitize]).thenReturn()
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
this.hub.pipeline('order-validation', (pipeline, order) =>
|
|
70
|
+
pipeline.send(order).through([ValidateStock, ValidatePayment, ValidateAddress]).thenReturn()
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
validate(order: Order) {
|
|
75
|
+
return this.hub.pipe(order, 'order-validation');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Hub runtime API:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
hub.defaults(cb); // set the default pipeline definition
|
|
84
|
+
hub.pipeline(name, cb); // register a named pipeline
|
|
85
|
+
hub.pipe(passable, name?); // execute a named or default pipeline
|
|
86
|
+
hub.has(name); // boolean — does this name exist?
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `PipelineModule`
|
|
90
|
+
|
|
91
|
+
DI module exposing `Pipeline`, `PipelineHub`, and `PIPELINE_FACTORY`. Import once at the root:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
@Module({
|
|
95
|
+
imports: [PipelineModule.forRoot()],
|
|
96
|
+
})
|
|
97
|
+
class AppModule {}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `PIPELINE_FACTORY`
|
|
101
|
+
|
|
102
|
+
DI token for a factory that produces fresh `Pipeline` instances backed by the container.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { Inject, Injectable } from '@stackra/container';
|
|
106
|
+
import { PIPELINE_FACTORY, PipelineFactory } from '@stackra/pipeline';
|
|
107
|
+
|
|
108
|
+
@Injectable()
|
|
109
|
+
class RequestHandler {
|
|
110
|
+
constructor(@Inject(PIPELINE_FACTORY) private makePipeline: PipelineFactory) {}
|
|
111
|
+
|
|
112
|
+
handle(request: Request) {
|
|
113
|
+
return this.makePipeline<Request, Response>()
|
|
114
|
+
.send(request)
|
|
115
|
+
.through([Auth, Logging, Handler])
|
|
116
|
+
.thenReturn();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `PipelineError`
|
|
122
|
+
|
|
123
|
+
Thrown when a pipe cannot be resolved or executed. Carries a `code` for programmatic dispatch:
|
|
124
|
+
|
|
125
|
+
| Code | Meaning |
|
|
126
|
+
| ------------------------ | ------------------------------------------------------------ |
|
|
127
|
+
| `INVALID_PIPE_TYPE` | Pipe was not a function, string, object, or tuple |
|
|
128
|
+
| `INVALID_PIPE_ENTRY` | Tuple's first element was of an unsupported type |
|
|
129
|
+
| `NO_CONTAINER` | String pipe used but no container provided to Pipeline |
|
|
130
|
+
| `PIPE_RESOLUTION_FAILED` | Container threw while resolving a string pipe |
|
|
131
|
+
| `INVALID_RESOLVED_PIPE` | Container returned a non-object for a string pipe |
|
|
132
|
+
| `METHOD_NOT_FOUND` | Pipe object is missing the configured handler method |
|
|
133
|
+
| `PIPE_EXECUTION_FAILED` | A pipe threw during execution (`cause` carries the original) |
|
|
134
|
+
| `INVALID_PIPELINE_NAME` | Empty name passed to `hub.pipeline(name, cb)` |
|
|
135
|
+
| `PIPELINE_NOT_FOUND` | `hub.pipe(v, name)` called with an unregistered name |
|
|
136
|
+
| `NO_DEFAULT_PIPELINE` | `hub.pipe(v)` called with no name and no default registered |
|
|
137
|
+
|
|
138
|
+
## Pipe forms
|
|
139
|
+
|
|
140
|
+
A pipe can be any of four shapes:
|
|
141
|
+
|
|
142
|
+
### Closure
|
|
143
|
+
|
|
144
|
+
Simplest form — a function taking `(passable, next)`:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const AddOne = (n: number, next: (n: number) => number) => next(n + 1);
|
|
148
|
+
|
|
149
|
+
new Pipeline<number>().send(1).through([AddOne]).thenReturn(); // 2
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### String (DI-resolved)
|
|
153
|
+
|
|
154
|
+
Resolved from the container by name/token. The resolved instance must have a handler method (default `handle`):
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
@Injectable()
|
|
158
|
+
class LoggingPipe {
|
|
159
|
+
handle(request: Request, next: (r: Request) => Response) {
|
|
160
|
+
console.log(request.url);
|
|
161
|
+
return next(request);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// register `LoggingPipe` under the string 'logging' via a module provider
|
|
166
|
+
// ...
|
|
167
|
+
new Pipeline<Request, Response>(container)
|
|
168
|
+
.send(request)
|
|
169
|
+
.through(['logging'])
|
|
170
|
+
.then((r) => processRequest(r));
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Object
|
|
174
|
+
|
|
175
|
+
Instance with a handler method — bypasses container resolution:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
new Pipeline<Request, Response>()
|
|
179
|
+
.send(request)
|
|
180
|
+
.through([new LoggingPipe()])
|
|
181
|
+
.then((r) => processRequest(r));
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Tuple (parameterized)
|
|
185
|
+
|
|
186
|
+
`[pipe, ...params]` — extra params flow to the handler after `(passable, next)`:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
@Injectable()
|
|
190
|
+
class RateLimitPipe {
|
|
191
|
+
handle(req: Request, next: (r: Request) => Response, limit: number, window: string) {
|
|
192
|
+
// ...
|
|
193
|
+
return next(req);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
new Pipeline<Request, Response>(container)
|
|
198
|
+
.send(request)
|
|
199
|
+
.through([['rate-limit', 100, '1m']])
|
|
200
|
+
.then((r) => processRequest(r));
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Container integration
|
|
204
|
+
|
|
205
|
+
Every string pipe is resolved through the `@stackra/container` DI system. The `Pipeline` constructor takes an optional `IApplication`:
|
|
206
|
+
|
|
207
|
+
- **With container** — string pipes work, `Pipeline` is itself `@Injectable()` so it can be injected anywhere.
|
|
208
|
+
- **Without container** — only function and object pipes are supported (string pipes throw `NO_CONTAINER`).
|
|
209
|
+
|
|
210
|
+
When injecting `Pipeline` into your services, always inject a **factory** (`PIPELINE_FACTORY`) rather than a singleton — pipelines are single-use, and reusing the same instance across requests will leak state.
|
|
211
|
+
|
|
212
|
+
## `via()` — custom method names
|
|
213
|
+
|
|
214
|
+
By default the pipeline calls `handle(passable, next)` on pipe objects. Use `.via(method)` to invoke a different method:
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
class ProcessorPipe {
|
|
218
|
+
process(data: Data, next: (d: Data) => Data) {
|
|
219
|
+
return next(transform(data));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
new Pipeline<Data>().send(data).via('process').through([new ProcessorPipe()]).thenReturn();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## `finally()` — post-execution callback
|
|
227
|
+
|
|
228
|
+
The `finally` callback fires after `then()` / `thenReturn()` completes, receiving the (potentially mutated) passable:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
new Pipeline<Request, Response>()
|
|
232
|
+
.send(request)
|
|
233
|
+
.through([Auth, Logging])
|
|
234
|
+
.finally((req) => telemetry.record(req))
|
|
235
|
+
.then((r) => handleRequest(r));
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## `PipelineHub` presets
|
|
239
|
+
|
|
240
|
+
Use the Hub when the same pipeline definition is called from multiple sites. Register once, invoke by name — the passable is provided at call time.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
@Injectable()
|
|
244
|
+
class BootstrapService {
|
|
245
|
+
constructor(private hub: PipelineHub) {}
|
|
246
|
+
|
|
247
|
+
onModuleInit() {
|
|
248
|
+
// Preset A — inbound HTTP
|
|
249
|
+
this.hub.pipeline('http-inbound', (pipeline, req) =>
|
|
250
|
+
pipeline.send(req).through([Auth, RateLimit, Logger]).thenReturn()
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Preset B — outbound API call
|
|
254
|
+
this.hub.pipeline('http-outbound', (pipeline, req) =>
|
|
255
|
+
pipeline.send(req).through([AddAuthHeader, Retry, Telemetry]).thenReturn()
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// elsewhere
|
|
261
|
+
const finalReq = hub.pipe(request, 'http-inbound');
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Types
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
type PipeClosure<TPassable, TResult> = (
|
|
268
|
+
passable: TPassable,
|
|
269
|
+
next: (passable: TPassable) => TResult
|
|
270
|
+
) => TResult;
|
|
271
|
+
|
|
272
|
+
type PipeTuple = [PipeEntry, ...unknown[]];
|
|
273
|
+
|
|
274
|
+
type PipeEntry = string | object | PipeClosure<unknown, unknown>;
|
|
275
|
+
|
|
276
|
+
type PipeType = PipeClosure<unknown, unknown> | string | object | PipeTuple;
|
|
277
|
+
|
|
278
|
+
type PipelineDefinition = <T>(pipeline: Pipeline, passable: T) => unknown;
|
|
279
|
+
|
|
280
|
+
type PipelineFactory = <TPassable = unknown, TReturn = TPassable>() => Pipeline<TPassable, TReturn>;
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Error handling
|
|
284
|
+
|
|
285
|
+
Every pipe execution is wrapped in a try/catch. If a pipe throws:
|
|
286
|
+
|
|
287
|
+
- **Non-`PipelineError`** — wrapped in a new `PipelineError` with code `PIPE_EXECUTION_FAILED` and the original attached as `cause`.
|
|
288
|
+
- **Already a `PipelineError`** — re-thrown unchanged.
|
|
289
|
+
|
|
290
|
+
This preserves the original stack trace via the `cause` chain while giving downstream code a stable error surface.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { PipelineError } from '@stackra/pipeline';
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const result = pipeline.thenReturn();
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (err instanceof PipelineError) {
|
|
299
|
+
console.error(err.code, err.message, err.cause);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Integration with `@stackra/ssr/middleware`
|
|
305
|
+
|
|
306
|
+
`@stackra/pipeline` is the runtime that powers HTTP and UI middleware in `@stackra/ssr`. Any `MiddlewareDefinition` can be adapted to a `PipeType` via `toPipe(mw, container)` and dropped straight into `.through([...])`.
|
|
307
|
+
|
|
308
|
+
See the [SSR middleware docs](../ssr/README.md#middleware) for the full integration story.
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT © Stackra L.L.C
|