@nestjs/cqrs 11.0.3 → 12.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.
Files changed (149) hide show
  1. package/.circleci/config.yml +3 -3
  2. package/.nvmrc +1 -0
  3. package/dist/aggregate-root.d.ts +3 -67
  4. package/dist/aggregate-root.js +6 -83
  5. package/dist/classes/command.d.ts +2 -2
  6. package/dist/classes/command.js +2 -6
  7. package/dist/classes/constants.js +1 -4
  8. package/dist/classes/index.d.ts +2 -2
  9. package/dist/classes/index.js +2 -18
  10. package/dist/classes/query.d.ts +2 -2
  11. package/dist/classes/query.js +2 -6
  12. package/dist/command-bus.d.ts +5 -5
  13. package/dist/command-bus.js +29 -32
  14. package/dist/constants.js +1 -4
  15. package/dist/cqrs.module.d.ts +5 -5
  16. package/dist/cqrs.module.js +34 -37
  17. package/dist/decorators/command-handler.decorator.d.ts +1 -1
  18. package/dist/decorators/command-handler.decorator.js +9 -13
  19. package/dist/decorators/constants.js +7 -10
  20. package/dist/decorators/events-handler.decorator.d.ts +1 -1
  21. package/dist/decorators/events-handler.decorator.js +9 -13
  22. package/dist/decorators/index.d.ts +5 -5
  23. package/dist/decorators/index.js +5 -21
  24. package/dist/decorators/publishable.decorator.js +3 -6
  25. package/dist/decorators/query-handler.decorator.d.ts +1 -1
  26. package/dist/decorators/query-handler.decorator.js +9 -13
  27. package/dist/decorators/saga.decorator.js +5 -9
  28. package/dist/event-bus.d.ts +10 -8
  29. package/dist/event-bus.js +52 -52
  30. package/dist/event-publisher.d.ts +8 -7
  31. package/dist/event-publisher.js +17 -18
  32. package/dist/exceptions/command-not-found.exception.js +1 -5
  33. package/dist/exceptions/index.d.ts +7 -7
  34. package/dist/exceptions/index.js +7 -23
  35. package/dist/exceptions/invalid-command-handler.exception.js +1 -5
  36. package/dist/exceptions/invalid-events-handler.exception.js +1 -5
  37. package/dist/exceptions/invalid-query-handler.exception.js +1 -5
  38. package/dist/exceptions/invalid-saga.exception.js +1 -5
  39. package/dist/exceptions/query-not-found.exception.js +1 -5
  40. package/dist/exceptions/unsupported-saga-scope.exception.js +1 -5
  41. package/dist/helpers/default-command-pubsub.d.ts +1 -1
  42. package/dist/helpers/default-command-pubsub.js +1 -5
  43. package/dist/helpers/default-event-id-provider.d.ts +1 -1
  44. package/dist/helpers/default-event-id-provider.js +3 -6
  45. package/dist/helpers/default-pubsub.d.ts +1 -1
  46. package/dist/helpers/default-pubsub.js +1 -5
  47. package/dist/helpers/default-query-pubsub.d.ts +1 -1
  48. package/dist/helpers/default-query-pubsub.js +1 -5
  49. package/dist/helpers/default-unhandled-exception-pubsub.d.ts +1 -1
  50. package/dist/helpers/default-unhandled-exception-pubsub.js +1 -5
  51. package/dist/index.d.ts +15 -14
  52. package/dist/index.js +15 -30
  53. package/dist/interfaces/aggregate-root.interface.d.ts +63 -0
  54. package/dist/interfaces/aggregate-root.interface.js +1 -0
  55. package/dist/interfaces/commands/command-bus.interface.d.ts +3 -3
  56. package/dist/interfaces/commands/command-bus.interface.js +1 -2
  57. package/dist/interfaces/commands/command-handler.interface.d.ts +2 -2
  58. package/dist/interfaces/commands/command-handler.interface.js +1 -2
  59. package/dist/interfaces/commands/command-metadata.interface.js +1 -2
  60. package/dist/interfaces/commands/command-publisher.interface.d.ts +1 -1
  61. package/dist/interfaces/commands/command-publisher.interface.js +1 -2
  62. package/dist/interfaces/commands/command.interface.js +1 -2
  63. package/dist/interfaces/cqrs-module-options.interface.d.ts +5 -5
  64. package/dist/interfaces/cqrs-module-options.interface.js +1 -2
  65. package/dist/interfaces/events/event-bus.interface.d.ts +2 -2
  66. package/dist/interfaces/events/event-bus.interface.js +1 -2
  67. package/dist/interfaces/events/event-handler.interface.d.ts +1 -1
  68. package/dist/interfaces/events/event-handler.interface.js +1 -2
  69. package/dist/interfaces/events/event-id-provider.interface.d.ts +1 -1
  70. package/dist/interfaces/events/event-id-provider.interface.js +1 -2
  71. package/dist/interfaces/events/event-publisher.interface.d.ts +2 -2
  72. package/dist/interfaces/events/event-publisher.interface.js +1 -2
  73. package/dist/interfaces/events/event.interface.js +1 -2
  74. package/dist/interfaces/events/message-source.interface.d.ts +1 -1
  75. package/dist/interfaces/events/message-source.interface.js +1 -2
  76. package/dist/interfaces/exceptions/unhandled-exception-info.interface.d.ts +2 -2
  77. package/dist/interfaces/exceptions/unhandled-exception-info.interface.js +1 -2
  78. package/dist/interfaces/exceptions/unhandled-exception-publisher.interface.d.ts +3 -3
  79. package/dist/interfaces/exceptions/unhandled-exception-publisher.interface.js +1 -2
  80. package/dist/interfaces/index.d.ts +20 -19
  81. package/dist/interfaces/index.js +20 -35
  82. package/dist/interfaces/providers-introspection-result.interface.d.ts +4 -4
  83. package/dist/interfaces/providers-introspection-result.interface.js +1 -2
  84. package/dist/interfaces/queries/query-bus.interface.d.ts +3 -3
  85. package/dist/interfaces/queries/query-bus.interface.js +1 -2
  86. package/dist/interfaces/queries/query-handler.interface.d.ts +2 -2
  87. package/dist/interfaces/queries/query-handler.interface.js +1 -2
  88. package/dist/interfaces/queries/query-metadata.interface.js +1 -2
  89. package/dist/interfaces/queries/query-publisher.interface.d.ts +1 -1
  90. package/dist/interfaces/queries/query-publisher.interface.js +1 -2
  91. package/dist/interfaces/queries/query-result.interface.js +1 -2
  92. package/dist/interfaces/queries/query.interface.js +1 -2
  93. package/dist/interfaces/saga.type.d.ts +2 -2
  94. package/dist/interfaces/saga.type.js +1 -2
  95. package/dist/mixins/index.d.ts +1 -0
  96. package/dist/mixins/index.js +1 -0
  97. package/dist/mixins/with-aggregate-root.mixin.d.ts +11 -0
  98. package/dist/mixins/with-aggregate-root.mixin.js +75 -0
  99. package/dist/operators/index.d.ts +1 -1
  100. package/dist/operators/index.js +1 -17
  101. package/dist/operators/of-type.d.ts +1 -1
  102. package/dist/operators/of-type.js +3 -6
  103. package/dist/query-bus.d.ts +5 -5
  104. package/dist/query-bus.js +28 -31
  105. package/dist/scopes/async.context.js +9 -13
  106. package/dist/scopes/index.d.ts +1 -1
  107. package/dist/scopes/index.js +1 -17
  108. package/dist/services/explorer.service.d.ts +5 -5
  109. package/dist/services/explorer.service.js +14 -20
  110. package/dist/storages/aggregate-root.storage.d.ts +3 -3
  111. package/dist/storages/aggregate-root.storage.js +9 -9
  112. package/dist/unhandled-exception-bus.d.ts +2 -2
  113. package/dist/unhandled-exception-bus.js +14 -17
  114. package/dist/utils/index.d.ts +1 -1
  115. package/dist/utils/index.js +1 -17
  116. package/dist/utils/observable-bus.js +3 -7
  117. package/package.json +34 -26
  118. package/test/e2e/aggregate-commit-context.spec.ts +243 -0
  119. package/test/e2e/basic-flows.spec.ts +46 -26
  120. package/test/e2e/generics.spec.ts +3 -3
  121. package/test/e2e/request-scope.spec.ts +23 -22
  122. package/test/e2e/sync-event-handlers-app.spec.ts +123 -0
  123. package/test/e2e/unhandled-exceptions.spec.ts +55 -20
  124. package/tsconfig.build.json +3 -2
  125. package/vitest.config.e2e.ts +13 -0
  126. package/vitest.config.ts +8 -0
  127. package/dist/exceptions/invalid-module-ref.exception.d.ts +0 -3
  128. package/dist/exceptions/invalid-module-ref.exception.js +0 -8
  129. package/dist/helpers/default-get-event-id.d.ts +0 -9
  130. package/dist/helpers/default-get-event-id.js +0 -18
  131. package/dist/helpers/default-get-event-name.d.ts +0 -2
  132. package/dist/helpers/default-get-event-name.js +0 -8
  133. package/dist/interfaces/cqrs-options.interface.d.ts +0 -10
  134. package/dist/interfaces/cqrs-options.interface.js +0 -2
  135. package/dist/interfaces/event-observable.interface.d.ts +0 -4
  136. package/dist/interfaces/event-observable.interface.js +0 -2
  137. package/dist/utils/command-handler.decorator.d.ts +0 -3
  138. package/dist/utils/command-handler.decorator.js +0 -9
  139. package/dist/utils/constants.d.ts +0 -2
  140. package/dist/utils/constants.js +0 -4
  141. package/dist/utils/default-pubsub.d.ts +0 -9
  142. package/dist/utils/default-pubsub.js +0 -14
  143. package/dist/utils/events-handler.decorator.d.ts +0 -3
  144. package/dist/utils/events-handler.decorator.js +0 -9
  145. package/eslint.config.mjs +0 -42
  146. package/index.d.ts +0 -1
  147. package/index.js +0 -6
  148. package/jest.config.js +0 -9
  149. package/test/jest-e2e.json +0 -9
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
2
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
3
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -11,19 +10,17 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
10
  var __param = (this && this.__param) || function (paramIndex, decorator) {
12
11
  return function (target, key) { decorator(target, key, paramIndex); }
13
12
  };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.UnhandledExceptionBus = void 0;
16
- const common_1 = require("@nestjs/common");
17
- require("reflect-metadata");
18
- const rxjs_1 = require("rxjs");
19
- const constants_1 = require("./constants");
20
- const default_unhandled_exception_pubsub_1 = require("./helpers/default-unhandled-exception-pubsub");
21
- const observable_bus_1 = require("./utils/observable-bus");
13
+ import { Inject, Injectable, Optional } from '@nestjs/common';
14
+ import 'reflect-metadata';
15
+ import { filter } from 'rxjs';
16
+ import { CQRS_MODULE_OPTIONS } from './constants.js';
17
+ import { DefaultUnhandledExceptionPubSub } from './helpers/default-unhandled-exception-pubsub.js';
18
+ import { ObservableBus } from './utils/observable-bus.js';
22
19
  /**
23
20
  * A bus that publishes unhandled exceptions.
24
21
  * @template Cause The type of the cause of the exception.
25
22
  */
26
- let UnhandledExceptionBus = class UnhandledExceptionBus extends observable_bus_1.ObservableBus {
23
+ let UnhandledExceptionBus = class UnhandledExceptionBus extends ObservableBus {
27
24
  constructor(options) {
28
25
  super();
29
26
  this.options = options;
@@ -44,7 +41,7 @@ let UnhandledExceptionBus = class UnhandledExceptionBus extends observable_bus_1
44
41
  */
45
42
  static ofType(...types) {
46
43
  const isInstanceOf = (exceptionInfo) => !!types.find((classType) => exceptionInfo.exception instanceof classType);
47
- return (source) => source.pipe((0, rxjs_1.filter)(isInstanceOf));
44
+ return (source) => source.pipe(filter(isInstanceOf));
48
45
  }
49
46
  /**
50
47
  * Gets the publisher of the bus.
@@ -66,13 +63,13 @@ let UnhandledExceptionBus = class UnhandledExceptionBus extends observable_bus_1
66
63
  return this._publisher.publish(info);
67
64
  }
68
65
  useDefaultPublisher() {
69
- this._publisher = new default_unhandled_exception_pubsub_1.DefaultUnhandledExceptionPubSub(this.subject$);
66
+ this._publisher = new DefaultUnhandledExceptionPubSub(this.subject$);
70
67
  }
71
68
  };
72
- exports.UnhandledExceptionBus = UnhandledExceptionBus;
73
- exports.UnhandledExceptionBus = UnhandledExceptionBus = __decorate([
74
- (0, common_1.Injectable)(),
75
- __param(0, (0, common_1.Optional)()),
76
- __param(0, (0, common_1.Inject)(constants_1.CQRS_MODULE_OPTIONS)),
69
+ UnhandledExceptionBus = __decorate([
70
+ Injectable(),
71
+ __param(0, Optional()),
72
+ __param(0, Inject(CQRS_MODULE_OPTIONS)),
77
73
  __metadata("design:paramtypes", [Object])
78
74
  ], UnhandledExceptionBus);
75
+ export { UnhandledExceptionBus };
@@ -1 +1 @@
1
- export * from './observable-bus';
1
+ export * from './observable-bus.js';
@@ -1,17 +1 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./observable-bus"), exports);
1
+ export * from './observable-bus.js';
@@ -1,15 +1,11 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ObservableBus = void 0;
4
- const rxjs_1 = require("rxjs");
5
- class ObservableBus extends rxjs_1.Observable {
1
+ import { Observable, Subject } from 'rxjs';
2
+ export class ObservableBus extends Observable {
6
3
  constructor() {
7
4
  super();
8
- this._subject$ = new rxjs_1.Subject();
5
+ this._subject$ = new Subject();
9
6
  this.source = this._subject$;
10
7
  }
11
8
  get subject$() {
12
9
  return this._subject$;
13
10
  }
14
11
  }
15
- exports.ObservableBus = ObservableBus;
package/package.json CHANGED
@@ -1,14 +1,29 @@
1
1
  {
2
2
  "name": "@nestjs/cqrs",
3
- "version": "11.0.3",
3
+ "version": "12.1.0",
4
4
  "description": "A lightweight CQRS module for Nest framework (node.js)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "engines": {
18
+ "node": "^20.19.0 || >=22.12.0"
19
+ },
5
20
  "license": "MIT",
6
21
  "url": "https://github.com/nestjs/cqrs#readme",
7
22
  "scripts": {
8
- "test": "jest",
23
+ "test": "vitest run",
9
24
  "build": "tsc -p tsconfig.build.json",
10
25
  "format": "prettier --write \"**/*.ts\"",
11
- "lint": "eslint \"src/**/*.ts\" --fix",
26
+ "lint": "oxlint src/",
12
27
  "prepublish:next": "npm run build",
13
28
  "publish:next": "npm publish --access public --tag next",
14
29
  "prepublish:npm": "npm run build",
@@ -16,36 +31,29 @@
16
31
  "prerelease": "npm run build",
17
32
  "release": "release-it",
18
33
  "prepare": "husky",
19
- "test:e2e": "jest --config ./test/jest-e2e.json --runInBand"
34
+ "test:e2e": "vitest run --config vitest.config.e2e.ts"
20
35
  },
21
36
  "devDependencies": {
22
- "@commitlint/cli": "19.8.0",
23
- "@commitlint/config-angular": "19.8.0",
24
- "@eslint/eslintrc": "3.3.1",
25
- "@eslint/js": "9.23.0",
26
- "@nestjs/common": "11.0.12",
27
- "@nestjs/core": "11.0.12",
28
- "@nestjs/testing": "11.0.12",
29
- "@types/jest": "29.5.14",
30
- "@types/node": "22.13.11",
31
- "eslint": "9.23.0",
32
- "eslint-config-prettier": "10.1.1",
33
- "eslint-plugin-prettier": "5.2.4",
34
- "globals": "16.0.0",
37
+ "@commitlint/cli": "21.2.3",
38
+ "@commitlint/config-angular": "21.2.3",
39
+ "@nestjs/common": "12.1.1",
40
+ "@nestjs/core": "12.1.1",
41
+ "@nestjs/testing": "12.1.1",
42
+ "@types/node": "24.19.0",
35
43
  "husky": "9.1.7",
36
- "jest": "29.7.0",
37
- "lint-staged": "15.5.0",
38
- "prettier": "3.5.3",
44
+ "lint-staged": "17.6.0",
45
+ "oxlint": "1.86.0",
46
+ "prettier": "3.9.9",
39
47
  "reflect-metadata": "0.2.2",
40
- "release-it": "18.1.2",
48
+ "release-it": "21.1.0",
41
49
  "rxjs": "7.8.2",
42
- "ts-jest": "29.2.6",
43
- "typescript": "5.8.2",
44
- "typescript-eslint": "8.27.0"
50
+ "typescript": "7.0.2",
51
+ "vite": "8.3.1",
52
+ "vitest": "4.1.11"
45
53
  },
46
54
  "peerDependencies": {
47
- "@nestjs/common": "^10.0.0 || ^11.0.0",
48
- "@nestjs/core": "^10.0.0 || ^11.0.0",
55
+ "@nestjs/common": "^12.0.0",
56
+ "@nestjs/core": "^12.0.0",
49
57
  "reflect-metadata": "^0.1.13 || ^0.2.0",
50
58
  "rxjs": "^7.2.0"
51
59
  },
@@ -0,0 +1,243 @@
1
+ import { Inject, Injectable, Module, Scope } from '@nestjs/common';
2
+ import { REQUEST } from '@nestjs/core';
3
+ import { Test, TestingModule } from '@nestjs/testing';
4
+ import { setImmediate } from 'timers/promises';
5
+ import {
6
+ AggregateRoot,
7
+ AsyncContext,
8
+ CommandBus,
9
+ CommandHandler,
10
+ CqrsModule,
11
+ EventBus,
12
+ EventPublisher,
13
+ EventsHandler,
14
+ ICommandHandler,
15
+ IEvent,
16
+ IEventHandler,
17
+ IEventPublisher,
18
+ Publishable,
19
+ } from '../../src/index.js';
20
+ import { waitImmediate } from '../utils/wait-immediate.js';
21
+
22
+ interface Transaction {
23
+ id: string;
24
+ fail?: Error;
25
+ }
26
+
27
+ class OrderPlacedEvent implements IEvent {
28
+ constructor(public readonly orderId: string) {}
29
+ }
30
+
31
+ class Order extends AggregateRoot {
32
+ constructor(public readonly id: string) {
33
+ super();
34
+ }
35
+
36
+ place() {
37
+ this.apply(new OrderPlacedEvent(this.id));
38
+ }
39
+ }
40
+
41
+ @Publishable()
42
+ class PublishableOrder extends Order {}
43
+
44
+ class PlaceOrderCommand {
45
+ constructor(
46
+ public readonly orderId: string,
47
+ public readonly transaction?: Transaction,
48
+ public readonly publishable = false,
49
+ ) {}
50
+ }
51
+
52
+ class ScopedPlaceOrderCommand {
53
+ constructor(
54
+ public readonly orderId: string,
55
+ public readonly transaction: Transaction,
56
+ ) {}
57
+ }
58
+
59
+ /**
60
+ * Stands in for a publisher that writes events through the dispatcher context's transaction
61
+ * (an outbox, or durable workflows) before it hands them to the in-memory handlers.
62
+ */
63
+ @Injectable()
64
+ class TransactionalEventPublisher implements IEventPublisher {
65
+ readonly written: Array<{
66
+ orderIds: string[];
67
+ context: unknown;
68
+ asyncContext?: AsyncContext;
69
+ }> = [];
70
+ private readonly inner: IEventPublisher;
71
+
72
+ constructor(eventBus: EventBus) {
73
+ this.inner = eventBus.publisher;
74
+ eventBus.publisher = this;
75
+ }
76
+
77
+ publish(event: IEvent, context?: unknown, asyncContext?: AsyncContext) {
78
+ return this.publishAll([event], context, asyncContext);
79
+ }
80
+
81
+ async publishAll(
82
+ events: IEvent[],
83
+ context?: unknown,
84
+ asyncContext?: AsyncContext,
85
+ ) {
86
+ await setImmediate();
87
+ const transaction = (context as { transaction?: Transaction } | undefined)
88
+ ?.transaction;
89
+ if (transaction?.fail) {
90
+ throw transaction.fail;
91
+ }
92
+ this.written.push({
93
+ orderIds: events.map((event) => (event as OrderPlacedEvent).orderId),
94
+ context,
95
+ asyncContext,
96
+ });
97
+ events.forEach((event) => this.inner.publish(event, context, asyncContext));
98
+ return `written in ${transaction?.id ?? 'no transaction'}`;
99
+ }
100
+ }
101
+
102
+ @CommandHandler(PlaceOrderCommand)
103
+ class PlaceOrderHandler implements ICommandHandler<PlaceOrderCommand> {
104
+ constructor(private readonly publisher: EventPublisher) {}
105
+
106
+ async execute({ orderId, transaction, publishable }: PlaceOrderCommand) {
107
+ const order = publishable
108
+ ? new PublishableOrder(orderId)
109
+ : this.publisher.mergeObjectContext(new Order(orderId));
110
+ order.place();
111
+ return transaction ? order.commit({ transaction }) : order.commit();
112
+ }
113
+ }
114
+
115
+ @CommandHandler(ScopedPlaceOrderCommand, { scope: Scope.REQUEST })
116
+ class ScopedPlaceOrderHandler implements ICommandHandler<ScopedPlaceOrderCommand> {
117
+ constructor(
118
+ private readonly publisher: EventPublisher,
119
+ @Inject(REQUEST) private readonly context: AsyncContext,
120
+ ) {}
121
+
122
+ async execute({ orderId, transaction }: ScopedPlaceOrderCommand) {
123
+ const MergedOrder = this.publisher.mergeClassContext(Order, this.context);
124
+ const order = new MergedOrder(orderId);
125
+ order.place();
126
+ return order.commit({ transaction });
127
+ }
128
+ }
129
+
130
+ @EventsHandler(OrderPlacedEvent)
131
+ class OrderPlacedHandler implements IEventHandler<OrderPlacedEvent> {
132
+ static readonly handled: string[] = [];
133
+
134
+ handle(event: OrderPlacedEvent) {
135
+ OrderPlacedHandler.handled.push(event.orderId);
136
+ }
137
+ }
138
+
139
+ @Module({
140
+ imports: [CqrsModule.forRoot()],
141
+ providers: [
142
+ TransactionalEventPublisher,
143
+ PlaceOrderHandler,
144
+ ScopedPlaceOrderHandler,
145
+ OrderPlacedHandler,
146
+ ],
147
+ })
148
+ class OrdersModule {}
149
+
150
+ describe('Aggregate commit with a dispatcher context', () => {
151
+ let moduleRef: TestingModule;
152
+ let commandBus: CommandBus;
153
+ let publisher: TransactionalEventPublisher;
154
+
155
+ beforeAll(async () => {
156
+ moduleRef = await Test.createTestingModule({
157
+ imports: [OrdersModule],
158
+ }).compile();
159
+ await moduleRef.init();
160
+
161
+ commandBus = moduleRef.get(CommandBus);
162
+ publisher = moduleRef.get(TransactionalEventPublisher);
163
+ });
164
+
165
+ beforeEach(() => {
166
+ publisher.written.length = 0;
167
+ OrderPlacedHandler.handled.length = 0;
168
+ });
169
+
170
+ afterAll(async () => {
171
+ await moduleRef.close();
172
+ });
173
+
174
+ it('awaits the publisher, which receives the transaction and the events', async () => {
175
+ const transaction = { id: 'tx-1' };
176
+
177
+ const result = await commandBus.execute(
178
+ new PlaceOrderCommand('order-1', transaction),
179
+ );
180
+
181
+ expect(result).toBe('written in tx-1');
182
+ expect(publisher.written).toEqual([
183
+ {
184
+ orderIds: ['order-1'],
185
+ context: { transaction },
186
+ asyncContext: undefined,
187
+ },
188
+ ]);
189
+ await waitImmediate();
190
+ expect(OrderPlacedHandler.handled).toEqual(['order-1']);
191
+ });
192
+
193
+ it("rejects with the publisher's error", async () => {
194
+ const fail = new Error('serialization failure');
195
+
196
+ await expect(
197
+ commandBus.execute(
198
+ new PlaceOrderCommand('order-2', { id: 'tx-2', fail }),
199
+ ),
200
+ ).rejects.toBe(fail);
201
+
202
+ await waitImmediate();
203
+ expect(publisher.written).toEqual([]);
204
+ expect(OrderPlacedHandler.handled).toEqual([]);
205
+ });
206
+
207
+ it('passes the transaction from a @Publishable() aggregate', async () => {
208
+ const transaction = { id: 'tx-3' };
209
+
210
+ await commandBus.execute(
211
+ new PlaceOrderCommand('order-3', transaction, true),
212
+ );
213
+
214
+ expect(publisher.written).toEqual([
215
+ {
216
+ orderIds: ['order-3'],
217
+ context: { transaction },
218
+ asyncContext: undefined,
219
+ },
220
+ ]);
221
+ });
222
+
223
+ it('keeps the async context of a request-scoped handler', async () => {
224
+ const asyncContext = new AsyncContext();
225
+ const transaction = { id: 'tx-4' };
226
+
227
+ await commandBus.execute(
228
+ new ScopedPlaceOrderCommand('order-4', transaction),
229
+ asyncContext,
230
+ );
231
+
232
+ expect(publisher.written).toEqual([
233
+ { orderIds: ['order-4'], context: { transaction }, asyncContext },
234
+ ]);
235
+ });
236
+
237
+ it('keeps passing the aggregate as the dispatcher context without one', async () => {
238
+ await commandBus.execute(new PlaceOrderCommand('order-5'));
239
+
240
+ expect(publisher.written).toHaveLength(1);
241
+ expect(publisher.written[0].context).toBeInstanceOf(Order);
242
+ });
243
+ });
@@ -1,20 +1,24 @@
1
1
  import { Test, TestingModule } from '@nestjs/testing';
2
2
  import { setTimeout } from 'timers/promises';
3
- import { CommandBus, QueryBus } from '../../src';
4
- import { AppModule } from '../src/app.module';
5
- import { DropAncientItemHandler } from '../src/heroes/commands/handlers/drop-ancient-item.handler';
6
- import { KillDragonHandler } from '../src/heroes/commands/handlers/kill-dragon.handler';
7
- import { DropAncientItemCommand } from '../src/heroes/commands/impl/drop-ancient-item.command';
8
- import { KillDragonCommand } from '../src/heroes/commands/impl/kill-dragon.command';
9
- import { HeroFoundItemHandler } from '../src/heroes/events/handlers/hero-found-item.handler';
10
- import { HeroKilledDragonHandler } from '../src/heroes/events/handlers/hero-killed-dragon.handler';
11
- import { HeroFoundItemEvent } from '../src/heroes/events/impl/hero-found-item.event';
12
- import { HeroKilledDragonEvent } from '../src/heroes/events/impl/hero-killed-dragon.event';
13
- import { GetHeroesQuery } from '../src/heroes/queries/impl';
14
- import { HERO_ID } from '../src/heroes/repository/fixtures/user';
15
- import { ANCIENT_ITEM_ID } from '../src/heroes/sagas/heroes.sagas';
16
- import { NoopHandler } from '../src/noop/events/handlers/noop.handler';
17
- import { waitImmediate } from '../utils/wait-immediate';
3
+ import type { MockInstance } from 'vitest';
4
+ import { CommandBus, QueryBus } from '../../src/index.js';
5
+ import { AppModule } from '../src/app.module.js';
6
+ import { DropAncientItemHandler } from '../src/heroes/commands/handlers/drop-ancient-item.handler.js';
7
+ import { KillDragonHandler } from '../src/heroes/commands/handlers/kill-dragon.handler.js';
8
+ import { DropAncientItemCommand } from '../src/heroes/commands/impl/drop-ancient-item.command.js';
9
+ import { KillDragonCommand } from '../src/heroes/commands/impl/kill-dragon.command.js';
10
+ import { DragonDiedHandler } from '../src/heroes/events/handlers/dragon-died.handler.js';
11
+ import { HeroFoundItemHandler } from '../src/heroes/events/handlers/hero-found-item.handler.js';
12
+ import { HeroKilledDragonHandler } from '../src/heroes/events/handlers/hero-killed-dragon.handler.js';
13
+ import { DragonDiedEvent } from '../src/heroes/events/impl/dragon-died.event.js';
14
+ import { HeroFoundItemEvent } from '../src/heroes/events/impl/hero-found-item.event.js';
15
+ import { HeroKilledDragonEvent } from '../src/heroes/events/impl/hero-killed-dragon.event.js';
16
+ import { DRAGON_ID } from '../src/heroes/repository/fixtures/dragon.js';
17
+ import { GetHeroesQuery, GetHeroQuery } from '../src/heroes/queries/impl/index.js';
18
+ import { HERO_ID } from '../src/heroes/repository/fixtures/user.js';
19
+ import { ANCIENT_ITEM_ID } from '../src/heroes/sagas/heroes.sagas.js';
20
+ import { NoopHandler } from '../src/noop/events/handlers/noop.handler.js';
21
+ import { waitImmediate } from '../utils/wait-immediate.js';
18
22
 
19
23
  describe('Basic flows', () => {
20
24
  let moduleRef: TestingModule;
@@ -28,23 +32,26 @@ describe('Basic flows', () => {
28
32
  });
29
33
 
30
34
  describe('when "KillDragonCommand" command is dispatched', () => {
31
- let killDragonExecuteSpy: jest.SpyInstance;
32
- let heroKilledDragonHandleSpy: jest.SpyInstance;
33
- let noopEventHandleSpy: jest.SpyInstance;
35
+ let killDragonExecuteSpy: MockInstance;
36
+ let heroKilledDragonHandleSpy: MockInstance;
37
+ let dragonDiedHandleSpy: MockInstance;
38
+ let noopEventHandleSpy: MockInstance;
34
39
  let command: KillDragonCommand;
35
40
 
36
41
  beforeAll(async () => {
37
42
  const killDragonHandler = moduleRef.get(KillDragonHandler);
38
43
  const heroKilledDragonHandler = moduleRef.get(HeroKilledDragonHandler);
44
+ const dragonDiedHandler = moduleRef.get(DragonDiedHandler);
39
45
  const noopHandler = moduleRef.get(NoopHandler);
40
46
 
41
- killDragonExecuteSpy = jest.spyOn(killDragonHandler, 'execute');
42
- heroKilledDragonHandleSpy = jest.spyOn(heroKilledDragonHandler, 'handle');
43
- noopEventHandleSpy = jest.spyOn(noopHandler, 'handle');
47
+ killDragonExecuteSpy = vi.spyOn(killDragonHandler, 'execute');
48
+ heroKilledDragonHandleSpy = vi.spyOn(heroKilledDragonHandler, 'handle');
49
+ dragonDiedHandleSpy = vi.spyOn(dragonDiedHandler, 'handle');
50
+ noopEventHandleSpy = vi.spyOn(noopHandler, 'handle');
44
51
 
45
52
  const commandBus = moduleRef.get(CommandBus);
46
53
  const heroId = HERO_ID;
47
- const dragonId = 'dragonId';
54
+ const dragonId = DRAGON_ID;
48
55
 
49
56
  command = new KillDragonCommand(heroId, dragonId);
50
57
  await commandBus.execute(command);
@@ -60,19 +67,24 @@ describe('Basic flows', () => {
60
67
  expect(heroKilledDragonHandleSpy).toHaveBeenCalledWith(event);
61
68
  });
62
69
 
70
+ it('should handle "DragonDiedEvent" event from aggregate root', () => {
71
+ const event = new DragonDiedEvent(DRAGON_ID);
72
+ expect(dragonDiedHandleSpy).toHaveBeenCalledWith(event);
73
+ });
74
+
63
75
  it('should not trigger "NoopHandler" event', () => {
64
76
  expect(noopEventHandleSpy).not.toHaveBeenCalled();
65
77
  });
66
78
 
67
79
  describe('when saga triggered', () => {
68
- let dropAncientExecuteSpy: jest.SpyInstance;
69
- let heroFoundItemSpy: jest.SpyInstance;
80
+ let dropAncientExecuteSpy: MockInstance;
81
+ let heroFoundItemSpy: MockInstance;
70
82
 
71
83
  beforeAll(async () => {
72
84
  const dropAncientItemHandler = moduleRef.get(DropAncientItemHandler);
73
85
  const heroFoundItemHandler = moduleRef.get(HeroFoundItemHandler);
74
- dropAncientExecuteSpy = jest.spyOn(dropAncientItemHandler, 'execute');
75
- heroFoundItemSpy = jest.spyOn(heroFoundItemHandler, 'handle');
86
+ dropAncientExecuteSpy = vi.spyOn(dropAncientItemHandler, 'execute');
87
+ heroFoundItemSpy = vi.spyOn(heroFoundItemHandler, 'handle');
76
88
 
77
89
  // Wait 1 second for saga to be triggered
78
90
  await setTimeout(1000);
@@ -100,6 +112,14 @@ describe('Basic flows', () => {
100
112
  });
101
113
  });
102
114
 
115
+ describe('when "GetHeroQuery" query is executed', () => {
116
+ it('should return a single hero', async () => {
117
+ const queryBus = moduleRef.get(QueryBus);
118
+ const hero = await queryBus.execute(new GetHeroQuery(1234));
119
+ expect(hero).toEqual(expect.objectContaining({ id: HERO_ID }));
120
+ });
121
+ });
122
+
103
123
  afterAll(async () => {
104
124
  await moduleRef.close();
105
125
  });
@@ -1,4 +1,4 @@
1
- import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
1
+ import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper.js';
2
2
  import { Test, TestingModule } from '@nestjs/testing';
3
3
  import {
4
4
  Command,
@@ -6,8 +6,8 @@ import {
6
6
  ICommandHandler,
7
7
  Query,
8
8
  QueryBus,
9
- } from '../../src';
10
- import { AppModule } from '../src/app.module';
9
+ } from '../../src/index.js';
10
+ import { AppModule } from '../src/app.module.js';
11
11
 
12
12
  describe('Generics', () => {
13
13
  let moduleRef: TestingModule;
@@ -1,19 +1,20 @@
1
1
  import { Test, TestingModule } from '@nestjs/testing';
2
2
  import { setTimeout } from 'timers/promises';
3
- import { AsyncContext, CommandBus, QueryBus } from '../../src';
4
- import { AppModule } from '../src/app.module';
5
- import { HERO_ID } from '../src/heroes/repository/fixtures/user';
6
- import { ANCIENT_ITEM_ID } from '../src/heroes/sagas/heroes.sagas';
7
- import { ScopedDropAncientItemHandler } from '../src/scoped/commands/handlers/scoped-drop-ancient-item.handler';
8
- import { ScopedKillDragonHandler } from '../src/scoped/commands/handlers/scoped-kill-dragon.handler';
9
- import { ScopedDropAncientItemCommand } from '../src/scoped/commands/impl/scoped-drop-ancient-item.command';
10
- import { ScopedKillDragonCommand } from '../src/scoped/commands/impl/scoped-kill-dragon.command';
11
- import { ScopedHeroFoundItemHandler } from '../src/scoped/events/handlers/scoped-hero-found-item.handler';
12
- import { ScopedHeroKilledDragonHandler } from '../src/scoped/events/handlers/scoped-hero-killed-dragon.handler';
13
- import { ScopedHeroFoundItemEvent } from '../src/scoped/events/impl/hero-found-item.event';
14
- import { ScopedHeroKilledDragonEvent } from '../src/scoped/events/impl/hero-killed-dragon.event';
15
- import { ScopedGetHeroesQuery } from '../src/scoped/queries/impl';
16
- import { waitImmediate } from '../utils/wait-immediate';
3
+ import type { MockInstance } from 'vitest';
4
+ import { AsyncContext, CommandBus, QueryBus } from '../../src/index.js';
5
+ import { AppModule } from '../src/app.module.js';
6
+ import { HERO_ID } from '../src/heroes/repository/fixtures/user.js';
7
+ import { ANCIENT_ITEM_ID } from '../src/heroes/sagas/heroes.sagas.js';
8
+ import { ScopedDropAncientItemHandler } from '../src/scoped/commands/handlers/scoped-drop-ancient-item.handler.js';
9
+ import { ScopedKillDragonHandler } from '../src/scoped/commands/handlers/scoped-kill-dragon.handler.js';
10
+ import { ScopedDropAncientItemCommand } from '../src/scoped/commands/impl/scoped-drop-ancient-item.command.js';
11
+ import { ScopedKillDragonCommand } from '../src/scoped/commands/impl/scoped-kill-dragon.command.js';
12
+ import { ScopedHeroFoundItemHandler } from '../src/scoped/events/handlers/scoped-hero-found-item.handler.js';
13
+ import { ScopedHeroKilledDragonHandler } from '../src/scoped/events/handlers/scoped-hero-killed-dragon.handler.js';
14
+ import { ScopedHeroFoundItemEvent } from '../src/scoped/events/impl/hero-found-item.event.js';
15
+ import { ScopedHeroKilledDragonEvent } from '../src/scoped/events/impl/hero-killed-dragon.event.js';
16
+ import { ScopedGetHeroesQuery } from '../src/scoped/queries/impl/index.js';
17
+ import { waitImmediate } from '../utils/wait-immediate.js';
17
18
 
18
19
  describe('Request scope', () => {
19
20
  let moduleRef: TestingModule;
@@ -29,8 +30,8 @@ describe('Request scope', () => {
29
30
  describe('when "ScopedKillDragonCommand" command is dispatched', () => {
30
31
  const asyncContext = new AsyncContext();
31
32
 
32
- let killDragonExecuteSpy: jest.SpyInstance;
33
- let heroKilledDragonHandleSpy: jest.SpyInstance;
33
+ let killDragonExecuteSpy: MockInstance;
34
+ let heroKilledDragonHandleSpy: MockInstance;
34
35
  let command: ScopedKillDragonCommand;
35
36
 
36
37
  beforeAll(async () => {
@@ -45,8 +46,8 @@ describe('Request scope', () => {
45
46
  asyncContext.id,
46
47
  );
47
48
 
48
- killDragonExecuteSpy = jest.spyOn(killDragonHandler, 'execute');
49
- heroKilledDragonHandleSpy = jest.spyOn(heroKilledDragonHandler, 'handle');
49
+ killDragonExecuteSpy = vi.spyOn(killDragonHandler, 'execute');
50
+ heroKilledDragonHandleSpy = vi.spyOn(heroKilledDragonHandler, 'handle');
50
51
 
51
52
  const commandBus = moduleRef.get(CommandBus);
52
53
  const heroId = HERO_ID;
@@ -70,8 +71,8 @@ describe('Request scope', () => {
70
71
  });
71
72
 
72
73
  describe('when saga triggered', () => {
73
- let dropAncientExecuteSpy: jest.SpyInstance;
74
- let heroFoundItemSpy: jest.SpyInstance;
74
+ let dropAncientExecuteSpy: MockInstance;
75
+ let heroFoundItemSpy: MockInstance;
75
76
 
76
77
  beforeAll(async () => {
77
78
  const dropAncientItemHandler = await moduleRef.resolve(
@@ -82,8 +83,8 @@ describe('Request scope', () => {
82
83
  ScopedHeroFoundItemHandler,
83
84
  asyncContext.id,
84
85
  );
85
- dropAncientExecuteSpy = jest.spyOn(dropAncientItemHandler, 'execute');
86
- heroFoundItemSpy = jest.spyOn(heroFoundItemHandler, 'handle');
86
+ dropAncientExecuteSpy = vi.spyOn(dropAncientItemHandler, 'execute');
87
+ heroFoundItemSpy = vi.spyOn(heroFoundItemHandler, 'handle');
87
88
 
88
89
  // Wait 1 second for saga to be triggered
89
90
  await setTimeout(1000);