@zudojs/cqrs 1.0.0 → 1.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/README.md +10 -0
- package/dist/command/commandBus.core.js +1 -1
- package/dist/cqrsErrors/cqrsError.base.d.ts +5 -4
- package/dist/cqrsErrors/cqrsError.base.js +5 -14
- package/dist/cqrsMiddleware/cqrsMiddleware.compose.d.ts +14 -0
- package/dist/cqrsMiddleware/cqrsMiddleware.compose.js +33 -0
- package/dist/cqrsMiddleware/cqrsMiddleware.core.d.ts +14 -11
- package/dist/cqrsMiddleware/cqrsMiddleware.core.js +43 -31
- package/dist/cqrsMiddleware/index.d.ts +1 -0
- package/dist/cqrsMiddleware/index.js +1 -0
- package/dist/query/queryBus.core.js +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Command Query Responsibility Segregation (CQRS) primitives for separating read and write operations.
|
|
4
4
|
|
|
5
|
+
<!-- zudo-docs:start -->
|
|
6
|
+
|
|
7
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-cqrs](https://zudojs.oyinlola.site/docs/packages-cqrs) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-cqrs.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
8
|
+
|
|
9
|
+
<!-- zudo-docs:end -->
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
```bash
|
|
@@ -103,6 +109,10 @@ Validation and handler resolution run at the end of the middleware pipeline,
|
|
|
103
109
|
so middleware observes `InvalidCommandError` / `CommandHandlerNotFoundError`
|
|
104
110
|
like any other failure. Each middleware may call `next()` at most once.
|
|
105
111
|
|
|
112
|
+
`onTiming` is an observer: if it throws, the command or query still
|
|
113
|
+
succeeds (or fails with its own error). Pass `onTimingError` to see those
|
|
114
|
+
observer failures.
|
|
115
|
+
|
|
106
116
|
## Features
|
|
107
117
|
|
|
108
118
|
- Command bus for write operations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { executeCommandHandler } from "../command/commandHandler.core.js";
|
|
2
2
|
import { CommandHandlerNotFoundError, DuplicateHandlerError, InvalidCommandError, } from "../cqrsErrors/cqrsError.base.js";
|
|
3
|
-
import { composeMiddleware } from "../cqrsMiddleware/cqrsMiddleware.
|
|
3
|
+
import { composeMiddleware } from "../cqrsMiddleware/cqrsMiddleware.compose.js";
|
|
4
4
|
import { assertExecutableHandler, assertHandlerType, assertMiddleware, } from "../cqrsValidation/cqrsValidation.core.js";
|
|
5
5
|
/**
|
|
6
6
|
* Command bus implementation.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CqrsError, ErrorCode, type ErrorMetadata } from "@zudojs/errors";
|
|
2
2
|
/**
|
|
3
3
|
* Base error for failures originating from the CQRS package.
|
|
4
|
+
*
|
|
5
|
+
* Owned by `@zudojs/errors` and re-exported here, so
|
|
6
|
+
* `instanceof CqrsError` matches across both packages.
|
|
4
7
|
*/
|
|
5
|
-
export
|
|
6
|
-
constructor(message: string, options?: BaseErrorOptions);
|
|
7
|
-
}
|
|
8
|
+
export { CqrsError };
|
|
8
9
|
/**
|
|
9
10
|
* Thrown when a CQRS request is invalid.
|
|
10
11
|
*
|
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import { BaseError, ErrorCategory, ErrorCode, ErrorSeverity, } from "@zudojs/errors";
|
|
1
|
+
import { BaseError, CqrsError, ErrorCategory, ErrorCode, ErrorSeverity, } from "@zudojs/errors";
|
|
2
2
|
/**
|
|
3
3
|
* Base error for failures originating from the CQRS package.
|
|
4
|
+
*
|
|
5
|
+
* Owned by `@zudojs/errors` and re-exported here, so
|
|
6
|
+
* `instanceof CqrsError` matches across both packages.
|
|
4
7
|
*/
|
|
5
|
-
export
|
|
6
|
-
constructor(message, options = {}) {
|
|
7
|
-
super(message, {
|
|
8
|
-
...options,
|
|
9
|
-
code: options.code ?? ErrorCode.INTERNAL_ERROR,
|
|
10
|
-
category: options.category ?? ErrorCategory.SYSTEM,
|
|
11
|
-
severity: options.severity ?? ErrorSeverity.ERROR,
|
|
12
|
-
statusCode: options.statusCode ?? 500,
|
|
13
|
-
expose: options.expose ?? false,
|
|
14
|
-
isOperational: options.isOperational ?? true,
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
}
|
|
8
|
+
export { CqrsError };
|
|
18
9
|
/**
|
|
19
10
|
* Thrown when a CQRS request is invalid.
|
|
20
11
|
*
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CQRS middleware composition, built on `compose` from `@zudojs/middleware`.
|
|
3
|
+
*/
|
|
4
|
+
import type { CqrsMiddleware } from "../cqrsTypes/cqrsTypes.type.js";
|
|
5
|
+
/**
|
|
6
|
+
* Combines multiple middleware functions into a single middleware.
|
|
7
|
+
*
|
|
8
|
+
* Each middleware may call `next()` at most once per execution; a second
|
|
9
|
+
* call throws `MiddlewareExecutionError`. The command and query buses
|
|
10
|
+
* build their pipelines with this function, so the same rule applies
|
|
11
|
+
* there.
|
|
12
|
+
*/
|
|
13
|
+
export declare function composeMiddleware(middleware: readonly CqrsMiddleware[]): CqrsMiddleware;
|
|
14
|
+
//# sourceMappingURL=cqrsMiddleware.compose.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CQRS middleware composition, built on `compose` from `@zudojs/middleware`.
|
|
3
|
+
*/
|
|
4
|
+
import { compose } from "@zudojs/middleware";
|
|
5
|
+
import { MiddlewareExecutionError } from "../cqrsErrors/cqrsError.base.js";
|
|
6
|
+
/** Adapts one CQRS middleware to the shared `(context, next)` shape. */
|
|
7
|
+
function adapt(middleware) {
|
|
8
|
+
return async (state, next) => {
|
|
9
|
+
let called = false;
|
|
10
|
+
return middleware(state.request, state.context, (request, context) => {
|
|
11
|
+
if (called) {
|
|
12
|
+
return Promise.reject(new MiddlewareExecutionError());
|
|
13
|
+
}
|
|
14
|
+
called = true;
|
|
15
|
+
state.request = request;
|
|
16
|
+
state.context = context;
|
|
17
|
+
return next();
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Combines multiple middleware functions into a single middleware.
|
|
23
|
+
*
|
|
24
|
+
* Each middleware may call `next()` at most once per execution; a second
|
|
25
|
+
* call throws `MiddlewareExecutionError`. The command and query buses
|
|
26
|
+
* build their pipelines with this function, so the same rule applies
|
|
27
|
+
* there.
|
|
28
|
+
*/
|
|
29
|
+
export function composeMiddleware(middleware) {
|
|
30
|
+
const run = compose(middleware.map(adapt), async (state) => state.terminal(state.request, state.context), { maxDepth: Number.POSITIVE_INFINITY });
|
|
31
|
+
return async (request, context, terminal) => run({ request, context, terminal });
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=cqrsMiddleware.compose.js.map
|
|
@@ -45,12 +45,19 @@ export interface CqrsTiming {
|
|
|
45
45
|
export interface TimingMiddlewareOptions extends MiddlewareOptions {
|
|
46
46
|
/**
|
|
47
47
|
* Receives the measurement after each execution (success or failure).
|
|
48
|
-
*
|
|
48
|
+
* The callback is an observer: if it throws or rejects, the error is
|
|
49
|
+
* passed to `onTimingError` (when given) and otherwise discarded. It
|
|
50
|
+
* never changes the outcome of the command or query.
|
|
49
51
|
*
|
|
50
52
|
* Optional: measurements are always exposed through
|
|
51
53
|
* `TimingMiddleware.lastTiming` as well.
|
|
52
54
|
*/
|
|
53
55
|
readonly onTiming?: (timing: CqrsTiming) => void | Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Receives any error thrown by `onTiming`. Errors thrown here are
|
|
58
|
+
* discarded as well.
|
|
59
|
+
*/
|
|
60
|
+
readonly onTimingError?: (error: unknown, timing: CqrsTiming) => void;
|
|
54
61
|
/**
|
|
55
62
|
* Monotonic clock in milliseconds. Defaults to `performance.now()`.
|
|
56
63
|
*/
|
|
@@ -104,7 +111,12 @@ export declare function contextMiddleware(options?: MiddlewareOptions): CqrsMidd
|
|
|
104
111
|
* `acquire` must resolve to a release function.
|
|
105
112
|
*/
|
|
106
113
|
export interface CqrsLock {
|
|
107
|
-
|
|
114
|
+
/**
|
|
115
|
+
* The release function may be synchronous or return a promise; the
|
|
116
|
+
* middleware awaits it. A rejected release surfaces as a `CqrsError`
|
|
117
|
+
* (when the handler succeeded) rather than an unhandled rejection.
|
|
118
|
+
*/
|
|
119
|
+
acquire(key: string): (() => void | Promise<void>) | Promise<() => void | Promise<void>>;
|
|
108
120
|
}
|
|
109
121
|
/**
|
|
110
122
|
* Options for `lockMiddleware`.
|
|
@@ -129,15 +141,6 @@ export declare function commandMiddleware(middleware: CommandMiddleware): CqrsMi
|
|
|
129
141
|
* Adapts query-specific middleware to generic CQRS middleware.
|
|
130
142
|
*/
|
|
131
143
|
export declare function queryMiddleware(middleware: QueryMiddleware): CqrsMiddleware;
|
|
132
|
-
/**
|
|
133
|
-
* Combines multiple middleware functions into a single middleware.
|
|
134
|
-
*
|
|
135
|
-
* Each middleware may call `next()` at most once per execution; a second
|
|
136
|
-
* call throws `MiddlewareExecutionError`. The command and query buses
|
|
137
|
-
* build their pipelines with this function, so the same rule applies
|
|
138
|
-
* there.
|
|
139
|
-
*/
|
|
140
|
-
export declare function composeMiddleware(middleware: readonly CqrsMiddleware[]): CqrsMiddleware;
|
|
141
144
|
/**
|
|
142
145
|
* Creates a middleware that runs a callback before execution.
|
|
143
146
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseError } from "@zudojs/errors";
|
|
2
|
-
import { CqrsError, CqrsValidationError, InvalidMiddlewareError,
|
|
2
|
+
import { CqrsError, CqrsValidationError, InvalidMiddlewareError, } from "../cqrsErrors/cqrsError.base.js";
|
|
3
3
|
/**
|
|
4
4
|
* Middleware that measures command or query execution time.
|
|
5
5
|
*
|
|
@@ -10,6 +10,10 @@ export function timingMiddleware(options = {}) {
|
|
|
10
10
|
if (options.onTiming !== undefined && typeof options.onTiming !== "function") {
|
|
11
11
|
throw new InvalidMiddlewareError("timingMiddleware onTiming must be a function.");
|
|
12
12
|
}
|
|
13
|
+
if (options.onTimingError !== undefined &&
|
|
14
|
+
typeof options.onTimingError !== "function") {
|
|
15
|
+
throw new InvalidMiddlewareError("timingMiddleware onTimingError must be a function.");
|
|
16
|
+
}
|
|
13
17
|
if (options.now !== undefined && typeof options.now !== "function") {
|
|
14
18
|
throw new InvalidMiddlewareError("timingMiddleware now must be a function.");
|
|
15
19
|
}
|
|
@@ -21,9 +25,20 @@ export function timingMiddleware(options = {}) {
|
|
|
21
25
|
const record = async (timing) => {
|
|
22
26
|
lastTiming = timing;
|
|
23
27
|
count += 1;
|
|
24
|
-
if (onTiming) {
|
|
28
|
+
if (!onTiming) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
25
32
|
await onTiming(timing);
|
|
26
33
|
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
try {
|
|
36
|
+
options.onTimingError?.(error, timing);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
27
42
|
};
|
|
28
43
|
const middleware = async (request, context, next) => {
|
|
29
44
|
if (options.enabled === false) {
|
|
@@ -154,12 +169,35 @@ export function lockMiddleware(lock, options = {}) {
|
|
|
154
169
|
lockKey: key,
|
|
155
170
|
});
|
|
156
171
|
}
|
|
172
|
+
// `release()` may be asynchronous (a Redis DEL, say). It used to be
|
|
173
|
+
// called and dropped, so a rejected release was an unhandled promise
|
|
174
|
+
// rejection — which terminates the process under Node's defaults.
|
|
175
|
+
let result;
|
|
157
176
|
try {
|
|
158
|
-
|
|
177
|
+
result = await next(request, context);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
try {
|
|
181
|
+
await release();
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// The handler's failure is the one the caller needs to see.
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await release();
|
|
159
190
|
}
|
|
160
|
-
|
|
161
|
-
release()
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw new CqrsError(`CQRS lock release failed (key "${key}").`, {
|
|
193
|
+
cause: error,
|
|
194
|
+
metadata: {
|
|
195
|
+
lockKey: key,
|
|
196
|
+
requestType: getRequestType(request),
|
|
197
|
+
},
|
|
198
|
+
});
|
|
162
199
|
}
|
|
200
|
+
return result;
|
|
163
201
|
};
|
|
164
202
|
}
|
|
165
203
|
/**
|
|
@@ -178,32 +216,6 @@ export function queryMiddleware(middleware) {
|
|
|
178
216
|
return middleware(request, context, async (query, nextContext) => next(query, nextContext));
|
|
179
217
|
};
|
|
180
218
|
}
|
|
181
|
-
/**
|
|
182
|
-
* Combines multiple middleware functions into a single middleware.
|
|
183
|
-
*
|
|
184
|
-
* Each middleware may call `next()` at most once per execution; a second
|
|
185
|
-
* call throws `MiddlewareExecutionError`. The command and query buses
|
|
186
|
-
* build their pipelines with this function, so the same rule applies
|
|
187
|
-
* there.
|
|
188
|
-
*/
|
|
189
|
-
export function composeMiddleware(middleware) {
|
|
190
|
-
const stack = [...middleware];
|
|
191
|
-
return async (request, context, terminal) => {
|
|
192
|
-
let index = -1;
|
|
193
|
-
const dispatch = async (currentIndex, currentRequest, currentContext) => {
|
|
194
|
-
if (currentIndex <= index) {
|
|
195
|
-
throw new MiddlewareExecutionError();
|
|
196
|
-
}
|
|
197
|
-
index = currentIndex;
|
|
198
|
-
const current = stack[currentIndex];
|
|
199
|
-
if (!current) {
|
|
200
|
-
return terminal(currentRequest, currentContext);
|
|
201
|
-
}
|
|
202
|
-
return current(currentRequest, currentContext, (nextRequest, nextContext) => dispatch(currentIndex + 1, nextRequest, nextContext));
|
|
203
|
-
};
|
|
204
|
-
return dispatch(0, request, context);
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
219
|
/**
|
|
208
220
|
* Creates a middleware that runs a callback before execution.
|
|
209
221
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { executeQueryHandler } from "../query/queryHandler.core.js";
|
|
2
2
|
import { QueryHandlerNotFoundError, DuplicateHandlerError, InvalidQueryError, } from "../cqrsErrors/cqrsError.base.js";
|
|
3
|
-
import { composeMiddleware } from "../cqrsMiddleware/cqrsMiddleware.
|
|
3
|
+
import { composeMiddleware } from "../cqrsMiddleware/cqrsMiddleware.compose.js";
|
|
4
4
|
import { assertExecutableHandler, assertHandlerType, assertMiddleware, } from "../cqrsValidation/cqrsValidation.core.js";
|
|
5
5
|
/**
|
|
6
6
|
* Query bus implementation.
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/cqrs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Command Query Responsibility Segregation (CQRS) primitives for separating read and write operations.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -21,8 +25,9 @@
|
|
|
21
25
|
"!dist/.tsbuildinfo"
|
|
22
26
|
],
|
|
23
27
|
"dependencies": {
|
|
24
|
-
"@zudojs/errors": "1.
|
|
25
|
-
"@zudojs/events": "1.
|
|
28
|
+
"@zudojs/errors": "1.1.0",
|
|
29
|
+
"@zudojs/events": "1.1.0",
|
|
30
|
+
"@zudojs/middleware": "1.0.2"
|
|
26
31
|
},
|
|
27
32
|
"devDependencies": {
|
|
28
33
|
"typescript": "7.0.2",
|