@zudojs/cqrs 0.1.0 → 1.0.1

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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -7
  3. package/dist/command/command.core.d.ts +5 -2
  4. package/dist/command/command.core.js +11 -7
  5. package/dist/command/commandBus.core.d.ts +20 -6
  6. package/dist/command/commandBus.core.js +47 -62
  7. package/dist/command/commandHandler.core.d.ts +9 -3
  8. package/dist/command/commandHandler.core.js +23 -6
  9. package/dist/cqrsContext/cqrsContext.core.d.ts +11 -0
  10. package/dist/cqrsContext/cqrsContext.core.js +25 -7
  11. package/dist/cqrsDecorators/cqrsDecorators.core.d.ts +14 -8
  12. package/dist/cqrsDecorators/cqrsDecorators.core.js +79 -60
  13. package/dist/cqrsErrors/cqrsError.base.d.ts +34 -2
  14. package/dist/cqrsErrors/cqrsError.base.js +58 -5
  15. package/dist/cqrsEvents/cqrsEvents.type.d.ts +13 -6
  16. package/dist/cqrsEvents/cqrsEvents.type.js +11 -1
  17. package/dist/cqrsMiddleware/cqrsMiddleware.core.d.ts +101 -8
  18. package/dist/cqrsMiddleware/cqrsMiddleware.core.js +123 -32
  19. package/dist/cqrsMiddleware/index.d.ts +2 -1
  20. package/dist/cqrsMiddleware/index.js +2 -1
  21. package/dist/cqrsTypes/cqrsTypes.type.d.ts +17 -30
  22. package/dist/cqrsTypes/cqrsTypes.type.js +0 -16
  23. package/dist/cqrsValidation/cqrsValidation.core.d.ts +32 -0
  24. package/dist/cqrsValidation/cqrsValidation.core.js +53 -0
  25. package/dist/cqrsValidation/index.d.ts +7 -0
  26. package/dist/cqrsValidation/index.js +7 -0
  27. package/dist/handlerRegistry/handlerRegistry.core.d.ts +5 -0
  28. package/dist/handlerRegistry/handlerRegistry.core.js +28 -10
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +6 -2
  31. package/dist/query/query.core.d.ts +5 -2
  32. package/dist/query/query.core.js +11 -7
  33. package/dist/query/queryBus.core.d.ts +20 -6
  34. package/dist/query/queryBus.core.js +47 -62
  35. package/dist/query/queryHandler.core.d.ts +9 -3
  36. package/dist/query/queryHandler.core.js +23 -6
  37. package/package.json +26 -14
  38. package/dist/command/command.core.d.ts.map +0 -1
  39. package/dist/command/command.core.js.map +0 -1
  40. package/dist/command/commandBus.core.d.ts.map +0 -1
  41. package/dist/command/commandBus.core.js.map +0 -1
  42. package/dist/command/commandHandler.core.d.ts.map +0 -1
  43. package/dist/command/commandHandler.core.js.map +0 -1
  44. package/dist/command/commandResult.type.d.ts.map +0 -1
  45. package/dist/command/commandResult.type.js.map +0 -1
  46. package/dist/command/index.d.ts.map +0 -1
  47. package/dist/command/index.js.map +0 -1
  48. package/dist/cqrsContext/cqrsContext.core.d.ts.map +0 -1
  49. package/dist/cqrsContext/cqrsContext.core.js.map +0 -1
  50. package/dist/cqrsContext/index.d.ts.map +0 -1
  51. package/dist/cqrsContext/index.js.map +0 -1
  52. package/dist/cqrsDecorators/cqrsDecorators.core.d.ts.map +0 -1
  53. package/dist/cqrsDecorators/cqrsDecorators.core.js.map +0 -1
  54. package/dist/cqrsDecorators/index.d.ts.map +0 -1
  55. package/dist/cqrsDecorators/index.js.map +0 -1
  56. package/dist/cqrsErrors/cqrsError.base.d.ts.map +0 -1
  57. package/dist/cqrsErrors/cqrsError.base.js.map +0 -1
  58. package/dist/cqrsErrors/index.d.ts.map +0 -1
  59. package/dist/cqrsErrors/index.js.map +0 -1
  60. package/dist/cqrsEvents/cqrsEventBus.core.d.ts.map +0 -1
  61. package/dist/cqrsEvents/cqrsEventBus.core.js.map +0 -1
  62. package/dist/cqrsEvents/cqrsEventResult.type.d.ts.map +0 -1
  63. package/dist/cqrsEvents/cqrsEventResult.type.js.map +0 -1
  64. package/dist/cqrsEvents/cqrsEvents.type.d.ts.map +0 -1
  65. package/dist/cqrsEvents/cqrsEvents.type.js.map +0 -1
  66. package/dist/cqrsEvents/index.d.ts.map +0 -1
  67. package/dist/cqrsEvents/index.js.map +0 -1
  68. package/dist/cqrsMiddleware/cqrsMiddleware.core.d.ts.map +0 -1
  69. package/dist/cqrsMiddleware/cqrsMiddleware.core.js.map +0 -1
  70. package/dist/cqrsMiddleware/index.d.ts.map +0 -1
  71. package/dist/cqrsMiddleware/index.js.map +0 -1
  72. package/dist/cqrsTypes/cqrsTypes.type.d.ts.map +0 -1
  73. package/dist/cqrsTypes/cqrsTypes.type.js.map +0 -1
  74. package/dist/cqrsTypes/index.d.ts.map +0 -1
  75. package/dist/cqrsTypes/index.js.map +0 -1
  76. package/dist/handlerRegistry/handlerRegistry.core.d.ts.map +0 -1
  77. package/dist/handlerRegistry/handlerRegistry.core.js.map +0 -1
  78. package/dist/handlerRegistry/index.d.ts.map +0 -1
  79. package/dist/handlerRegistry/index.js.map +0 -1
  80. package/dist/index.d.ts.map +0 -1
  81. package/dist/index.js.map +0 -1
  82. package/dist/query/index.d.ts.map +0 -1
  83. package/dist/query/index.js.map +0 -1
  84. package/dist/query/query.core.d.ts.map +0 -1
  85. package/dist/query/query.core.js.map +0 -1
  86. package/dist/query/queryBus.core.d.ts.map +0 -1
  87. package/dist/query/queryBus.core.js.map +0 -1
  88. package/dist/query/queryHandler.core.d.ts.map +0 -1
  89. package/dist/query/queryHandler.core.js.map +0 -1
  90. package/dist/query/queryResult.type.d.ts.map +0 -1
  91. 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 { createCommandBus, createQueryBus } from "@zudojs/cqrs";
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
- commandBus.register("CreateUser", async (command) => {
20
- return userRepository.create(command.data);
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.register("GetUser", async (query) => {
24
- return userRepository.findById(query.id);
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
- - Command and query handlers
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<string, never>>(type: TType, payload?: TPayload): Readonly<{
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 (typeof value === "object" &&
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 resolves a handler by command type and executes it
23
- * through the registered middleware pipeline.
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
- * Builds the command execution pipeline.
88
+ * Terminal pipeline step: validates the delivered command, resolves
89
+ * its handler and executes it.
76
90
  */
77
- private buildPipeline;
91
+ private dispatch;
78
92
  /**
79
93
  * Resolves the execution context.
80
94
  */
@@ -1,31 +1,43 @@
1
- import { BaseError, ErrorCategory, ErrorCode, ErrorSeverity, } from "@zudojs/errors";
2
- import { CommandHandler, executeCommandHandler, } from "../command/commandHandler.core.js";
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 resolves a handler by command type and executes it
7
- * through the registered middleware pipeline.
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
- this.middleware = [...(options.middleware ?? [])];
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
- if (!commandType.trim()) {
22
- throw new TypeError("Command type cannot be empty.");
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 Error(`A handler is already registered for command "${commandType}".`);
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
- if (!commandType.trim()) {
47
- throw new TypeError("Command type cannot be empty.");
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.buildPipeline(handler);
91
- return pipeline(command, executionContext);
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
- if (typeof middleware !== "function") {
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
- * Builds the command execution pipeline.
120
+ * Terminal pipeline step: validates the delivered command, resolves
121
+ * its handler and executes it.
123
122
  */
124
- buildPipeline(handler) {
125
- let next = async (command, context) => executeCommandHandler(handler, command, context);
126
- for (let index = this.middleware.length - 1; index >= 0; index--) {
127
- const middleware = this.middleware[index];
128
- const current = next;
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 next;
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 BaseError("A valid command is required.", {
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
- if (typeof command.type !== "string" || command.type.trim().length === 0) {
160
- throw new BaseError("Command type is required.", {
161
- code: ErrorCode.INVALID_COMMAND,
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 CommandHandler | ((command: Command, context?: CqrsContext) => unknown);
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: CommandHandler<TCommand, TResult> | ((command: TCommand, context?: CqrsContext) => TResult | Promise<TResult>), command: TCommand, context?: CqrsContext): Promise<TResult>;
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 TypeError("Command handler must be a function.");
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 instanceof CommandHandler || typeof value === "function";
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
- throw new TypeError(`Invalid command handler for "${command.type}".`);
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: input.requestId ?? createRequestId(),
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 ?? parent.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
- ...(parent.metadata ?? {}),
44
- ...(overrides.metadata ?? {}),
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 a command handler.
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 CommandHandler(type: string): ClassDecorator;
69
+ export declare function CommandHandlerFor(type: string): ClassDecorator;
67
70
  /**
68
- * Marks a class as a query handler.
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 QueryHandler(type: string): ClassDecorator;
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 as a command handler.
98
+ * Determines whether a class is decorated with `CommandHandlerFor`.
93
99
  */
94
- export declare function isCommandHandler(target: unknown): boolean;
100
+ export declare function isDecoratedCommandHandler(target: unknown): boolean;
95
101
  /**
96
- * Determines whether a class is decorated as a query handler.
102
+ * Determines whether a class is decorated with `QueryHandlerFor`.
97
103
  */
98
- export declare function isQueryHandler(target: unknown): boolean;
104
+ export declare function isDecoratedQueryHandler(target: unknown): boolean;
99
105
  /**
100
106
  * Creates a reusable command handler decorator.
101
107
  */