@nestjs/cqrs 10.2.8 → 11.0.0-next.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.
- package/.circleci/config.yml +29 -1
- package/dist/classes/command.d.ts +9 -0
- package/dist/classes/command.js +7 -0
- package/dist/classes/constants.d.ts +1 -0
- package/dist/classes/constants.js +4 -0
- package/dist/classes/index.d.ts +2 -0
- package/dist/classes/index.js +18 -0
- package/dist/classes/query.d.ts +9 -0
- package/dist/classes/query.js +7 -0
- package/dist/command-bus.d.ts +26 -7
- package/dist/command-bus.js +57 -21
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -0
- package/dist/cqrs.module.d.ts +5 -4
- package/dist/cqrs.module.js +21 -11
- package/dist/decorators/command-handler.decorator.d.ts +3 -1
- package/dist/decorators/command-handler.decorator.js +8 -3
- package/dist/decorators/events-handler.decorator.d.ts +3 -1
- package/dist/decorators/events-handler.decorator.js +9 -2
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.js +6 -1
- package/dist/decorators/publishable.decorator.d.ts +5 -0
- package/dist/decorators/publishable.decorator.js +13 -0
- package/dist/decorators/query-handler.decorator.d.ts +3 -1
- package/dist/decorators/query-handler.decorator.js +8 -3
- package/dist/event-bus.d.ts +51 -10
- package/dist/event-bus.js +106 -31
- package/dist/event-publisher.d.ts +5 -2
- package/dist/event-publisher.js +10 -8
- package/dist/exceptions/command-not-found.exception.js +1 -1
- package/dist/exceptions/index.d.ts +1 -0
- package/dist/exceptions/index.js +6 -1
- package/dist/exceptions/invalid-command-handler.exception.js +1 -1
- package/dist/exceptions/invalid-events-handler.exception.js +1 -1
- package/dist/exceptions/invalid-query-handler.exception.js +1 -1
- package/dist/exceptions/invalid-saga.exception.js +1 -1
- package/dist/exceptions/query-not-found.exception.js +1 -1
- package/dist/exceptions/unsupported-saga-scope.exception.d.ts +3 -0
- package/dist/exceptions/unsupported-saga-scope.exception.js +9 -0
- package/dist/helpers/default-event-id-provider.d.ts +6 -0
- package/dist/helpers/default-event-id-provider.js +10 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -1
- package/dist/interfaces/commands/command-bus.interface.d.ts +18 -1
- package/dist/interfaces/commands/command-handler.interface.d.ts +9 -2
- package/dist/interfaces/cqrs-module-options.interface.d.ts +38 -8
- package/dist/interfaces/events/event-bus.interface.d.ts +41 -4
- package/dist/interfaces/events/event-id-provider.interface.d.ts +11 -0
- package/dist/interfaces/events/event-id-provider.interface.js +2 -0
- package/dist/interfaces/events/event-publisher.interface.d.ts +7 -4
- package/dist/interfaces/index.d.ts +2 -0
- package/dist/interfaces/index.js +7 -1
- package/dist/interfaces/providers-introspection-result.interface.d.ts +10 -0
- package/dist/interfaces/providers-introspection-result.interface.js +2 -0
- package/dist/interfaces/queries/query-bus.interface.d.ts +18 -1
- package/dist/interfaces/queries/query-handler.interface.d.ts +9 -2
- package/dist/interfaces/saga.type.d.ts +1 -1
- package/dist/operators/index.js +5 -1
- package/dist/operators/of-type.d.ts +1 -1
- package/dist/operators/of-type.js +1 -2
- package/dist/query-bus.d.ts +26 -6
- package/dist/query-bus.js +49 -20
- package/dist/scopes/async.context.d.ts +30 -0
- package/dist/scopes/async.context.js +58 -0
- package/dist/scopes/index.d.ts +1 -0
- package/dist/scopes/index.js +17 -0
- package/dist/services/explorer.service.d.ts +4 -6
- package/dist/services/explorer.service.js +14 -14
- package/dist/storages/aggregate-root.storage.d.ts +8 -0
- package/dist/storages/aggregate-root.storage.js +21 -0
- package/dist/unhandled-exception-bus.d.ts +4 -3
- package/dist/unhandled-exception-bus.js +18 -5
- package/dist/utils/index.js +5 -1
- package/jest.config.js +5 -0
- package/package.json +29 -17
- package/renovate.json +8 -7
- package/test/e2e/basic-flows.spec.ts +98 -0
- package/test/e2e/generics.spec.ts +198 -0
- package/test/e2e/request-scope.spec.ts +141 -0
- package/test/e2e/unhandled-exceptions.spec.ts +73 -0
- package/test/jest-e2e.json +9 -0
- package/test/utils/wait-immediate.ts +2 -0
- package/tsconfig.build.json +10 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Type } from '@nestjs/common';
|
|
2
2
|
import 'reflect-metadata';
|
|
3
3
|
import { Observable } from 'rxjs';
|
|
4
|
-
import { ICommand, IEvent, IUnhandledExceptionPublisher, UnhandledExceptionInfo } from './interfaces';
|
|
4
|
+
import { CqrsModuleOptions, ICommand, IEvent, IUnhandledExceptionPublisher, UnhandledExceptionInfo } from './interfaces';
|
|
5
5
|
import { ObservableBus } from './utils/observable-bus';
|
|
6
6
|
/**
|
|
7
7
|
* A bus that publishes unhandled exceptions.
|
|
8
8
|
* @template Cause The type of the cause of the exception.
|
|
9
9
|
*/
|
|
10
10
|
export declare class UnhandledExceptionBus<Cause = IEvent | ICommand> extends ObservableBus<UnhandledExceptionInfo<Cause>> {
|
|
11
|
+
private readonly options?;
|
|
11
12
|
private _publisher;
|
|
12
|
-
constructor();
|
|
13
|
+
constructor(options?: CqrsModuleOptions | undefined);
|
|
13
14
|
/**
|
|
14
15
|
* Filter values depending on their instance type (comparison is made
|
|
15
16
|
* using native `instanceof`).
|
|
@@ -17,7 +18,7 @@ export declare class UnhandledExceptionBus<Cause = IEvent | ICommand> extends Ob
|
|
|
17
18
|
* @param types List of types to filter by.
|
|
18
19
|
* @return A stream only emitting the filtered exceptions.
|
|
19
20
|
*/
|
|
20
|
-
static ofType<TInput, TOutput>(...types: Type<TOutput>[]): (source: Observable<UnhandledExceptionInfo<TInput>>) => Observable<UnhandledExceptionInfo<TOutput>>;
|
|
21
|
+
static ofType<TInput extends IEvent | ICommand, TOutput extends IEvent | ICommand>(...types: Type<TOutput>[]): (source: Observable<UnhandledExceptionInfo<TInput>>) => Observable<UnhandledExceptionInfo<TOutput>>;
|
|
21
22
|
/**
|
|
22
23
|
* Gets the publisher of the bus.
|
|
23
24
|
*/
|
|
@@ -8,11 +8,15 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
exports.UnhandledExceptionBus = void 0;
|
|
13
16
|
const common_1 = require("@nestjs/common");
|
|
14
17
|
require("reflect-metadata");
|
|
15
18
|
const rxjs_1 = require("rxjs");
|
|
19
|
+
const constants_1 = require("./constants");
|
|
16
20
|
const default_unhandled_exception_pubsub_1 = require("./helpers/default-unhandled-exception-pubsub");
|
|
17
21
|
const observable_bus_1 = require("./utils/observable-bus");
|
|
18
22
|
/**
|
|
@@ -20,9 +24,16 @@ const observable_bus_1 = require("./utils/observable-bus");
|
|
|
20
24
|
* @template Cause The type of the cause of the exception.
|
|
21
25
|
*/
|
|
22
26
|
let UnhandledExceptionBus = class UnhandledExceptionBus extends observable_bus_1.ObservableBus {
|
|
23
|
-
constructor() {
|
|
27
|
+
constructor(options) {
|
|
24
28
|
super();
|
|
25
|
-
this.
|
|
29
|
+
this.options = options;
|
|
30
|
+
if (this.options?.unhandledExceptionPublisher) {
|
|
31
|
+
this._publisher = this.options
|
|
32
|
+
.unhandledExceptionPublisher;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
this.useDefaultPublisher();
|
|
36
|
+
}
|
|
26
37
|
}
|
|
27
38
|
/**
|
|
28
39
|
* Filter values depending on their instance type (comparison is made
|
|
@@ -58,8 +69,10 @@ let UnhandledExceptionBus = class UnhandledExceptionBus extends observable_bus_1
|
|
|
58
69
|
this._publisher = new default_unhandled_exception_pubsub_1.DefaultUnhandledExceptionPubSub(this.subject$);
|
|
59
70
|
}
|
|
60
71
|
};
|
|
61
|
-
UnhandledExceptionBus =
|
|
72
|
+
exports.UnhandledExceptionBus = UnhandledExceptionBus;
|
|
73
|
+
exports.UnhandledExceptionBus = UnhandledExceptionBus = __decorate([
|
|
62
74
|
(0, common_1.Injectable)(),
|
|
63
|
-
|
|
75
|
+
__param(0, (0, common_1.Optional)()),
|
|
76
|
+
__param(0, (0, common_1.Inject)(constants_1.CQRS_MODULE_OPTIONS)),
|
|
77
|
+
__metadata("design:paramtypes", [Object])
|
|
64
78
|
], UnhandledExceptionBus);
|
|
65
|
-
exports.UnhandledExceptionBus = UnhandledExceptionBus;
|
package/dist/utils/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
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);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
package/jest.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/cqrs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.0-next.1",
|
|
4
4
|
"description": "A lightweight CQRS module for Nest framework (node.js)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"url": "https://github.com/nestjs/cqrs#readme",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest",
|
|
9
|
-
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"build": "tsc -p tsconfig.build.json",
|
|
10
10
|
"format": "prettier --write \"**/*.ts\"",
|
|
11
11
|
"lint": "eslint \"src/**/*.ts\" --fix",
|
|
12
12
|
"prepublish:next": "npm run build",
|
|
@@ -15,32 +15,32 @@
|
|
|
15
15
|
"publish:npm": "npm publish --access public",
|
|
16
16
|
"prerelease": "npm run build",
|
|
17
17
|
"release": "release-it",
|
|
18
|
-
"prepare": "husky install"
|
|
19
|
-
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"uuid": "11.0.2"
|
|
18
|
+
"prepare": "husky install",
|
|
19
|
+
"test:e2e": "jest --config ./test/jest-e2e.json --runInBand"
|
|
22
20
|
},
|
|
21
|
+
"dependencies": {},
|
|
23
22
|
"devDependencies": {
|
|
24
|
-
"@commitlint/cli": "19.
|
|
25
|
-
"@commitlint/config-angular": "19.
|
|
26
|
-
"@nestjs/common": "10.4.
|
|
27
|
-
"@nestjs/core": "10.4.
|
|
28
|
-
"@
|
|
23
|
+
"@commitlint/cli": "19.6.0",
|
|
24
|
+
"@commitlint/config-angular": "19.6.0",
|
|
25
|
+
"@nestjs/common": "10.4.11",
|
|
26
|
+
"@nestjs/core": "10.4.11",
|
|
27
|
+
"@nestjs/testing": "10.4.12",
|
|
29
28
|
"@types/jest": "29.5.14",
|
|
30
|
-
"@
|
|
31
|
-
"@typescript-eslint/
|
|
32
|
-
"eslint": "
|
|
29
|
+
"@types/node": "22.10.1",
|
|
30
|
+
"@typescript-eslint/eslint-plugin": "8.16.0",
|
|
31
|
+
"@typescript-eslint/parser": "8.16.0",
|
|
32
|
+
"eslint": "9.15.0",
|
|
33
33
|
"eslint-config-prettier": "9.1.0",
|
|
34
34
|
"eslint-plugin-import": "2.31.0",
|
|
35
|
-
"husky": "9.1.
|
|
35
|
+
"husky": "9.1.7",
|
|
36
36
|
"jest": "29.7.0",
|
|
37
37
|
"lint-staged": "15.2.10",
|
|
38
|
-
"prettier": "3.
|
|
38
|
+
"prettier": "3.4.1",
|
|
39
39
|
"reflect-metadata": "0.2.2",
|
|
40
40
|
"release-it": "17.10.0",
|
|
41
41
|
"rxjs": "7.8.1",
|
|
42
42
|
"ts-jest": "29.2.5",
|
|
43
|
-
"typescript": "5.
|
|
43
|
+
"typescript": "5.7.2"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@nestjs/common": "^9.0.0 || ^10.0.0",
|
|
@@ -54,5 +54,17 @@
|
|
|
54
54
|
"repository": {
|
|
55
55
|
"type": "git",
|
|
56
56
|
"url": "https://github.com/nestjs/cqrs"
|
|
57
|
+
},
|
|
58
|
+
"changelog": {
|
|
59
|
+
"labels": {
|
|
60
|
+
"feature": "Features",
|
|
61
|
+
"bug": "Bug fixes",
|
|
62
|
+
"enhancement": "Enhancements",
|
|
63
|
+
"docs": "Docs",
|
|
64
|
+
"dependencies": "Dependencies",
|
|
65
|
+
"type: code style": "Code style tweaks",
|
|
66
|
+
"status: blocked": "Breaking changes",
|
|
67
|
+
"breaking change": "Breaking changes"
|
|
68
|
+
}
|
|
57
69
|
}
|
|
58
70
|
}
|
package/renovate.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"semanticCommits": true,
|
|
3
|
-
"packageRules": [
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
]
|
|
3
|
+
"packageRules": [
|
|
4
|
+
{
|
|
5
|
+
"depTypeList": ["devDependencies"],
|
|
6
|
+
"automerge": true
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"labels": ["dependencies"],
|
|
10
|
+
"extends": ["config:base"]
|
|
10
11
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
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 { waitImmediate } from '../utils/wait-immediate';
|
|
17
|
+
|
|
18
|
+
describe('Basic flows', () => {
|
|
19
|
+
let moduleRef: TestingModule;
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
moduleRef = await Test.createTestingModule({
|
|
23
|
+
imports: [AppModule],
|
|
24
|
+
}).compile();
|
|
25
|
+
|
|
26
|
+
await moduleRef.init();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('when "KillDragonCommand" command is dispatched', () => {
|
|
30
|
+
let killDragonExecuteSpy: jest.SpyInstance;
|
|
31
|
+
let heroKilledDragonHandleSpy: jest.SpyInstance;
|
|
32
|
+
let command: KillDragonCommand;
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
const killDragonHandler = moduleRef.get(KillDragonHandler);
|
|
36
|
+
const heroKilledDragonHandler = moduleRef.get(HeroKilledDragonHandler);
|
|
37
|
+
|
|
38
|
+
killDragonExecuteSpy = jest.spyOn(killDragonHandler, 'execute');
|
|
39
|
+
heroKilledDragonHandleSpy = jest.spyOn(heroKilledDragonHandler, 'handle');
|
|
40
|
+
|
|
41
|
+
const commandBus = moduleRef.get(CommandBus);
|
|
42
|
+
const heroId = HERO_ID;
|
|
43
|
+
const dragonId = 'dragonId';
|
|
44
|
+
|
|
45
|
+
command = new KillDragonCommand(heroId, dragonId);
|
|
46
|
+
await commandBus.execute(command);
|
|
47
|
+
await waitImmediate();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should execute command handler', () => {
|
|
51
|
+
expect(killDragonExecuteSpy).toHaveBeenCalledWith(command);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle "HeroKillDragonEvent" event', () => {
|
|
55
|
+
const event = new HeroKilledDragonEvent(command.heroId, command.dragonId);
|
|
56
|
+
expect(heroKilledDragonHandleSpy).toHaveBeenCalledWith(event);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('when saga triggered', () => {
|
|
60
|
+
let dropAncientExecuteSpy: jest.SpyInstance;
|
|
61
|
+
let heroFoundItemSpy: jest.SpyInstance;
|
|
62
|
+
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
const dropAncientItemHandler = moduleRef.get(DropAncientItemHandler);
|
|
65
|
+
const heroFoundItemHandler = moduleRef.get(HeroFoundItemHandler);
|
|
66
|
+
dropAncientExecuteSpy = jest.spyOn(dropAncientItemHandler, 'execute');
|
|
67
|
+
heroFoundItemSpy = jest.spyOn(heroFoundItemHandler, 'handle');
|
|
68
|
+
|
|
69
|
+
// Wait 1 second for saga to be triggered
|
|
70
|
+
await setTimeout(1000);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should dispatch "DropAncientItemCommand" and execute its command handler', () => {
|
|
74
|
+
expect(dropAncientExecuteSpy).toHaveBeenCalledWith(
|
|
75
|
+
new DropAncientItemCommand(HERO_ID, ANCIENT_ITEM_ID),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle "HeroFoundItemEvent" event', () => {
|
|
80
|
+
expect(heroFoundItemSpy).toHaveBeenCalledWith(
|
|
81
|
+
new HeroFoundItemEvent(HERO_ID, ANCIENT_ITEM_ID),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('when "GetHeroesQuery" query is executed', () => {
|
|
88
|
+
it('should return all heroes', async () => {
|
|
89
|
+
const queryBus = moduleRef.get(QueryBus);
|
|
90
|
+
const heroes = await queryBus.execute(new GetHeroesQuery());
|
|
91
|
+
expect(heroes).toEqual([expect.objectContaining({ id: HERO_ID })]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterAll(async () => {
|
|
96
|
+
await moduleRef.close();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
|
|
2
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
3
|
+
import {
|
|
4
|
+
Command,
|
|
5
|
+
CommandBus,
|
|
6
|
+
ICommandHandler,
|
|
7
|
+
Query,
|
|
8
|
+
QueryBus,
|
|
9
|
+
} from '../../src';
|
|
10
|
+
import { AppModule } from '../src/app.module';
|
|
11
|
+
|
|
12
|
+
describe('Generics', () => {
|
|
13
|
+
let moduleRef: TestingModule;
|
|
14
|
+
let commandBus: CommandBus;
|
|
15
|
+
let queryBus: QueryBus;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
moduleRef = await Test.createTestingModule({
|
|
19
|
+
imports: [AppModule],
|
|
20
|
+
}).compile();
|
|
21
|
+
|
|
22
|
+
await moduleRef.init();
|
|
23
|
+
|
|
24
|
+
commandBus = moduleRef.get(CommandBus);
|
|
25
|
+
queryBus = moduleRef.get(QueryBus);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('Commands', () => {
|
|
29
|
+
describe('when "Command" utility class is used', () => {
|
|
30
|
+
it('should infer return type', async () => {
|
|
31
|
+
const command = new Command<string>();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await commandBus.execute(command).then((value) => {
|
|
35
|
+
value as string;
|
|
36
|
+
|
|
37
|
+
// @ts-expect-error
|
|
38
|
+
value as number;
|
|
39
|
+
});
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// Do nothing
|
|
42
|
+
} finally {
|
|
43
|
+
expect(true).toBeTruthy();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('when any other class is used', () => {
|
|
49
|
+
it('should fallback to any return type', async () => {
|
|
50
|
+
class MyCommand {}
|
|
51
|
+
|
|
52
|
+
const command = new MyCommand();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await commandBus.execute(command).then((value) => {
|
|
56
|
+
value as string;
|
|
57
|
+
value as number;
|
|
58
|
+
});
|
|
59
|
+
} catch (err) {
|
|
60
|
+
// Do nothing
|
|
61
|
+
} finally {
|
|
62
|
+
expect(true).toBeTruthy();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should use the 2nd generic parameter as return type', async () => {
|
|
67
|
+
class MyCommand {}
|
|
68
|
+
|
|
69
|
+
const command = new MyCommand();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await commandBus.execute<MyCommand, string>(command).then((value) => {
|
|
73
|
+
value as string;
|
|
74
|
+
|
|
75
|
+
// @ts-expect-error
|
|
76
|
+
value as number;
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// Do nothing
|
|
80
|
+
} finally {
|
|
81
|
+
expect(true).toBeTruthy();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('Queries', () => {
|
|
88
|
+
describe('when "Query" utility class is used', () => {
|
|
89
|
+
it('should infer return type', async () => {
|
|
90
|
+
const query = new Query<string>();
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await queryBus.execute(query).then((value) => {
|
|
94
|
+
value as string;
|
|
95
|
+
|
|
96
|
+
// @ts-expect-error
|
|
97
|
+
value as number;
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// Do nothing
|
|
101
|
+
} finally {
|
|
102
|
+
expect(true).toBeTruthy();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('when any other class is used', () => {
|
|
108
|
+
it('should fallback to any return type', async () => {
|
|
109
|
+
class MyQuery {}
|
|
110
|
+
|
|
111
|
+
const query = new MyQuery();
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await queryBus.execute(query).then((value) => {
|
|
115
|
+
value as string;
|
|
116
|
+
value as number;
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
// Do nothing
|
|
120
|
+
} finally {
|
|
121
|
+
expect(true).toBeTruthy();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should use the 2nd generic parameter as return type', async () => {
|
|
126
|
+
class MyQuery {}
|
|
127
|
+
|
|
128
|
+
const query = new MyQuery();
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await queryBus.execute<MyQuery, string>(query).then((value) => {
|
|
132
|
+
value as string;
|
|
133
|
+
|
|
134
|
+
// @ts-expect-error
|
|
135
|
+
value as number;
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
// Do nothing
|
|
139
|
+
} finally {
|
|
140
|
+
expect(true).toBeTruthy();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Command handlers', () => {
|
|
147
|
+
it('should infer return type', async () => {
|
|
148
|
+
class Test extends Command<{
|
|
149
|
+
value: string;
|
|
150
|
+
}> {}
|
|
151
|
+
|
|
152
|
+
class ValidHandler implements ICommandHandler<Test> {
|
|
153
|
+
execute(command: Test): Promise<{ value: string }> {
|
|
154
|
+
throw new Error('Method not implemented.');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class InvalidHandler implements ICommandHandler<Test> {
|
|
159
|
+
// @ts-expect-error
|
|
160
|
+
execute(command: Test): Promise<{ value: number }> {
|
|
161
|
+
throw new Error('Method not implemented.');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
commandBus.bind(
|
|
167
|
+
new InstanceWrapper({
|
|
168
|
+
metatype: ValidHandler,
|
|
169
|
+
instance: new ValidHandler(),
|
|
170
|
+
}),
|
|
171
|
+
'Test',
|
|
172
|
+
);
|
|
173
|
+
commandBus.bind(
|
|
174
|
+
new InstanceWrapper({
|
|
175
|
+
metatype: InvalidHandler,
|
|
176
|
+
instance: new InvalidHandler() as any,
|
|
177
|
+
}),
|
|
178
|
+
'Test2',
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
await commandBus.execute(new Test()).then((value) => {
|
|
182
|
+
value.value as string;
|
|
183
|
+
|
|
184
|
+
// @ts-expect-error
|
|
185
|
+
value as number;
|
|
186
|
+
});
|
|
187
|
+
} catch (err) {
|
|
188
|
+
// Do nothing
|
|
189
|
+
} finally {
|
|
190
|
+
expect(true).toBeTruthy();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
afterAll(async () => {
|
|
196
|
+
await moduleRef.close();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
|
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';
|
|
17
|
+
|
|
18
|
+
describe('Request scope', () => {
|
|
19
|
+
let moduleRef: TestingModule;
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
moduleRef = await Test.createTestingModule({
|
|
23
|
+
imports: [AppModule],
|
|
24
|
+
}).compile();
|
|
25
|
+
|
|
26
|
+
await moduleRef.init();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('when "ScopedKillDragonCommand" command is dispatched', () => {
|
|
30
|
+
const asyncContext = new AsyncContext();
|
|
31
|
+
|
|
32
|
+
let killDragonExecuteSpy: jest.SpyInstance;
|
|
33
|
+
let heroKilledDragonHandleSpy: jest.SpyInstance;
|
|
34
|
+
let command: ScopedKillDragonCommand;
|
|
35
|
+
|
|
36
|
+
beforeAll(async () => {
|
|
37
|
+
moduleRef.registerRequestByContextId(asyncContext, asyncContext.id);
|
|
38
|
+
|
|
39
|
+
const killDragonHandler = await moduleRef.resolve(
|
|
40
|
+
ScopedKillDragonHandler,
|
|
41
|
+
asyncContext.id,
|
|
42
|
+
);
|
|
43
|
+
const heroKilledDragonHandler = await moduleRef.resolve(
|
|
44
|
+
ScopedHeroKilledDragonHandler,
|
|
45
|
+
asyncContext.id,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
killDragonExecuteSpy = jest.spyOn(killDragonHandler, 'execute');
|
|
49
|
+
heroKilledDragonHandleSpy = jest.spyOn(heroKilledDragonHandler, 'handle');
|
|
50
|
+
|
|
51
|
+
const commandBus = moduleRef.get(CommandBus);
|
|
52
|
+
const heroId = HERO_ID;
|
|
53
|
+
const dragonId = 'dragonId';
|
|
54
|
+
|
|
55
|
+
command = new ScopedKillDragonCommand(heroId, dragonId);
|
|
56
|
+
await commandBus.execute(command, asyncContext);
|
|
57
|
+
await waitImmediate();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should execute command handler', () => {
|
|
61
|
+
expect(killDragonExecuteSpy).toHaveBeenCalledWith(command);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle "ScopedHeroKilledDragonEvent" event', () => {
|
|
65
|
+
const event = new ScopedHeroKilledDragonEvent(
|
|
66
|
+
command.heroId,
|
|
67
|
+
command.dragonId,
|
|
68
|
+
);
|
|
69
|
+
expect(heroKilledDragonHandleSpy).toHaveBeenCalledWith(event);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('when saga triggered', () => {
|
|
73
|
+
let dropAncientExecuteSpy: jest.SpyInstance;
|
|
74
|
+
let heroFoundItemSpy: jest.SpyInstance;
|
|
75
|
+
|
|
76
|
+
beforeAll(async () => {
|
|
77
|
+
const dropAncientItemHandler = await moduleRef.resolve(
|
|
78
|
+
ScopedDropAncientItemHandler,
|
|
79
|
+
asyncContext.id,
|
|
80
|
+
);
|
|
81
|
+
const heroFoundItemHandler = await moduleRef.resolve(
|
|
82
|
+
ScopedHeroFoundItemHandler,
|
|
83
|
+
asyncContext.id,
|
|
84
|
+
);
|
|
85
|
+
dropAncientExecuteSpy = jest.spyOn(dropAncientItemHandler, 'execute');
|
|
86
|
+
heroFoundItemSpy = jest.spyOn(heroFoundItemHandler, 'handle');
|
|
87
|
+
|
|
88
|
+
// Wait 1 second for saga to be triggered
|
|
89
|
+
await setTimeout(1000);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should dispatch "ScopedDropAncientItemCommand" and execute its command handler', () => {
|
|
93
|
+
expect(dropAncientExecuteSpy).toHaveBeenCalledWith(
|
|
94
|
+
new ScopedDropAncientItemCommand(HERO_ID, ANCIENT_ITEM_ID),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle "ScopedHeroFoundItemEvent" event', () => {
|
|
99
|
+
expect(heroFoundItemSpy).toHaveBeenCalledWith(
|
|
100
|
+
new ScopedHeroFoundItemEvent(HERO_ID, ANCIENT_ITEM_ID),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('when "ScopedKillDragonCommand" command is dispatched and no async context is provided', () => {
|
|
107
|
+
const heroId = HERO_ID;
|
|
108
|
+
const dragonId = 'dragonId';
|
|
109
|
+
const command = new ScopedKillDragonCommand(heroId, dragonId);
|
|
110
|
+
|
|
111
|
+
it('should generate a unique async context', async () => {
|
|
112
|
+
const commandBus = moduleRef.get(CommandBus);
|
|
113
|
+
const result = await commandBus.execute(command);
|
|
114
|
+
expect(result).toEqual({ heroId, dragonId });
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('when "ScopedGetHeroesQuery" query is executed', () => {
|
|
119
|
+
it('should return all heroes', async () => {
|
|
120
|
+
const asyncContext = new AsyncContext();
|
|
121
|
+
const queryBus = moduleRef.get(QueryBus);
|
|
122
|
+
const heroes = await queryBus.execute(
|
|
123
|
+
new ScopedGetHeroesQuery(),
|
|
124
|
+
asyncContext,
|
|
125
|
+
);
|
|
126
|
+
expect(heroes).toEqual([expect.objectContaining({ id: HERO_ID })]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('when "ScopedGetHeroesQuery" query is executed and no async context is provided', () => {
|
|
131
|
+
it('should return all heroes', async () => {
|
|
132
|
+
const queryBus = moduleRef.get(QueryBus);
|
|
133
|
+
const heroes = await queryBus.execute(new ScopedGetHeroesQuery());
|
|
134
|
+
expect(heroes).toEqual([expect.objectContaining({ id: HERO_ID })]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
afterAll(async () => {
|
|
139
|
+
await moduleRef.close();
|
|
140
|
+
});
|
|
141
|
+
});
|