@cqrs-ddd/pipeline 0.5.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/COMMERCIAL_LICENSE.txt +34 -0
- package/LICENSE +661 -0
- package/README.md +42 -0
- package/dist/behaviors/logging.behavior.d.ts +195 -0
- package/dist/behaviors/logging.behavior.d.ts.map +1 -0
- package/dist/behaviors/logging.behavior.js +206 -0
- package/dist/behaviors/logging.behavior.js.map +1 -0
- package/dist/constants/pipeline-context.constants.d.ts +34 -0
- package/dist/constants/pipeline-context.constants.d.ts.map +1 -0
- package/dist/constants/pipeline-context.constants.js +34 -0
- package/dist/constants/pipeline-context.constants.js.map +1 -0
- package/dist/create-pipeline.d.ts +122 -0
- package/dist/create-pipeline.d.ts.map +1 -0
- package/dist/create-pipeline.js +193 -0
- package/dist/create-pipeline.js.map +1 -0
- package/dist/entries.d.ts +13 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +3 -0
- package/dist/entries.js.map +1 -0
- package/dist/errors/missing-partition.error.d.ts +27 -0
- package/dist/errors/missing-partition.error.d.ts.map +1 -0
- package/dist/errors/missing-partition.error.js +33 -0
- package/dist/errors/missing-partition.error.js.map +1 -0
- package/dist/errors/missing-pipeline-item.error.d.ts +8 -0
- package/dist/errors/missing-pipeline-item.error.d.ts.map +1 -0
- package/dist/errors/missing-pipeline-item.error.js +16 -0
- package/dist/errors/missing-pipeline-item.error.js.map +1 -0
- package/dist/handler-pipeline.d.ts +62 -0
- package/dist/handler-pipeline.d.ts.map +1 -0
- package/dist/handler-pipeline.js +74 -0
- package/dist/handler-pipeline.js.map +1 -0
- package/dist/helpers/behavior-entries.d.ts +34 -0
- package/dist/helpers/behavior-entries.d.ts.map +1 -0
- package/dist/helpers/behavior-entries.js +50 -0
- package/dist/helpers/behavior-entries.js.map +1 -0
- package/dist/helpers/behavior-id.d.ts +43 -0
- package/dist/helpers/behavior-id.d.ts.map +1 -0
- package/dist/helpers/behavior-id.js +38 -0
- package/dist/helpers/behavior-id.js.map +1 -0
- package/dist/helpers/logging.intent.d.ts +17 -0
- package/dist/helpers/logging.intent.d.ts.map +1 -0
- package/dist/helpers/logging.intent.js +18 -0
- package/dist/helpers/logging.intent.js.map +1 -0
- package/dist/helpers/postgres-json.d.ts +25 -0
- package/dist/helpers/postgres-json.d.ts.map +1 -0
- package/dist/helpers/postgres-json.js +35 -0
- package/dist/helpers/postgres-json.js.map +1 -0
- package/dist/helpers/replace-request.d.ts +19 -0
- package/dist/helpers/replace-request.d.ts.map +1 -0
- package/dist/helpers/replace-request.js +29 -0
- package/dist/helpers/replace-request.js.map +1 -0
- package/dist/helpers/tenant-partition.d.ts +38 -0
- package/dist/helpers/tenant-partition.d.ts.map +1 -0
- package/dist/helpers/tenant-partition.js +27 -0
- package/dist/helpers/tenant-partition.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/context-source.interface.d.ts +42 -0
- package/dist/interfaces/context-source.interface.d.ts.map +1 -0
- package/dist/interfaces/context-source.interface.js +3 -0
- package/dist/interfaces/context-source.interface.js.map +1 -0
- package/dist/interfaces/pipeline-behavior-contract.interface.d.ts +99 -0
- package/dist/interfaces/pipeline-behavior-contract.interface.d.ts.map +1 -0
- package/dist/interfaces/pipeline-behavior-contract.interface.js +22 -0
- package/dist/interfaces/pipeline-behavior-contract.interface.js.map +1 -0
- package/dist/interfaces/pipeline-handler-meta.interface.d.ts +15 -0
- package/dist/interfaces/pipeline-handler-meta.interface.d.ts.map +1 -0
- package/dist/interfaces/pipeline-handler-meta.interface.js +3 -0
- package/dist/interfaces/pipeline-handler-meta.interface.js.map +1 -0
- package/dist/interfaces/pipeline.behavior.interface.d.ts +38 -0
- package/dist/interfaces/pipeline.behavior.interface.d.ts.map +1 -0
- package/dist/interfaces/pipeline.behavior.interface.js +3 -0
- package/dist/interfaces/pipeline.behavior.interface.js.map +1 -0
- package/dist/interfaces/pipeline.context.interface.d.ts +69 -0
- package/dist/interfaces/pipeline.context.interface.d.ts.map +1 -0
- package/dist/interfaces/pipeline.context.interface.js +3 -0
- package/dist/interfaces/pipeline.context.interface.js.map +1 -0
- package/dist/logger.d.ts +45 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +64 -0
- package/dist/logger.js.map +1 -0
- package/dist/options/global-behaviors.options.d.ts +57 -0
- package/dist/options/global-behaviors.options.d.ts.map +1 -0
- package/dist/options/global-behaviors.options.js +3 -0
- package/dist/options/global-behaviors.options.js.map +1 -0
- package/dist/pipeline-items.d.ts +22 -0
- package/dist/pipeline-items.d.ts.map +1 -0
- package/dist/pipeline-items.js +34 -0
- package/dist/pipeline-items.js.map +1 -0
- package/dist/pipeline.context.d.ts +86 -0
- package/dist/pipeline.context.d.ts.map +1 -0
- package/dist/pipeline.context.js +109 -0
- package/dist/pipeline.context.js.map +1 -0
- package/dist/services/pipeline-contracts.d.ts +29 -0
- package/dist/services/pipeline-contracts.d.ts.map +1 -0
- package/dist/services/pipeline-contracts.js +102 -0
- package/dist/services/pipeline-contracts.js.map +1 -0
- package/dist/services/pipeline-plan.d.ts +49 -0
- package/dist/services/pipeline-plan.d.ts.map +1 -0
- package/dist/services/pipeline-plan.js +111 -0
- package/dist/services/pipeline-plan.js.map +1 -0
- package/dist/services/pipeline-runner.d.ts +26 -0
- package/dist/services/pipeline-runner.d.ts.map +1 -0
- package/dist/services/pipeline-runner.js +68 -0
- package/dist/services/pipeline-runner.js.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @cqrs-ddd/pipeline
|
|
2
|
+
|
|
3
|
+
A framework-neutral pipeline engine: wrap plain functions and class methods with
|
|
4
|
+
behaviors such as logging, validation, authorization, caching or idempotency, and declare
|
|
5
|
+
the behaviors of handler classes with `@UsePipeline` and `@SkipPipeline`. No
|
|
6
|
+
framework, no dependency-injection container and no CQRS infrastructure are required;
|
|
7
|
+
the `@cqrs-ddd/pipeline-<name>` packages provide the behaviors.
|
|
8
|
+
|
|
9
|
+
**Documentation:** [guide](https://aristoteliss.github.io/ddd-cqrs/packages/pipeline/) · [API reference](https://aristoteliss.github.io/ddd-cqrs/api/cqrs-ddd/pipeline/) · [all packages](https://aristoteliss.github.io/ddd-cqrs/)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @cqrs-ddd/pipeline
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node.js 22.12 or later, or another runtime with `AsyncLocalStorage` (Bun, Deno).
|
|
18
|
+
Published as an ES module; a CommonJS application loads it with `require()`.
|
|
19
|
+
|
|
20
|
+
## Example
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createPipeline, logging } from '@cqrs-ddd/pipeline';
|
|
24
|
+
|
|
25
|
+
const pipeline = createPipeline({
|
|
26
|
+
globalBehaviors: { before: [logging({ requestResponseLogLevel: 'log' })] },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const getPrice = pipeline.wrap({ name: 'getPrice', kind: 'query' })(
|
|
30
|
+
async (sku: string) => prices.find(sku),
|
|
31
|
+
);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A method can be decorated instead, in either TypeScript decorator mode:
|
|
35
|
+
`@pipeline.wrap({ kind: 'query' })`. A plain function needs a `name`, which keys and logs
|
|
36
|
+
use to tell operations apart, and a `kind`: a `query` reads, a `command` changes state,
|
|
37
|
+
an `event` reacts.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
Dual-licensed under **AGPLv3** and a **Commercial License**. See `LICENSE` and
|
|
42
|
+
`COMMERCIAL_LICENSE.txt`.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { IPipelineBehavior, NextDelegate } from '../interfaces/pipeline.behavior.interface.js';
|
|
2
|
+
import { IPipelineContext } from '../interfaces/pipeline.context.interface.js';
|
|
3
|
+
import type { LogLevel, PipelineLogger } from '../logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* An error class used as a `mapLogLevel` key. It is matched with `instanceof`, so
|
|
6
|
+
* it also covers its subclasses.
|
|
7
|
+
*/
|
|
8
|
+
export type ErrorClass = abstract new (...args: never[]) => Error;
|
|
9
|
+
/**
|
|
10
|
+
* Configuration options for the logging behavior.
|
|
11
|
+
*
|
|
12
|
+
* Levels use {@link LogLevel} names (`'verbose'`, `'debug'`, `'log'`,
|
|
13
|
+
* `'warn'`, `'error'`, `'fatal'`).
|
|
14
|
+
* When using `nestjs-pino`, these map to pino levels:
|
|
15
|
+
* `'verbose'` → `trace`, `'debug'` → `debug`, `'log'` → `info`, `'warn'` → `warn`,
|
|
16
|
+
* `'error'` → `error`, `'fatal'` → `fatal`.
|
|
17
|
+
* Use `'none'` to suppress a message entirely.
|
|
18
|
+
*
|
|
19
|
+
* @example Structured application logging with safe payload visibility
|
|
20
|
+
* ```ts
|
|
21
|
+
* export const createUser = pipeline.wrap(
|
|
22
|
+
* { name: 'createUser', kind: 'command' },
|
|
23
|
+
* [LoggingBehavior, {
|
|
24
|
+
* logFormat: 'structured',
|
|
25
|
+
* requestResponseLogLevel: 'debug',
|
|
26
|
+
* excludeRequestObj: false,
|
|
27
|
+
* excludeResponseObj: true,
|
|
28
|
+
* redactKeys: ['code'],
|
|
29
|
+
* mapLogLevel: new Map([[UniqueEmailException, 'warn']]),
|
|
30
|
+
* }],
|
|
31
|
+
* )(async (command: CreateUserCommand) => users.create(command));
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export interface LoggingBehaviorOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Log level for the post-success metric line (correlation ID, request
|
|
37
|
+
* kind/name, handler name, and elapsed duration in ms). Emitted once
|
|
38
|
+
* `next()` resolves without throwing.
|
|
39
|
+
* Uses LogLevel names or 'none' to suppress.
|
|
40
|
+
* Default: 'log'.
|
|
41
|
+
*/
|
|
42
|
+
metricLogLevel?: LogLevel | 'none';
|
|
43
|
+
/**
|
|
44
|
+
* Log level used when the wrapped handler throws.
|
|
45
|
+
* Uses LogLevel names or 'none' to suppress.
|
|
46
|
+
* Overridden per error type when `mapLogLevel` has a matching entry.
|
|
47
|
+
* Default: 'error'.
|
|
48
|
+
*/
|
|
49
|
+
errorLogLevel?: LogLevel | 'none';
|
|
50
|
+
/**
|
|
51
|
+
* Per-exception-type log level override, keyed by error class.
|
|
52
|
+
*
|
|
53
|
+
* When the thrown error is an `instanceof` more than one registered key,
|
|
54
|
+
* the most specific (most derived) matching class wins — e.g. with both
|
|
55
|
+
* `Error` and `ForbiddenException` registered, a thrown `ForbiddenException`
|
|
56
|
+
* logs at the `ForbiddenException` level, regardless of the order the
|
|
57
|
+
* entries were inserted into the Map. Falls back to `errorLogLevel` when
|
|
58
|
+
* no key matches.
|
|
59
|
+
* Example: `new Map([[Error, 'error'], [ForbiddenException, 'warn']])`
|
|
60
|
+
*/
|
|
61
|
+
mapLogLevel?: Map<ErrorClass, LogLevel | 'none'>;
|
|
62
|
+
/**
|
|
63
|
+
* Log level for request/response payloads.
|
|
64
|
+
* Uses LogLevel names or 'none' to suppress.
|
|
65
|
+
* Default: 'debug'.
|
|
66
|
+
*/
|
|
67
|
+
requestResponseLogLevel?: LogLevel | 'none';
|
|
68
|
+
/**
|
|
69
|
+
* Keys to exclude from request/response logs.
|
|
70
|
+
* Passed to `safeStringify`/`safeSanitize` as a `Set<string>` for filtering object properties.
|
|
71
|
+
* Supports dot notation for nested properties (e.g. `'ctx.sessionUser'`).
|
|
72
|
+
* Example: `['password', 'token', 'ctx.sessionUser']` omits those fields from the log output.
|
|
73
|
+
* Default: [] (no keys excluded).
|
|
74
|
+
*/
|
|
75
|
+
excludeKeys?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Additional keys or dot-paths to mask with `[REDACTED]` when request/response
|
|
78
|
+
* payload logging is enabled. Unlike {@link excludeKeys}, redacted properties
|
|
79
|
+
* stay in the payload so its shape remains visible.
|
|
80
|
+
*
|
|
81
|
+
* This option is additive and has no effect unless configured.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* logging({
|
|
86
|
+
* excludeRequestObj: false,
|
|
87
|
+
* redactKeys: ['profile.email', 'payment.cardToken'],
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
redactKeys?: string[];
|
|
92
|
+
/**
|
|
93
|
+
* Masks the core {@link DEFAULT_REDACT_KEYS} (password, token, authorization,
|
|
94
|
+
* cookie, API keys, card data, etc.) in request/response payload logs. Custom
|
|
95
|
+
* {@link redactKeys} are merged on top.
|
|
96
|
+
*
|
|
97
|
+
* Defaults to `true`. Set it to `false` only when raw payload logging is an
|
|
98
|
+
* explicit application requirement and the caller owns the resulting exposure
|
|
99
|
+
* risk.
|
|
100
|
+
*
|
|
101
|
+
* @default true
|
|
102
|
+
*
|
|
103
|
+
* @example Opting out, deliberately
|
|
104
|
+
* ```ts
|
|
105
|
+
* logging({ excludeRequestObj: false, redactSensitiveKeys: false });
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
redactSensitiveKeys?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* If true, omits the request payload from logs entirely, logging the
|
|
111
|
+
* placeholder `'[exclude request obj]'` instead.
|
|
112
|
+
* Default: true.
|
|
113
|
+
*/
|
|
114
|
+
excludeRequestObj?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* If true, omits the response payload from logs entirely, logging the
|
|
117
|
+
* placeholder `'[exclude response obj]'` instead.
|
|
118
|
+
* Default: true.
|
|
119
|
+
*/
|
|
120
|
+
excludeResponseObj?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Output shape for request/response/metric/error logs.
|
|
123
|
+
* - 'text': single interpolated string (default).
|
|
124
|
+
* - 'structured': plain object payload (e.g. `{ msg, request }` for
|
|
125
|
+
* request/response, or `{ message, stack, ... }` for errors), suitable
|
|
126
|
+
* for structured loggers like nestjs-pino/pino that serialize objects
|
|
127
|
+
* into JSON fields.
|
|
128
|
+
* Default: 'text'.
|
|
129
|
+
*/
|
|
130
|
+
logFormat?: 'text' | 'structured';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Pipeline behavior that logs each request/response and execution timing.
|
|
134
|
+
*
|
|
135
|
+
* Emits the incoming request, a completion metric (duration), and the response,
|
|
136
|
+
* each at a configurable log level (see `LoggingBehaviorOptions`). Request and
|
|
137
|
+
* response payloads are excluded by default and can be opted in, with key-based
|
|
138
|
+
* exclusion/redaction. Logging and serialization failures do not change the
|
|
139
|
+
* handler result or error. If the wrapped handler throws,
|
|
140
|
+
* the error is logged — including its stack trace and, when present, the error's
|
|
141
|
+
* `optionalParams` (see `ErrorWithOptionalParams`) — and then re-thrown
|
|
142
|
+
* unchanged. Writes through the logger passed to the constructor, or `console`.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* const pipeline = createPipeline({ behaviors: [new LoggingBehavior(pinoAdapter)] });
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export declare class LoggingBehavior implements IPipelineBehavior {
|
|
150
|
+
private readonly logger;
|
|
151
|
+
constructor(logger?: PipelineLogger);
|
|
152
|
+
/**
|
|
153
|
+
* Logs the request, a duration metric, and the response or error around
|
|
154
|
+
* `next()`, using the options from `context.getBehaviorOptions(LoggingBehavior)`
|
|
155
|
+
* and the defaults documented on {@link LoggingBehaviorOptions}.
|
|
156
|
+
*
|
|
157
|
+
* @param context - The pipeline context for the current request.
|
|
158
|
+
* @param next - Delegate that invokes the next behavior/handler in the chain.
|
|
159
|
+
* @returns The value resolved by `next()`.
|
|
160
|
+
* @throws Whatever `next()` throws, unchanged, after logging it. A logging
|
|
161
|
+
* or serialization failure never changes the returned value or the error.
|
|
162
|
+
*/
|
|
163
|
+
handle(context: IPipelineContext, next: NextDelegate): Promise<unknown>;
|
|
164
|
+
private observe;
|
|
165
|
+
/**
|
|
166
|
+
* Builds the sanitizer configuration for payload logging.
|
|
167
|
+
*
|
|
168
|
+
* The default sensitive keys are masked unless `redactSensitiveKeys` is
|
|
169
|
+
* false; keys listed in `redactKeys` are always masked. With no key to mask,
|
|
170
|
+
* only the exclusion set applies.
|
|
171
|
+
*/
|
|
172
|
+
private buildSanitizeOptions;
|
|
173
|
+
/**
|
|
174
|
+
* Dispatches `message` (and any `optionalParams`) to the logger method
|
|
175
|
+
* matching `level` — e.g. `level: 'warn'` calls `this.logger.warn(...)`.
|
|
176
|
+
* No-op when `level` is `'none'` or the underlying logger doesn't
|
|
177
|
+
* implement that method.
|
|
178
|
+
*/
|
|
179
|
+
private log;
|
|
180
|
+
/**
|
|
181
|
+
* Extracts extra, loggable context from an error matching
|
|
182
|
+
* `ErrorWithOptionalParams`: a non-null object exposing a defined
|
|
183
|
+
* `optionalParams` property. `handle`'s `catch` branch uses this to decide
|
|
184
|
+
* whether the error log should be enriched with that extra context.
|
|
185
|
+
*
|
|
186
|
+
* The value is normalized into an array so it can be spread into the error
|
|
187
|
+
* log payload: passed through as-is if it's already an array, or wrapped in a
|
|
188
|
+
* single-element array otherwise.
|
|
189
|
+
*
|
|
190
|
+
* @returns The normalized `optionalParams`, or `undefined` when the error is
|
|
191
|
+
* not an object or does not define `optionalParams`.
|
|
192
|
+
*/
|
|
193
|
+
private optionalParamsOf;
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=logging.behavior.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging.behavior.d.ts","sourceRoot":"","sources":["../../src/behaviors/logging.behavior.ts"],"names":[],"mappings":"AASA,OAAO,EACL,iBAAiB,EACjB,YAAY,EACb,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AAC/E,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE7D;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,QAAQ,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAEnC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAElC;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,GAAG,CAAC,UAAU,EAAE,QAAQ,GAAG,MAAM,CAAC,CAAC;IAEjD;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAE5C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAEvB;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAEtB;;;;;;;;;;;;;;;OAeG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAE9B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;CACnC;AAOD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,eAAgB,YAAW,iBAAiB;IACvD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IAExC,YAAY,MAAM,CAAC,EAAE,cAAc,EAElC;IAED;;;;;;;;;;OAUG;IACG,MAAM,CACV,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,OAAO,CAAC,CA6HlB;IAED,OAAO,CAAC,OAAO;IAsBf;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB;IAiB5B;;;;;OAKG;IACH,OAAO,CAAC,GAAG;IAeX;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,gBAAgB;CAMzB"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { DEFAULT_REDACT_KEYS, safeSanitize, safeStringify, } from '@cqrs-ddd/safe-stringify';
|
|
4
|
+
/**
|
|
5
|
+
* Pipeline behavior that logs each request/response and execution timing.
|
|
6
|
+
*
|
|
7
|
+
* Emits the incoming request, a completion metric (duration), and the response,
|
|
8
|
+
* each at a configurable log level (see `LoggingBehaviorOptions`). Request and
|
|
9
|
+
* response payloads are excluded by default and can be opted in, with key-based
|
|
10
|
+
* exclusion/redaction. Logging and serialization failures do not change the
|
|
11
|
+
* handler result or error. If the wrapped handler throws,
|
|
12
|
+
* the error is logged — including its stack trace and, when present, the error's
|
|
13
|
+
* `optionalParams` (see `ErrorWithOptionalParams`) — and then re-thrown
|
|
14
|
+
* unchanged. Writes through the logger passed to the constructor, or `console`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const pipeline = createPipeline({ behaviors: [new LoggingBehavior(pinoAdapter)] });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class LoggingBehavior {
|
|
22
|
+
logger;
|
|
23
|
+
constructor(logger) {
|
|
24
|
+
this.logger = logger ?? console;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Logs the request, a duration metric, and the response or error around
|
|
28
|
+
* `next()`, using the options from `context.getBehaviorOptions(LoggingBehavior)`
|
|
29
|
+
* and the defaults documented on {@link LoggingBehaviorOptions}.
|
|
30
|
+
*
|
|
31
|
+
* @param context - The pipeline context for the current request.
|
|
32
|
+
* @param next - Delegate that invokes the next behavior/handler in the chain.
|
|
33
|
+
* @returns The value resolved by `next()`.
|
|
34
|
+
* @throws Whatever `next()` throws, unchanged, after logging it. A logging
|
|
35
|
+
* or serialization failure never changes the returned value or the error.
|
|
36
|
+
*/
|
|
37
|
+
async handle(context, next) {
|
|
38
|
+
const options = context.getBehaviorOptions(LoggingBehavior);
|
|
39
|
+
const metricLogLevel = options?.metricLogLevel ?? 'log';
|
|
40
|
+
const requestResponseLogLevel = options?.requestResponseLogLevel ?? 'debug';
|
|
41
|
+
const errorLogLevel = options?.errorLogLevel ?? 'error';
|
|
42
|
+
const excludeKeys = new Set(options?.excludeKeys);
|
|
43
|
+
const sanitizeOptions = this.buildSanitizeOptions(options, excludeKeys);
|
|
44
|
+
const excludeRequestObj = options?.excludeRequestObj ?? true;
|
|
45
|
+
const excludeResponseObj = options?.excludeResponseObj ?? true;
|
|
46
|
+
const logFormat = options?.logFormat ?? 'text';
|
|
47
|
+
const structured = logFormat === 'structured';
|
|
48
|
+
this.observe(() => {
|
|
49
|
+
if (requestResponseLogLevel === 'none')
|
|
50
|
+
return;
|
|
51
|
+
const requestPayload = excludeRequestObj
|
|
52
|
+
? '[exclude request obj]'
|
|
53
|
+
: structured
|
|
54
|
+
? safeSanitize(context.request, sanitizeOptions)
|
|
55
|
+
: safeStringify(context.request, sanitizeOptions);
|
|
56
|
+
this.log(requestResponseLogLevel, structured
|
|
57
|
+
? { msg: `Request → ${context.handlerName}`, request: requestPayload }
|
|
58
|
+
: `Request: ${requestPayload}`, context.handlerName);
|
|
59
|
+
});
|
|
60
|
+
const startTime = performance.now();
|
|
61
|
+
try {
|
|
62
|
+
const result = await next();
|
|
63
|
+
this.observe(() => {
|
|
64
|
+
const duration = (performance.now() - startTime).toFixed(2);
|
|
65
|
+
const metricMsg = `[${context.correlationId}] ${context.requestKind.toUpperCase()} ` +
|
|
66
|
+
`${context.requestName} → ${context.handlerName} completed in ${duration}ms`;
|
|
67
|
+
this.log(metricLogLevel, structured
|
|
68
|
+
? {
|
|
69
|
+
msg: metricMsg,
|
|
70
|
+
correlationId: context.correlationId,
|
|
71
|
+
requestKind: context.requestKind,
|
|
72
|
+
requestName: context.requestName,
|
|
73
|
+
handlerName: context.handlerName,
|
|
74
|
+
durationMs: Number(duration),
|
|
75
|
+
}
|
|
76
|
+
: metricMsg, context.handlerName);
|
|
77
|
+
if (requestResponseLogLevel === 'none')
|
|
78
|
+
return;
|
|
79
|
+
const responsePayload = excludeResponseObj
|
|
80
|
+
? '[exclude response obj]'
|
|
81
|
+
: result != null
|
|
82
|
+
? structured
|
|
83
|
+
? safeSanitize(result, sanitizeOptions)
|
|
84
|
+
: safeStringify(result, sanitizeOptions)
|
|
85
|
+
: '(void)';
|
|
86
|
+
this.log(requestResponseLogLevel, structured
|
|
87
|
+
? {
|
|
88
|
+
msg: `Response ← ${context.handlerName}`,
|
|
89
|
+
response: responsePayload,
|
|
90
|
+
}
|
|
91
|
+
: `Response: ${responsePayload}`, context.handlerName);
|
|
92
|
+
});
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
this.observe(() => {
|
|
97
|
+
const duration = (performance.now() - startTime).toFixed(2);
|
|
98
|
+
const err = error instanceof Error ? error : undefined;
|
|
99
|
+
let logLevel = errorLogLevel;
|
|
100
|
+
if (options?.mapLogLevel) {
|
|
101
|
+
let bestMatch;
|
|
102
|
+
for (const [errorType, level] of options.mapLogLevel.entries()) {
|
|
103
|
+
if (!(error instanceof errorType))
|
|
104
|
+
continue;
|
|
105
|
+
if (!bestMatch || errorType.prototype instanceof bestMatch) {
|
|
106
|
+
bestMatch = errorType;
|
|
107
|
+
logLevel = level;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const message = `[${context.correlationId}] ${context.requestKind.toUpperCase()} ` +
|
|
112
|
+
`${context.requestName} → ${context.handlerName} failed after ${duration}ms: ` +
|
|
113
|
+
`${err ? `${err.name}: ${err.message}` : String(error)}`;
|
|
114
|
+
const params = this.optionalParamsOf(error);
|
|
115
|
+
const optionalParams = params
|
|
116
|
+
? safeSanitize(params, sanitizeOptions)
|
|
117
|
+
: undefined;
|
|
118
|
+
if (structured) {
|
|
119
|
+
const payload = {
|
|
120
|
+
message,
|
|
121
|
+
...(err?.stack ? { stack: err.stack } : {}),
|
|
122
|
+
...(optionalParams ? { optionalParams } : {}),
|
|
123
|
+
};
|
|
124
|
+
this.log(logLevel, payload, context.handlerName);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.log(logLevel, message, ...(err?.stack ? [err.stack] : []), ...(optionalParams ?? []), context.handlerName);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
observe(write) {
|
|
134
|
+
try {
|
|
135
|
+
write();
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Observability failures must not alter business execution or error
|
|
139
|
+
// identity, but they must not be invisible either: a payload that always
|
|
140
|
+
// throws would otherwise silence a handler's logging with no trace.
|
|
141
|
+
try {
|
|
142
|
+
this.log('warn', `LoggingBehavior suppressed a log write: ${error instanceof Error
|
|
143
|
+
? `${error.name}: ${error.message}`
|
|
144
|
+
: 'unknown error'}`);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// The logger itself is unusable; nothing further is safe to attempt.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Builds the sanitizer configuration for payload logging.
|
|
153
|
+
*
|
|
154
|
+
* The default sensitive keys are masked unless `redactSensitiveKeys` is
|
|
155
|
+
* false; keys listed in `redactKeys` are always masked. With no key to mask,
|
|
156
|
+
* only the exclusion set applies.
|
|
157
|
+
*/
|
|
158
|
+
buildSanitizeOptions(options, excludeKeys) {
|
|
159
|
+
const redactKeys = [
|
|
160
|
+
...(options?.redactSensitiveKeys === false ? [] : DEFAULT_REDACT_KEYS),
|
|
161
|
+
...(options?.redactKeys ?? []),
|
|
162
|
+
];
|
|
163
|
+
if (redactKeys.length === 0)
|
|
164
|
+
return excludeKeys;
|
|
165
|
+
return {
|
|
166
|
+
excludeKeys,
|
|
167
|
+
redactKeys,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Dispatches `message` (and any `optionalParams`) to the logger method
|
|
172
|
+
* matching `level` — e.g. `level: 'warn'` calls `this.logger.warn(...)`.
|
|
173
|
+
* No-op when `level` is `'none'` or the underlying logger doesn't
|
|
174
|
+
* implement that method.
|
|
175
|
+
*/
|
|
176
|
+
log(level, message, ...optionalParams) {
|
|
177
|
+
if (level === 'none')
|
|
178
|
+
return;
|
|
179
|
+
const method = this.logger[level];
|
|
180
|
+
if (typeof method === 'function') {
|
|
181
|
+
method.call(this.logger, message, ...optionalParams);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Extracts extra, loggable context from an error matching
|
|
186
|
+
* `ErrorWithOptionalParams`: a non-null object exposing a defined
|
|
187
|
+
* `optionalParams` property. `handle`'s `catch` branch uses this to decide
|
|
188
|
+
* whether the error log should be enriched with that extra context.
|
|
189
|
+
*
|
|
190
|
+
* The value is normalized into an array so it can be spread into the error
|
|
191
|
+
* log payload: passed through as-is if it's already an array, or wrapped in a
|
|
192
|
+
* single-element array otherwise.
|
|
193
|
+
*
|
|
194
|
+
* @returns The normalized `optionalParams`, or `undefined` when the error is
|
|
195
|
+
* not an object or does not define `optionalParams`.
|
|
196
|
+
*/
|
|
197
|
+
optionalParamsOf(error) {
|
|
198
|
+
if (typeof error !== 'object' || error === null)
|
|
199
|
+
return undefined;
|
|
200
|
+
const { optionalParams } = error;
|
|
201
|
+
if (optionalParams === undefined)
|
|
202
|
+
return undefined;
|
|
203
|
+
return Array.isArray(optionalParams) ? optionalParams : [optionalParams];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=logging.behavior.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logging.behavior.js","sourceRoot":"","sources":["../../src/behaviors/logging.behavior.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { IPipelineContext } from '../interfaces/pipeline.context.interface.js';
|
|
3
|
+
/**
|
|
4
|
+
* Async-local store that propagates pipeline context across the async call chain.
|
|
5
|
+
* When a saga or nested CommandBus.execute() triggers a child handler,
|
|
6
|
+
* the child's PipelineContext inherits the parent's correlationId automatically.
|
|
7
|
+
*
|
|
8
|
+
* Stores a reference to the live context object so child pipelines inherit the
|
|
9
|
+
* same immutable correlation ID.
|
|
10
|
+
*
|
|
11
|
+
* Uses Node.js built-in `AsyncLocalStorage` — zero external dependencies.
|
|
12
|
+
*/
|
|
13
|
+
export declare const pipelineStore: AsyncLocalStorage<IPipelineContext<unknown, unknown>>;
|
|
14
|
+
/**
|
|
15
|
+
* Symbol-keyed setter for `response`. Only code that imports this symbol
|
|
16
|
+
* can write to `context.response` — keeping it readonly for behaviors.
|
|
17
|
+
*
|
|
18
|
+
* @internal Used by the pipeline runner.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SET_RESPONSE: unique symbol;
|
|
21
|
+
/** @internal Sets the correlation ID before execution begins. */
|
|
22
|
+
export declare const SET_CORRELATION_ID: unique symbol;
|
|
23
|
+
/**
|
|
24
|
+
* Symbol-keyed setter for `tenantId`. The runner assigns the tenant before the
|
|
25
|
+
* behavior chain starts; a custom runner may assign it on a context it
|
|
26
|
+
* constructs. The tenant is write-once: assigning a different value throws.
|
|
27
|
+
*/
|
|
28
|
+
export declare const SET_TENANT_ID: unique symbol;
|
|
29
|
+
/**
|
|
30
|
+
* Symbol-keyed setter for `request`, through which a behavior hands the handler a
|
|
31
|
+
* replacement input, such as a parsed copy; see `replaceRequest`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SET_REQUEST: unique symbol;
|
|
34
|
+
//# sourceMappingURL=pipeline-context.constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline-context.constants.d.ts","sourceRoot":"","sources":["../../src/constants/pipeline-context.constants.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AAE/E;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,uDAA4C,CAAC;AAEvE;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,OAAO,MAEjC,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,EAAE,OAAO,MAEvC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,OAAO,MAElC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,OAAO,MAA6C,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* Copyright (C) 2026-present Aristotelis — see repository license. */
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
/**
|
|
4
|
+
* Async-local store that propagates pipeline context across the async call chain.
|
|
5
|
+
* When a saga or nested CommandBus.execute() triggers a child handler,
|
|
6
|
+
* the child's PipelineContext inherits the parent's correlationId automatically.
|
|
7
|
+
*
|
|
8
|
+
* Stores a reference to the live context object so child pipelines inherit the
|
|
9
|
+
* same immutable correlation ID.
|
|
10
|
+
*
|
|
11
|
+
* Uses Node.js built-in `AsyncLocalStorage` — zero external dependencies.
|
|
12
|
+
*/
|
|
13
|
+
export const pipelineStore = new AsyncLocalStorage();
|
|
14
|
+
/**
|
|
15
|
+
* Symbol-keyed setter for `response`. Only code that imports this symbol
|
|
16
|
+
* can write to `context.response` — keeping it readonly for behaviors.
|
|
17
|
+
*
|
|
18
|
+
* @internal Used by the pipeline runner.
|
|
19
|
+
*/
|
|
20
|
+
export const SET_RESPONSE = Symbol('PipelineContext.setResponse');
|
|
21
|
+
/** @internal Sets the correlation ID before execution begins. */
|
|
22
|
+
export const SET_CORRELATION_ID = Symbol('PipelineContext.setCorrelationId');
|
|
23
|
+
/**
|
|
24
|
+
* Symbol-keyed setter for `tenantId`. The runner assigns the tenant before the
|
|
25
|
+
* behavior chain starts; a custom runner may assign it on a context it
|
|
26
|
+
* constructs. The tenant is write-once: assigning a different value throws.
|
|
27
|
+
*/
|
|
28
|
+
export const SET_TENANT_ID = Symbol('PipelineContext.setTenantId');
|
|
29
|
+
/**
|
|
30
|
+
* Symbol-keyed setter for `request`, through which a behavior hands the handler a
|
|
31
|
+
* replacement input, such as a parsed copy; see `replaceRequest`.
|
|
32
|
+
*/
|
|
33
|
+
export const SET_REQUEST = Symbol('PipelineContext.setRequest');
|
|
34
|
+
//# sourceMappingURL=pipeline-context.constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline-context.constants.js","sourceRoot":"","sources":["../../src/constants/pipeline-context.constants.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGrD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,iBAAiB,EAAoB,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAkB,MAAM,CAC/C,6BAA6B,CAC9B,CAAC;AAEF,iEAAiE;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAkB,MAAM,CACrD,kCAAkC,CACnC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAkB,MAAM,CAChD,6BAA6B,CAC9B,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAkB,MAAM,CAAC,4BAA4B,CAAC,CAAC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { PipelineBehaviorEntry } from './entries.js';
|
|
2
|
+
import type { ContextSources } from './interfaces/context-source.interface.js';
|
|
3
|
+
import type { IPipelineBehavior } from './interfaces/pipeline.behavior.interface.js';
|
|
4
|
+
import type { PipelineLogger } from './logger.js';
|
|
5
|
+
import type { GlobalBehaviorsOptions } from './options/global-behaviors.options.js';
|
|
6
|
+
import { type RequestKind } from './services/pipeline-plan.js';
|
|
7
|
+
import type { Constructor } from './types.js';
|
|
8
|
+
/** The kinds a wrapped function or a request declares. */
|
|
9
|
+
export type DeclaredKind = Exclude<RequestKind, 'unknown'>;
|
|
10
|
+
/**
|
|
11
|
+
* Brand by which a request tells a pipeline its kind, so a wrapped method needs no
|
|
12
|
+
* `kind` option; the request's class name then names the operation. `@cqrs-ddd/core`
|
|
13
|
+
* puts it on `BaseCommand`, `BaseQuery` and `DomainEvent`; any class can declare it.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* class GetPriceQuery {
|
|
18
|
+
* get [REQUEST_KIND]() {
|
|
19
|
+
* return 'query' as const;
|
|
20
|
+
* }
|
|
21
|
+
* constructor(readonly sku: string) {}
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare const REQUEST_KIND: unique symbol;
|
|
26
|
+
/** Configuration of {@link createPipeline}. */
|
|
27
|
+
export interface PipelineOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Instances of the behaviors that need dependencies, one per behavior class. A placed
|
|
30
|
+
* behavior without an instance is constructed with no arguments.
|
|
31
|
+
*/
|
|
32
|
+
behaviors?: readonly IPipelineBehavior[];
|
|
33
|
+
/**
|
|
34
|
+
* Behaviors applied to every wrapped function, by scope: `before` the call-site
|
|
35
|
+
* entries, `after` them. A behavior placed here and at a call site runs once, at its
|
|
36
|
+
* global position, with the call-site options shallowly merged over these.
|
|
37
|
+
*/
|
|
38
|
+
globalBehaviors?: GlobalBehaviorsOptions | GlobalBehaviorsOptions[];
|
|
39
|
+
/** Where executions take their tenant and correlation id from. */
|
|
40
|
+
sources?: ContextSources;
|
|
41
|
+
/**
|
|
42
|
+
* What a behavior contract violation does when a function or method is wrapped (module
|
|
43
|
+
* load or class definition): throw (`'strict'`), log a warning (`'warn'`), or nothing
|
|
44
|
+
* (`'off'`).
|
|
45
|
+
* @default 'strict'
|
|
46
|
+
*/
|
|
47
|
+
diagnostics?: 'strict' | 'warn' | 'off';
|
|
48
|
+
/**
|
|
49
|
+
* Receives the `'warn'` diagnostics, and is the logger of a `LoggingBehavior` the pipeline
|
|
50
|
+
* constructs itself (one placed without an instance in `behaviors`).
|
|
51
|
+
* @default console
|
|
52
|
+
*/
|
|
53
|
+
logger?: PipelineLogger;
|
|
54
|
+
}
|
|
55
|
+
/** Options of one {@link Pipeline.wrap} call. */
|
|
56
|
+
export interface WrapOptions {
|
|
57
|
+
/**
|
|
58
|
+
* The operation name, used in keys and logs. Required for a plain function; a method
|
|
59
|
+
* defaults to `Class.method`, or to the class name of a branded request.
|
|
60
|
+
*/
|
|
61
|
+
name?: string;
|
|
62
|
+
/**
|
|
63
|
+
* What the operation does: a `query` reads, a `command` changes state, an `event`
|
|
64
|
+
* reacts. Required unless every request carries {@link REQUEST_KIND}.
|
|
65
|
+
*/
|
|
66
|
+
kind?: DeclaredKind;
|
|
67
|
+
/** Global behaviors this operation opts out of. */
|
|
68
|
+
skip?: readonly Constructor<IPipelineBehavior>[];
|
|
69
|
+
}
|
|
70
|
+
type AnyFunction = (...args: any[]) => unknown;
|
|
71
|
+
/**
|
|
72
|
+
* What {@link Pipeline.wrap} returns: wraps a plain function, or decorates a method in
|
|
73
|
+
* the standard and the `experimentalDecorators` mode. The wrapped function returns a
|
|
74
|
+
* promise.
|
|
75
|
+
*/
|
|
76
|
+
export interface Wrap {
|
|
77
|
+
<F extends AnyFunction>(fn: F): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>;
|
|
78
|
+
<F extends AnyFunction>(method: F, context: ClassMethodDecoratorContext): F;
|
|
79
|
+
(target: object, propertyKey: string | symbol, descriptor: PropertyDescriptor): PropertyDescriptor;
|
|
80
|
+
}
|
|
81
|
+
/** A configured pipeline; see {@link createPipeline}. */
|
|
82
|
+
export interface Pipeline {
|
|
83
|
+
/**
|
|
84
|
+
* Prepares the behaviors of one operation: entries are behavior classes or
|
|
85
|
+
* `[Behavior, options]` tuples, outermost first, such as `cache({ key })`.
|
|
86
|
+
*
|
|
87
|
+
* @throws TypeError on a malformed entry or an unknown `kind`.
|
|
88
|
+
* @throws PipelineConfigurationError in `'strict'` mode when the result wraps a function
|
|
89
|
+
* or method of a declared `kind` whose behaviors violate a contract.
|
|
90
|
+
*/
|
|
91
|
+
wrap(...entries: PipelineBehaviorEntry[]): Wrap;
|
|
92
|
+
wrap(options: WrapOptions, ...entries: PipelineBehaviorEntry[]): Wrap;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Creates a pipeline: behaviors that wrap plain functions and class methods with
|
|
96
|
+
* cross-cutting concerns, with no framework and no dependency-injection container.
|
|
97
|
+
*
|
|
98
|
+
* Global behaviors are constructed when the pipeline is created, so a behavior whose
|
|
99
|
+
* required dependency is missing fails here rather than at its first call.
|
|
100
|
+
*
|
|
101
|
+
* @throws TypeError on a malformed global entry or two instances of one behavior class.
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* const pipeline = createPipeline({
|
|
105
|
+
* behaviors: [new CacheBehavior(buildCache({}))],
|
|
106
|
+
* globalBehaviors: { before: [logging({ requestResponseLogLevel: 'log' })] },
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* export const getPrice = pipeline.wrap(
|
|
110
|
+
* { name: 'getPrice', kind: 'query' },
|
|
111
|
+
* cache({ key }),
|
|
112
|
+
* )(async (sku: string) => prices.find(sku));
|
|
113
|
+
*
|
|
114
|
+
* class Prices {
|
|
115
|
+
* @pipeline.wrap({ kind: 'query' }, cache({ key }))
|
|
116
|
+
* async find(sku: string) {}
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export declare function createPipeline(options?: PipelineOptions): Pipeline;
|
|
121
|
+
export {};
|
|
122
|
+
//# sourceMappingURL=create-pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-pipeline.d.ts","sourceRoot":"","sources":["../src/create-pipeline.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAM1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6CAA6C,CAAC;AAKrF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAEpF,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,6BAA6B,CAAC;AAKrC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,0DAA0D;AAC1D,MAAM,MAAM,YAAY,GAAG,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;AAI3D;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,eAAuC,CAAC;AAEjE,+CAA+C;AAC/C,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACzC;;;;OAIG;IACH,eAAe,CAAC,EAAE,sBAAsB,GAAG,sBAAsB,EAAE,CAAC;IACpE,kEAAkE;IAClE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;IACxC;;;;OAIG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,mDAAmD;IACnD,IAAI,CAAC,EAAE,SAAS,WAAW,CAAC,iBAAiB,CAAC,EAAE,CAAC;CAClD;AAGD,KAAK,WAAW,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;AAE/C;;;;GAIG;AACH,MAAM,WAAW,IAAI;IACnB,CAAC,CAAC,SAAS,WAAW,EACpB,EAAE,EAAE,CAAC,GACJ,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,SAAS,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,2BAA2B,GAAG,CAAC,CAAC;IAC5E,CACE,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,MAAM,EAC5B,UAAU,EAAE,kBAAkB,GAC7B,kBAAkB,CAAC;CACvB;AAED,yDAAyD;AACzD,MAAM,WAAW,QAAQ;IACvB;;;;;;;OAOG;IACH,IAAI,CAAC,GAAG,OAAO,EAAE,qBAAqB,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,OAAO,EAAE,qBAAqB,EAAE,GAAG,IAAI,CAAC;CACvE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,eAAoB,GAAG,QAAQ,CA0KtE"}
|