@zudojs/cqrs 0.1.0 → 1.0.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 +88 -7
- package/dist/command/command.core.d.ts +5 -2
- package/dist/command/command.core.js +11 -7
- package/dist/command/commandBus.core.d.ts +20 -6
- package/dist/command/commandBus.core.js +47 -62
- package/dist/command/commandHandler.core.d.ts +9 -3
- package/dist/command/commandHandler.core.js +23 -6
- package/dist/cqrsContext/cqrsContext.core.d.ts +11 -0
- package/dist/cqrsContext/cqrsContext.core.js +25 -7
- package/dist/cqrsDecorators/cqrsDecorators.core.d.ts +14 -8
- package/dist/cqrsDecorators/cqrsDecorators.core.js +79 -60
- package/dist/cqrsErrors/cqrsError.base.d.ts +34 -2
- package/dist/cqrsErrors/cqrsError.base.js +58 -5
- package/dist/cqrsEvents/cqrsEvents.type.d.ts +13 -6
- package/dist/cqrsEvents/cqrsEvents.type.js +11 -1
- package/dist/cqrsMiddleware/cqrsMiddleware.core.d.ts +95 -7
- package/dist/cqrsMiddleware/cqrsMiddleware.core.js +97 -29
- package/dist/cqrsMiddleware/index.d.ts +2 -1
- package/dist/cqrsMiddleware/index.js +2 -1
- package/dist/cqrsTypes/cqrsTypes.type.d.ts +17 -30
- package/dist/cqrsTypes/cqrsTypes.type.js +0 -16
- package/dist/cqrsValidation/cqrsValidation.core.d.ts +32 -0
- package/dist/cqrsValidation/cqrsValidation.core.js +53 -0
- package/dist/cqrsValidation/index.d.ts +7 -0
- package/dist/cqrsValidation/index.js +7 -0
- package/dist/handlerRegistry/handlerRegistry.core.d.ts +5 -0
- package/dist/handlerRegistry/handlerRegistry.core.js +28 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -2
- package/dist/query/query.core.d.ts +5 -2
- package/dist/query/query.core.js +11 -7
- package/dist/query/queryBus.core.d.ts +20 -6
- package/dist/query/queryBus.core.js +47 -62
- package/dist/query/queryHandler.core.d.ts +9 -3
- package/dist/query/queryHandler.core.js +23 -6
- package/package.json +22 -14
- package/dist/command/command.core.d.ts.map +0 -1
- package/dist/command/command.core.js.map +0 -1
- package/dist/command/commandBus.core.d.ts.map +0 -1
- package/dist/command/commandBus.core.js.map +0 -1
- package/dist/command/commandHandler.core.d.ts.map +0 -1
- package/dist/command/commandHandler.core.js.map +0 -1
- package/dist/command/commandResult.type.d.ts.map +0 -1
- package/dist/command/commandResult.type.js.map +0 -1
- package/dist/command/index.d.ts.map +0 -1
- package/dist/command/index.js.map +0 -1
- package/dist/cqrsContext/cqrsContext.core.d.ts.map +0 -1
- package/dist/cqrsContext/cqrsContext.core.js.map +0 -1
- package/dist/cqrsContext/index.d.ts.map +0 -1
- package/dist/cqrsContext/index.js.map +0 -1
- package/dist/cqrsDecorators/cqrsDecorators.core.d.ts.map +0 -1
- package/dist/cqrsDecorators/cqrsDecorators.core.js.map +0 -1
- package/dist/cqrsDecorators/index.d.ts.map +0 -1
- package/dist/cqrsDecorators/index.js.map +0 -1
- package/dist/cqrsErrors/cqrsError.base.d.ts.map +0 -1
- package/dist/cqrsErrors/cqrsError.base.js.map +0 -1
- package/dist/cqrsErrors/index.d.ts.map +0 -1
- package/dist/cqrsErrors/index.js.map +0 -1
- package/dist/cqrsEvents/cqrsEventBus.core.d.ts.map +0 -1
- package/dist/cqrsEvents/cqrsEventBus.core.js.map +0 -1
- package/dist/cqrsEvents/cqrsEventResult.type.d.ts.map +0 -1
- package/dist/cqrsEvents/cqrsEventResult.type.js.map +0 -1
- package/dist/cqrsEvents/cqrsEvents.type.d.ts.map +0 -1
- package/dist/cqrsEvents/cqrsEvents.type.js.map +0 -1
- package/dist/cqrsEvents/index.d.ts.map +0 -1
- package/dist/cqrsEvents/index.js.map +0 -1
- package/dist/cqrsMiddleware/cqrsMiddleware.core.d.ts.map +0 -1
- package/dist/cqrsMiddleware/cqrsMiddleware.core.js.map +0 -1
- package/dist/cqrsMiddleware/index.d.ts.map +0 -1
- package/dist/cqrsMiddleware/index.js.map +0 -1
- package/dist/cqrsTypes/cqrsTypes.type.d.ts.map +0 -1
- package/dist/cqrsTypes/cqrsTypes.type.js.map +0 -1
- package/dist/cqrsTypes/index.d.ts.map +0 -1
- package/dist/cqrsTypes/index.js.map +0 -1
- package/dist/handlerRegistry/handlerRegistry.core.d.ts.map +0 -1
- package/dist/handlerRegistry/handlerRegistry.core.js.map +0 -1
- package/dist/handlerRegistry/index.d.ts.map +0 -1
- package/dist/handlerRegistry/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/query/index.d.ts.map +0 -1
- package/dist/query/index.js.map +0 -1
- package/dist/query/query.core.d.ts.map +0 -1
- package/dist/query/query.core.js.map +0 -1
- package/dist/query/queryBus.core.d.ts.map +0 -1
- package/dist/query/queryBus.core.js.map +0 -1
- package/dist/query/queryHandler.core.d.ts.map +0 -1
- package/dist/query/queryHandler.core.js.map +0 -1
- package/dist/query/queryResult.type.d.ts.map +0 -1
- package/dist/query/queryResult.type.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zudojs Contributors
|
|
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
CHANGED
|
@@ -10,28 +10,109 @@ npm install @zudojs/cqrs
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
+
Commands and queries are plain objects with a `type` discriminator. Give the
|
|
14
|
+
bus explicit generics (or a typed handler) so the payload and result types
|
|
15
|
+
flow through `register` and `execute`.
|
|
16
|
+
|
|
13
17
|
```typescript
|
|
14
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
createCommandBus,
|
|
20
|
+
createQueryBus,
|
|
21
|
+
createQueryHandler,
|
|
22
|
+
type CommandOf,
|
|
23
|
+
type QueryOf,
|
|
24
|
+
} from "@zudojs/cqrs";
|
|
25
|
+
|
|
26
|
+
interface User {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type CreateUser = CommandOf<"CreateUser", { name: string }>;
|
|
32
|
+
type GetUser = QueryOf<"GetUser", { id: string }>;
|
|
15
33
|
|
|
16
34
|
const commandBus = createCommandBus();
|
|
17
35
|
const queryBus = createQueryBus();
|
|
18
36
|
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
// Function handler with explicit generics
|
|
38
|
+
commandBus.register<CreateUser, User>("CreateUser", async (command) => {
|
|
39
|
+
return userRepository.create({ name: command.name });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Typed handler object created with a factory
|
|
43
|
+
queryBus.register(
|
|
44
|
+
"GetUser",
|
|
45
|
+
createQueryHandler<GetUser, User | undefined>("GetUser", async (query) =>
|
|
46
|
+
userRepository.findById(query.id),
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const user = await commandBus.execute<CreateUser, User>({
|
|
51
|
+
type: "CreateUser",
|
|
52
|
+
name: "Alice",
|
|
21
53
|
});
|
|
22
54
|
|
|
23
|
-
queryBus.
|
|
24
|
-
|
|
55
|
+
const found = await queryBus.execute<GetUser, User | undefined>({
|
|
56
|
+
type: "GetUser",
|
|
57
|
+
id: user.id,
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Class-based handlers extend `CommandHandler` / `QueryHandler`; any object with an
|
|
62
|
+
`execute()` method is accepted as well.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { CommandHandler, createCommandBus } from "@zudojs/cqrs";
|
|
66
|
+
|
|
67
|
+
class CreateUserHandler extends CommandHandler<CreateUser, User> {
|
|
68
|
+
readonly commandType = "CreateUser";
|
|
69
|
+
|
|
70
|
+
async execute(command: CreateUser): Promise<User> {
|
|
71
|
+
return userRepository.create({ name: command.name });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
createCommandBus().register("CreateUser", new CreateUserHandler());
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Middleware
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import {
|
|
82
|
+
createCommandBus,
|
|
83
|
+
timingMiddleware,
|
|
84
|
+
errorMiddleware,
|
|
85
|
+
isCqrsError,
|
|
86
|
+
} from "@zudojs/cqrs";
|
|
87
|
+
|
|
88
|
+
const timing = timingMiddleware({
|
|
89
|
+
onTiming: ({ request, durationMs }) =>
|
|
90
|
+
console.log(`${request.type} took ${durationMs.toFixed(1)}ms`),
|
|
25
91
|
});
|
|
92
|
+
|
|
93
|
+
const bus = createCommandBus({ middleware: [timing, errorMiddleware()] });
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await bus.execute({ type: "Unknown" });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
isCqrsError(error); // true — CommandHandlerNotFoundError
|
|
99
|
+
}
|
|
26
100
|
```
|
|
27
101
|
|
|
102
|
+
Validation and handler resolution run at the end of the middleware pipeline,
|
|
103
|
+
so middleware observes `InvalidCommandError` / `CommandHandlerNotFoundError`
|
|
104
|
+
like any other failure. Each middleware may call `next()` at most once.
|
|
105
|
+
|
|
28
106
|
## Features
|
|
29
107
|
|
|
30
108
|
- Command bus for write operations
|
|
31
109
|
- Query bus for read operations
|
|
32
|
-
- Middleware pipeline for both
|
|
33
|
-
-
|
|
110
|
+
- Middleware pipeline for both (timing, error normalisation, validation, locking, context enrichment)
|
|
111
|
+
- Function, object and class-based handlers
|
|
112
|
+
- Class decorators (`CommandHandlerFor`, `QueryHandlerFor`, `CqrsHandler`) for handler discovery
|
|
113
|
+
- Dedicated error classes (`isCqrsError`)
|
|
34
114
|
- Result types for explicit returns
|
|
115
|
+
- Execution contexts with correlation chains
|
|
35
116
|
|
|
36
117
|
## Use Cases
|
|
37
118
|
|
|
@@ -25,10 +25,13 @@ export declare abstract class MetadataCommand<TType extends string = string> ext
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Creates a simple immutable command object.
|
|
28
|
+
*
|
|
29
|
+
* The `type` argument always wins over any `type` key present in the
|
|
30
|
+
* payload, so untrusted payloads cannot reroute the command.
|
|
28
31
|
*/
|
|
29
|
-
export declare function createCommand<TType extends string, TPayload extends Record<string, unknown> = Record<
|
|
32
|
+
export declare function createCommand<TType extends string, TPayload extends Record<string, unknown> = Record<never, never>>(type: TType, payload?: TPayload): Readonly<{
|
|
30
33
|
readonly type: TType;
|
|
31
|
-
} & TPayload
|
|
34
|
+
} & Omit<TPayload, "type">>;
|
|
32
35
|
/**
|
|
33
36
|
* Returns the command type discriminator.
|
|
34
37
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isCqrsRequest } from "../cqrsTypes/cqrsTypes.type.js";
|
|
1
2
|
/**
|
|
2
3
|
* Base abstract command.
|
|
3
4
|
*
|
|
@@ -18,16 +19,23 @@ export class MetadataCommand extends Command {
|
|
|
18
19
|
metadata;
|
|
19
20
|
constructor(type, options = {}) {
|
|
20
21
|
super(type);
|
|
21
|
-
this.metadata = options.metadata
|
|
22
|
+
this.metadata = options.metadata
|
|
23
|
+
? Object.freeze({
|
|
24
|
+
...options.metadata,
|
|
25
|
+
})
|
|
26
|
+
: undefined;
|
|
22
27
|
}
|
|
23
28
|
}
|
|
24
29
|
/**
|
|
25
30
|
* Creates a simple immutable command object.
|
|
31
|
+
*
|
|
32
|
+
* The `type` argument always wins over any `type` key present in the
|
|
33
|
+
* payload, so untrusted payloads cannot reroute the command.
|
|
26
34
|
*/
|
|
27
35
|
export function createCommand(type, payload) {
|
|
28
36
|
return Object.freeze({
|
|
29
|
-
type,
|
|
30
37
|
...(payload ?? {}),
|
|
38
|
+
type,
|
|
31
39
|
});
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
@@ -40,11 +48,7 @@ export function getCommandType(command) {
|
|
|
40
48
|
* Determines whether a value is a command.
|
|
41
49
|
*/
|
|
42
50
|
export function isCommand(value) {
|
|
43
|
-
return (
|
|
44
|
-
value !== null &&
|
|
45
|
-
"type" in value &&
|
|
46
|
-
typeof value.type === "string" &&
|
|
47
|
-
value.type.length > 0);
|
|
51
|
+
return isCqrsRequest(value);
|
|
48
52
|
}
|
|
49
53
|
/**
|
|
50
54
|
* Creates a command type factory.
|
|
@@ -19,8 +19,14 @@ export interface CommandRegistration<TCommand extends Command = Command, TResult
|
|
|
19
19
|
/**
|
|
20
20
|
* Command bus implementation.
|
|
21
21
|
*
|
|
22
|
-
* The command bus
|
|
23
|
-
*
|
|
22
|
+
* The command bus runs every command through the registered middleware
|
|
23
|
+
* pipeline. Request validation and handler resolution happen at the end
|
|
24
|
+
* of the pipeline, so middleware observes `InvalidCommandError` and
|
|
25
|
+
* `CommandHandlerNotFoundError` like any other failure and may substitute
|
|
26
|
+
* the command (and therefore the handler) by forwarding a different
|
|
27
|
+
* request to `next()`.
|
|
28
|
+
*
|
|
29
|
+
* All failures are `CqrsError` instances (`isCqrsError(error) === true`).
|
|
24
30
|
*/
|
|
25
31
|
export declare class CommandBus implements CommandBusContract {
|
|
26
32
|
private readonly handlers;
|
|
@@ -29,6 +35,10 @@ export declare class CommandBus implements CommandBusContract {
|
|
|
29
35
|
constructor(options?: CommandBusOptions);
|
|
30
36
|
/**
|
|
31
37
|
* Registers a command handler.
|
|
38
|
+
*
|
|
39
|
+
* @throws InvalidHandlerTypeError when the type is empty or padded with whitespace.
|
|
40
|
+
* @throws HandlerConfigurationError when the handler is not callable.
|
|
41
|
+
* @throws DuplicateHandlerError when a handler already exists for the type.
|
|
32
42
|
*/
|
|
33
43
|
register<TCommand extends Command, TResult = void>(commandType: TCommand["type"], handler: CommandHandlerLike<TCommand, TResult>): this;
|
|
34
44
|
/**
|
|
@@ -36,7 +46,7 @@ export declare class CommandBus implements CommandBusContract {
|
|
|
36
46
|
*/
|
|
37
47
|
registerMany(registrations: readonly CommandRegistration[]): this;
|
|
38
48
|
/**
|
|
39
|
-
* Replaces an existing command handler.
|
|
49
|
+
* Replaces an existing command handler (or registers a new one).
|
|
40
50
|
*/
|
|
41
51
|
replace<TCommand extends Command, TResult = void>(commandType: TCommand["type"], handler: CommandHandlerLike<TCommand, TResult>): this;
|
|
42
52
|
/**
|
|
@@ -52,7 +62,10 @@ export declare class CommandBus implements CommandBusContract {
|
|
|
52
62
|
*/
|
|
53
63
|
getHandler<TCommand extends Command, TResult = void>(commandType: TCommand["type"]): CommandHandlerLike<TCommand, TResult> | undefined;
|
|
54
64
|
/**
|
|
55
|
-
* Executes a command.
|
|
65
|
+
* Executes a command through the middleware pipeline.
|
|
66
|
+
*
|
|
67
|
+
* @throws InvalidCommandError when the command delivered to the end of the pipeline is malformed.
|
|
68
|
+
* @throws CommandHandlerNotFoundError when no handler is registered for its type.
|
|
56
69
|
*/
|
|
57
70
|
execute<TCommand extends Command, TResult = void>(command: TCommand, context?: CqrsContext): Promise<TResult>;
|
|
58
71
|
/**
|
|
@@ -72,9 +85,10 @@ export declare class CommandBus implements CommandBusContract {
|
|
|
72
85
|
*/
|
|
73
86
|
getCommandTypes(): readonly string[];
|
|
74
87
|
/**
|
|
75
|
-
*
|
|
88
|
+
* Terminal pipeline step: validates the delivered command, resolves
|
|
89
|
+
* its handler and executes it.
|
|
76
90
|
*/
|
|
77
|
-
private
|
|
91
|
+
private dispatch;
|
|
78
92
|
/**
|
|
79
93
|
* Resolves the execution context.
|
|
80
94
|
*/
|
|
@@ -1,31 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { executeCommandHandler } from "../command/commandHandler.core.js";
|
|
2
|
+
import { CommandHandlerNotFoundError, DuplicateHandlerError, InvalidCommandError, } from "../cqrsErrors/cqrsError.base.js";
|
|
3
|
+
import { composeMiddleware } from "../cqrsMiddleware/cqrsMiddleware.core.js";
|
|
4
|
+
import { assertExecutableHandler, assertHandlerType, assertMiddleware, } from "../cqrsValidation/cqrsValidation.core.js";
|
|
3
5
|
/**
|
|
4
6
|
* Command bus implementation.
|
|
5
7
|
*
|
|
6
|
-
* The command bus
|
|
7
|
-
*
|
|
8
|
+
* The command bus runs every command through the registered middleware
|
|
9
|
+
* pipeline. Request validation and handler resolution happen at the end
|
|
10
|
+
* of the pipeline, so middleware observes `InvalidCommandError` and
|
|
11
|
+
* `CommandHandlerNotFoundError` like any other failure and may substitute
|
|
12
|
+
* the command (and therefore the handler) by forwarding a different
|
|
13
|
+
* request to `next()`.
|
|
14
|
+
*
|
|
15
|
+
* All failures are `CqrsError` instances (`isCqrsError(error) === true`).
|
|
8
16
|
*/
|
|
9
17
|
export class CommandBus {
|
|
10
18
|
handlers = new Map();
|
|
11
19
|
middleware;
|
|
12
20
|
contextFactory;
|
|
13
21
|
constructor(options = {}) {
|
|
14
|
-
|
|
22
|
+
const middleware = [...(options.middleware ?? [])];
|
|
23
|
+
for (const entry of middleware) {
|
|
24
|
+
assertMiddleware("command", entry);
|
|
25
|
+
}
|
|
26
|
+
this.middleware = middleware;
|
|
15
27
|
this.contextFactory = options.contextFactory;
|
|
16
28
|
}
|
|
17
29
|
/**
|
|
18
30
|
* Registers a command handler.
|
|
31
|
+
*
|
|
32
|
+
* @throws InvalidHandlerTypeError when the type is empty or padded with whitespace.
|
|
33
|
+
* @throws HandlerConfigurationError when the handler is not callable.
|
|
34
|
+
* @throws DuplicateHandlerError when a handler already exists for the type.
|
|
19
35
|
*/
|
|
20
36
|
register(commandType, handler) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
if (!handler) {
|
|
25
|
-
throw new TypeError(`A handler is required for command "${commandType}".`);
|
|
26
|
-
}
|
|
37
|
+
assertHandlerType("command", commandType);
|
|
38
|
+
assertExecutableHandler("command", commandType, handler);
|
|
27
39
|
if (this.handlers.has(commandType)) {
|
|
28
|
-
throw new
|
|
40
|
+
throw new DuplicateHandlerError("command", commandType);
|
|
29
41
|
}
|
|
30
42
|
this.handlers.set(commandType, handler);
|
|
31
43
|
return this;
|
|
@@ -40,12 +52,11 @@ export class CommandBus {
|
|
|
40
52
|
return this;
|
|
41
53
|
}
|
|
42
54
|
/**
|
|
43
|
-
* Replaces an existing command handler.
|
|
55
|
+
* Replaces an existing command handler (or registers a new one).
|
|
44
56
|
*/
|
|
45
57
|
replace(commandType, handler) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
58
|
+
assertHandlerType("command", commandType);
|
|
59
|
+
assertExecutableHandler("command", commandType, handler);
|
|
49
60
|
this.handlers.set(commandType, handler);
|
|
50
61
|
return this;
|
|
51
62
|
}
|
|
@@ -68,35 +79,22 @@ export class CommandBus {
|
|
|
68
79
|
return this.handlers.get(commandType);
|
|
69
80
|
}
|
|
70
81
|
/**
|
|
71
|
-
* Executes a command.
|
|
82
|
+
* Executes a command through the middleware pipeline.
|
|
83
|
+
*
|
|
84
|
+
* @throws InvalidCommandError when the command delivered to the end of the pipeline is malformed.
|
|
85
|
+
* @throws CommandHandlerNotFoundError when no handler is registered for its type.
|
|
72
86
|
*/
|
|
73
87
|
async execute(command, context) {
|
|
74
|
-
this.validateCommand(command);
|
|
75
|
-
const handler = this.getHandler(command.type);
|
|
76
|
-
if (!handler) {
|
|
77
|
-
throw new BaseError(`No handler is registered for command "${command.type}".`, {
|
|
78
|
-
code: ErrorCode.COMMAND_HANDLER_NOT_FOUND,
|
|
79
|
-
category: ErrorCategory.SYSTEM,
|
|
80
|
-
severity: ErrorSeverity.ERROR,
|
|
81
|
-
statusCode: 500,
|
|
82
|
-
expose: false,
|
|
83
|
-
isOperational: true,
|
|
84
|
-
metadata: {
|
|
85
|
-
commandType: command.type,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
88
|
const executionContext = await this.resolveContext(context);
|
|
90
|
-
const pipeline = this.
|
|
91
|
-
|
|
89
|
+
const pipeline = composeMiddleware(this.middleware);
|
|
90
|
+
const result = await pipeline(command, executionContext, (request, requestContext) => this.dispatch(request, requestContext));
|
|
91
|
+
return result;
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
94
|
* Adds middleware to the end of the pipeline.
|
|
95
95
|
*/
|
|
96
96
|
use(middleware) {
|
|
97
|
-
|
|
98
|
-
throw new TypeError("Command middleware must be a function.");
|
|
99
|
-
}
|
|
97
|
+
assertMiddleware("command", middleware);
|
|
100
98
|
this.middleware.push(middleware);
|
|
101
99
|
return this;
|
|
102
100
|
}
|
|
@@ -119,16 +117,16 @@ export class CommandBus {
|
|
|
119
117
|
return [...this.handlers.keys()];
|
|
120
118
|
}
|
|
121
119
|
/**
|
|
122
|
-
*
|
|
120
|
+
* Terminal pipeline step: validates the delivered command, resolves
|
|
121
|
+
* its handler and executes it.
|
|
123
122
|
*/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
next = async (command, context) => middleware(command, context, current);
|
|
123
|
+
async dispatch(request, context) {
|
|
124
|
+
this.validateCommand(request);
|
|
125
|
+
const handler = this.handlers.get(request.type);
|
|
126
|
+
if (!handler) {
|
|
127
|
+
throw new CommandHandlerNotFoundError(request.type);
|
|
130
128
|
}
|
|
131
|
-
return
|
|
129
|
+
return executeCommandHandler(handler, request, context);
|
|
132
130
|
}
|
|
133
131
|
/**
|
|
134
132
|
* Resolves the execution context.
|
|
@@ -147,24 +145,11 @@ export class CommandBus {
|
|
|
147
145
|
*/
|
|
148
146
|
validateCommand(command) {
|
|
149
147
|
if (!command || typeof command !== "object") {
|
|
150
|
-
throw new
|
|
151
|
-
code: ErrorCode.INVALID_COMMAND,
|
|
152
|
-
category: ErrorCategory.VALIDATION,
|
|
153
|
-
severity: ErrorSeverity.WARNING,
|
|
154
|
-
statusCode: 400,
|
|
155
|
-
expose: true,
|
|
156
|
-
isOperational: true,
|
|
157
|
-
});
|
|
148
|
+
throw new InvalidCommandError("A valid command is required.");
|
|
158
149
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
category: ErrorCategory.VALIDATION,
|
|
163
|
-
severity: ErrorSeverity.WARNING,
|
|
164
|
-
statusCode: 400,
|
|
165
|
-
expose: true,
|
|
166
|
-
isOperational: true,
|
|
167
|
-
});
|
|
150
|
+
const type = command.type;
|
|
151
|
+
if (typeof type !== "string" || type.trim().length === 0) {
|
|
152
|
+
throw new InvalidCommandError("Command type is required.");
|
|
168
153
|
}
|
|
169
154
|
}
|
|
170
155
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CommandHandler as CommandHandlerContract, Command, CqrsContext } from "../cqrsTypes/cqrsTypes.type.js";
|
|
1
|
+
import type { CommandHandler as CommandHandlerContract, CommandHandlerLike, Command, CqrsContext } from "../cqrsTypes/cqrsTypes.type.js";
|
|
2
2
|
/**
|
|
3
3
|
* Abstract base class for command handlers.
|
|
4
4
|
*
|
|
@@ -34,10 +34,16 @@ export declare function createCommandHandler<TCommand extends Command, TResult =
|
|
|
34
34
|
export declare function isCommandHandler(value: unknown): value is CommandHandler;
|
|
35
35
|
/**
|
|
36
36
|
* Determines whether a value can be used as a command handler.
|
|
37
|
+
*
|
|
38
|
+
* Accepts handler functions, `CommandHandler` instances and any plain
|
|
39
|
+
* object exposing an `execute` method (the `CommandHandler` interface).
|
|
37
40
|
*/
|
|
38
|
-
export declare function isCommandHandlerLike(value: unknown): value is
|
|
41
|
+
export declare function isCommandHandlerLike(value: unknown): value is CommandHandlerLike;
|
|
39
42
|
/**
|
|
40
43
|
* Executes either an object-based or function-based command handler.
|
|
44
|
+
*
|
|
45
|
+
* Object handlers only need an `execute` method; they do not have to
|
|
46
|
+
* extend the abstract `CommandHandler` class.
|
|
41
47
|
*/
|
|
42
|
-
export declare function executeCommandHandler<TCommand extends Command, TResult = void>(handler:
|
|
48
|
+
export declare function executeCommandHandler<TCommand extends Command, TResult = void>(handler: CommandHandlerLike<TCommand, TResult>, command: TCommand, context?: CqrsContext): Promise<TResult>;
|
|
43
49
|
//# sourceMappingURL=commandHandler.core.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { HandlerConfigurationError } from "../cqrsErrors/cqrsError.base.js";
|
|
2
|
+
import { isExecutableHandler } from "../cqrsValidation/cqrsValidation.core.js";
|
|
1
3
|
/**
|
|
2
4
|
* Abstract base class for command handlers.
|
|
3
5
|
*
|
|
@@ -15,7 +17,10 @@ export class FunctionCommandHandler extends CommandHandler {
|
|
|
15
17
|
constructor(commandType, handler) {
|
|
16
18
|
super();
|
|
17
19
|
if (typeof handler !== "function") {
|
|
18
|
-
throw new
|
|
20
|
+
throw new HandlerConfigurationError("Command handler must be a function.", {
|
|
21
|
+
handlerKind: "command",
|
|
22
|
+
handlerType: commandType,
|
|
23
|
+
});
|
|
19
24
|
}
|
|
20
25
|
this.commandType = commandType;
|
|
21
26
|
this.handler = handler;
|
|
@@ -38,20 +43,32 @@ export function isCommandHandler(value) {
|
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
40
45
|
* Determines whether a value can be used as a command handler.
|
|
46
|
+
*
|
|
47
|
+
* Accepts handler functions, `CommandHandler` instances and any plain
|
|
48
|
+
* object exposing an `execute` method (the `CommandHandler` interface).
|
|
41
49
|
*/
|
|
42
50
|
export function isCommandHandlerLike(value) {
|
|
43
|
-
return value
|
|
51
|
+
return isExecutableHandler(value);
|
|
44
52
|
}
|
|
45
53
|
/**
|
|
46
54
|
* Executes either an object-based or function-based command handler.
|
|
55
|
+
*
|
|
56
|
+
* Object handlers only need an `execute` method; they do not have to
|
|
57
|
+
* extend the abstract `CommandHandler` class.
|
|
47
58
|
*/
|
|
48
59
|
export async function executeCommandHandler(handler, command, context) {
|
|
49
|
-
if (handler instanceof CommandHandler) {
|
|
50
|
-
return await handler.execute(command, context);
|
|
51
|
-
}
|
|
52
60
|
if (typeof handler === "function") {
|
|
53
61
|
return await handler(command, context);
|
|
54
62
|
}
|
|
55
|
-
|
|
63
|
+
if (typeof handler === "object" &&
|
|
64
|
+
handler !== null &&
|
|
65
|
+
typeof handler
|
|
66
|
+
.execute === "function") {
|
|
67
|
+
return await handler.execute(command, context);
|
|
68
|
+
}
|
|
69
|
+
throw new HandlerConfigurationError(`Invalid command handler for "${command.type}": expected a function or an object with an execute() method.`, {
|
|
70
|
+
handlerKind: "command",
|
|
71
|
+
handlerType: command.type,
|
|
72
|
+
});
|
|
56
73
|
}
|
|
57
74
|
//# sourceMappingURL=commandHandler.core.js.map
|
|
@@ -32,10 +32,21 @@ export interface CreateExecutionContextInput {
|
|
|
32
32
|
export declare function createRequestId(): string;
|
|
33
33
|
/**
|
|
34
34
|
* Creates an immutable CQRS execution context.
|
|
35
|
+
*
|
|
36
|
+
* Every context belongs to a correlation chain: when no `correlationId`
|
|
37
|
+
* is supplied the context starts a new chain using its own `requestId`,
|
|
38
|
+
* so children created with `createChildExecutionContext` always share a
|
|
39
|
+
* correlation with their root.
|
|
35
40
|
*/
|
|
36
41
|
export declare function createExecutionContext(input?: CreateExecutionContextInput): CqrsExecutionContext;
|
|
37
42
|
/**
|
|
38
43
|
* Creates a child execution context while preserving correlation data.
|
|
44
|
+
*
|
|
45
|
+
* The child inherits the parent's correlation id; a parent without one
|
|
46
|
+
* (for example a hand-built object) contributes its `requestId` instead,
|
|
47
|
+
* and a fresh id is generated as a last resort, so `sharesCorrelation`
|
|
48
|
+
* holds between every parent and child. `metadata` is only set when the
|
|
49
|
+
* parent or the overrides supplied some.
|
|
39
50
|
*/
|
|
40
51
|
export declare function createChildExecutionContext(parent: CqrsExecutionContext, overrides?: CreateExecutionContextInput): CqrsExecutionContext;
|
|
41
52
|
/**
|
|
@@ -11,11 +11,17 @@ export function createRequestId() {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Creates an immutable CQRS execution context.
|
|
14
|
+
*
|
|
15
|
+
* Every context belongs to a correlation chain: when no `correlationId`
|
|
16
|
+
* is supplied the context starts a new chain using its own `requestId`,
|
|
17
|
+
* so children created with `createChildExecutionContext` always share a
|
|
18
|
+
* correlation with their root.
|
|
14
19
|
*/
|
|
15
20
|
export function createExecutionContext(input = {}) {
|
|
21
|
+
const requestId = input.requestId ?? createRequestId();
|
|
16
22
|
const context = {
|
|
17
|
-
requestId
|
|
18
|
-
correlationId: input.correlationId,
|
|
23
|
+
requestId,
|
|
24
|
+
correlationId: input.correlationId ?? requestId,
|
|
19
25
|
causationId: input.causationId,
|
|
20
26
|
userId: input.userId,
|
|
21
27
|
tenantId: input.tenantId,
|
|
@@ -30,19 +36,31 @@ export function createExecutionContext(input = {}) {
|
|
|
30
36
|
}
|
|
31
37
|
/**
|
|
32
38
|
* Creates a child execution context while preserving correlation data.
|
|
39
|
+
*
|
|
40
|
+
* The child inherits the parent's correlation id; a parent without one
|
|
41
|
+
* (for example a hand-built object) contributes its `requestId` instead,
|
|
42
|
+
* and a fresh id is generated as a last resort, so `sharesCorrelation`
|
|
43
|
+
* holds between every parent and child. `metadata` is only set when the
|
|
44
|
+
* parent or the overrides supplied some.
|
|
33
45
|
*/
|
|
34
46
|
export function createChildExecutionContext(parent, overrides = {}) {
|
|
47
|
+
const hasMetadata = parent.metadata !== undefined || overrides.metadata !== undefined;
|
|
35
48
|
return createExecutionContext({
|
|
36
49
|
requestId: overrides.requestId,
|
|
37
|
-
correlationId: overrides.correlationId ??
|
|
50
|
+
correlationId: overrides.correlationId ??
|
|
51
|
+
parent.correlationId ??
|
|
52
|
+
parent.requestId ??
|
|
53
|
+
createRequestId(),
|
|
38
54
|
causationId: overrides.causationId ?? parent.requestId ?? parent.causationId,
|
|
39
55
|
userId: overrides.userId ?? parent.userId,
|
|
40
56
|
tenantId: overrides.tenantId ?? parent.tenantId,
|
|
41
57
|
source: overrides.source ?? parent.source,
|
|
42
|
-
metadata:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
metadata: hasMetadata
|
|
59
|
+
? {
|
|
60
|
+
...(parent.metadata ?? {}),
|
|
61
|
+
...(overrides.metadata ?? {}),
|
|
62
|
+
}
|
|
63
|
+
: undefined,
|
|
46
64
|
});
|
|
47
65
|
}
|
|
48
66
|
/**
|
|
@@ -61,13 +61,19 @@ export type DecoratedCqrsClass<TInstance = object> = CqrsClass<TInstance> & {
|
|
|
61
61
|
*/
|
|
62
62
|
export declare function CqrsHandler<TType extends string>(kind: CqrsHandlerKind, type: TType): ClassDecorator;
|
|
63
63
|
/**
|
|
64
|
-
* Marks a class as
|
|
64
|
+
* Marks a class as the command handler for `type`.
|
|
65
|
+
*
|
|
66
|
+
* Named `CommandHandlerFor` so it does not collide with the abstract
|
|
67
|
+
* `CommandHandler` class exported from the same package.
|
|
65
68
|
*/
|
|
66
|
-
export declare function
|
|
69
|
+
export declare function CommandHandlerFor(type: string): ClassDecorator;
|
|
67
70
|
/**
|
|
68
|
-
* Marks a class as
|
|
71
|
+
* Marks a class as the query handler for `type`.
|
|
72
|
+
*
|
|
73
|
+
* Named `QueryHandlerFor` so it does not collide with the abstract
|
|
74
|
+
* `QueryHandler` class exported from the same package.
|
|
69
75
|
*/
|
|
70
|
-
export declare function
|
|
76
|
+
export declare function QueryHandlerFor(type: string): ClassDecorator;
|
|
71
77
|
/**
|
|
72
78
|
* Reads generic CQRS handler metadata from a class.
|
|
73
79
|
*/
|
|
@@ -89,13 +95,13 @@ export declare function getCqrsType(target: unknown): string | undefined;
|
|
|
89
95
|
*/
|
|
90
96
|
export declare function isCqrsHandler(target: unknown): boolean;
|
|
91
97
|
/**
|
|
92
|
-
* Determines whether a class is decorated
|
|
98
|
+
* Determines whether a class is decorated with `CommandHandlerFor`.
|
|
93
99
|
*/
|
|
94
|
-
export declare function
|
|
100
|
+
export declare function isDecoratedCommandHandler(target: unknown): boolean;
|
|
95
101
|
/**
|
|
96
|
-
* Determines whether a class is decorated
|
|
102
|
+
* Determines whether a class is decorated with `QueryHandlerFor`.
|
|
97
103
|
*/
|
|
98
|
-
export declare function
|
|
104
|
+
export declare function isDecoratedQueryHandler(target: unknown): boolean;
|
|
99
105
|
/**
|
|
100
106
|
* Creates a reusable command handler decorator.
|
|
101
107
|
*/
|