@nest-yalc-2/api-strategy 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +40 -0
- package/package.json +33 -0
- package/src/__tests__/call-strategy-wrappers.spec.ts +217 -0
- package/src/__tests__/context-call.service.spec.ts +52 -0
- package/src/__tests__/context-event.service.spec.ts +52 -0
- package/src/__tests__/event-strategy-composition.spec.ts +185 -0
- package/src/__tests__/header-whitelist.helper.spec.ts +30 -0
- package/src/__tests__/http-abstract-call.strategy.spec.ts +17 -0
- package/src/__tests__/nest-http-call.strategy.spec.ts +164 -0
- package/src/__tests__/nest-local-call.strategy.spec.ts +263 -0
- package/src/__tests__/nest-local-event.strategy.spec.ts +55 -0
- package/src/__tests__/rabbitmq-event.strategy.spec.ts +220 -0
- package/src/__tests__/strategy-selector.provider.spec.ts +182 -0
- package/src/context-call.interface.d.ts +12 -0
- package/src/context-call.interface.js +3 -0
- package/src/context-call.interface.js.map +1 -0
- package/src/context-call.interface.ts +49 -0
- package/src/context-call.service.d.ts +7 -0
- package/src/context-call.service.js +32 -0
- package/src/context-call.service.js.map +1 -0
- package/src/context-call.service.ts +31 -0
- package/src/context-event.interface.d.ts +4 -0
- package/src/context-event.interface.js +3 -0
- package/src/context-event.interface.js.map +1 -0
- package/src/context-event.interface.ts +4 -0
- package/src/context-event.service.d.ts +7 -0
- package/src/context-event.service.js +32 -0
- package/src/context-event.service.js.map +1 -0
- package/src/context-event.service.ts +31 -0
- package/src/header-whitelist.helper.d.ts +2 -0
- package/src/header-whitelist.helper.js +13 -0
- package/src/header-whitelist.helper.js.map +1 -0
- package/src/header-whitelist.helper.ts +12 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +32 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +17 -0
- package/src/strategies/composite-event.strategy.d.ts +14 -0
- package/src/strategies/composite-event.strategy.js +52 -0
- package/src/strategies/composite-event.strategy.js.map +1 -0
- package/src/strategies/composite-event.strategy.ts +76 -0
- package/src/strategies/conditional-call.strategy.d.ts +17 -0
- package/src/strategies/conditional-call.strategy.js +41 -0
- package/src/strategies/conditional-call.strategy.js.map +1 -0
- package/src/strategies/conditional-call.strategy.ts +70 -0
- package/src/strategies/conditional-event.strategy.d.ts +16 -0
- package/src/strategies/conditional-event.strategy.js +38 -0
- package/src/strategies/conditional-event.strategy.js.map +1 -0
- package/src/strategies/conditional-event.strategy.ts +54 -0
- package/src/strategies/fallback-call.strategy.d.ts +14 -0
- package/src/strategies/fallback-call.strategy.js +41 -0
- package/src/strategies/fallback-call.strategy.js.map +1 -0
- package/src/strategies/fallback-call.strategy.ts +67 -0
- package/src/strategies/http-abstract-call.strategy.d.ts +44 -0
- package/src/strategies/http-abstract-call.strategy.js +19 -0
- package/src/strategies/http-abstract-call.strategy.js.map +1 -0
- package/src/strategies/http-abstract-call.strategy.ts +238 -0
- package/src/strategies/nest-http-call.strategy.d.ts +30 -0
- package/src/strategies/nest-http-call.strategy.js +68 -0
- package/src/strategies/nest-http-call.strategy.js.map +1 -0
- package/src/strategies/nest-http-call.strategy.ts +121 -0
- package/src/strategies/nest-local-call.strategy.d.ts +32 -0
- package/src/strategies/nest-local-call.strategy.js +84 -0
- package/src/strategies/nest-local-call.strategy.js.map +1 -0
- package/src/strategies/nest-local-call.strategy.ts +153 -0
- package/src/strategies/nest-local-event.strategy.d.ts +17 -0
- package/src/strategies/nest-local-event.strategy.js +30 -0
- package/src/strategies/nest-local-event.strategy.js.map +1 -0
- package/src/strategies/nest-local-event.strategy.ts +42 -0
- package/src/strategies/rabbitmq-event.strategy.d.ts +34 -0
- package/src/strategies/rabbitmq-event.strategy.js +97 -0
- package/src/strategies/rabbitmq-event.strategy.js.map +1 -0
- package/src/strategies/rabbitmq-event.strategy.ts +154 -0
- package/src/strategies/shadow-call.strategy.d.ts +17 -0
- package/src/strategies/shadow-call.strategy.js +42 -0
- package/src/strategies/shadow-call.strategy.js.map +1 -0
- package/src/strategies/shadow-call.strategy.ts +87 -0
- package/src/strategy-selector.provider.d.ts +19 -0
- package/src/strategy-selector.provider.js +52 -0
- package/src/strategy-selector.provider.js.map +1 -0
- package/src/strategy-selector.provider.ts +103 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @nest-yalc-2/api-strategy
|
|
2
|
+
|
|
3
|
+
Transport strategy helpers for keeping application code stable while the
|
|
4
|
+
underlying call path changes.
|
|
5
|
+
|
|
6
|
+
Use this package to code against HTTP call or event interfaces while selecting
|
|
7
|
+
local Nest calls, remote HTTP calls, emitted events, fallback chains, shadow
|
|
8
|
+
calls, or conditional strategies through providers.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @nest-yalc-2/api-strategy
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Main Exports
|
|
17
|
+
|
|
18
|
+
- `NestLocalCallStrategy` for in-process Nest/Fastify calls.
|
|
19
|
+
- `NestHttpCallStrategy` and `HttpAbstractCallStrategy` for remote HTTP calls.
|
|
20
|
+
- `NestLocalEventStrategy` and `RabbitMqEventStrategy` for event emission.
|
|
21
|
+
- `FallbackCallStrategy`, `ShadowCallStrategy`, and conditional/composite
|
|
22
|
+
strategies for staged migrations and resilience.
|
|
23
|
+
- `ContextCallServiceFactory`, `ContextEventServiceFactory`, and strategy
|
|
24
|
+
selector providers for Nest DI wiring.
|
|
25
|
+
|
|
26
|
+
## Example
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { NestHttpCallStrategy } from '@nest-yalc-2/api-strategy';
|
|
30
|
+
|
|
31
|
+
const usersApi = new NestHttpCallStrategy(httpService, UserDto, baseUrl);
|
|
32
|
+
const users = await usersApi.get('/users');
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
- API Strategy guide:
|
|
38
|
+
https://github.com/Nek97/nestjs-yalc/blob/dev/docs/api-strategy.md
|
|
39
|
+
- API Strategy and EventManager patterns:
|
|
40
|
+
https://github.com/Nek97/nestjs-yalc/blob/dev/docs/api-strategy-event-manager-patterns.md
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nest-yalc-2/api-strategy",
|
|
3
|
+
"version": "1.3.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./*.js": "./src/*.js",
|
|
9
|
+
"./*": "./src/*"
|
|
10
|
+
},
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"main": "./src/index.js",
|
|
14
|
+
"types": "./src/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"tslib": "^2.6.3"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/Nek97/nestjs-yalc.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/Nek97/nestjs-yalc/issues"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/Nek97/nestjs-yalc#readme",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { jest, describe, expect, it } from "@jest/globals";
|
|
2
|
+
import type { IApiCallStrategy } from "../context-call.interface.js";
|
|
3
|
+
import { ConditionalCallStrategy } from "../strategies/conditional-call.strategy.js";
|
|
4
|
+
import { FallbackCallStrategy } from "../strategies/fallback-call.strategy.js";
|
|
5
|
+
import { ShadowCallStrategy } from "../strategies/shadow-call.strategy.js";
|
|
6
|
+
|
|
7
|
+
function callStrategy(overrides: Partial<IApiCallStrategy> = {}) {
|
|
8
|
+
return {
|
|
9
|
+
call: jest.fn(async () => ({ data: "call" })),
|
|
10
|
+
get: jest.fn(async () => ({ data: "get" })),
|
|
11
|
+
post: jest.fn(async () => ({ data: "post" })),
|
|
12
|
+
...overrides,
|
|
13
|
+
} satisfies IApiCallStrategy;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("ConditionalCallStrategy", () => {
|
|
17
|
+
it("delegates calls when enabled", async () => {
|
|
18
|
+
const target = callStrategy();
|
|
19
|
+
const strategy = new ConditionalCallStrategy(target, {
|
|
20
|
+
enabled: () => true,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await expect(strategy.call("/items")).resolves.toEqual({ data: "call" });
|
|
24
|
+
await expect(strategy.get("/items")).resolves.toEqual({ data: "get" });
|
|
25
|
+
await expect(strategy.post("/items")).resolves.toEqual({ data: "post" });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("delegates calls when no condition is configured", async () => {
|
|
29
|
+
const strategy = new ConditionalCallStrategy(callStrategy());
|
|
30
|
+
|
|
31
|
+
await expect(strategy.call("/items")).resolves.toEqual({ data: "call" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns a disabled response when disabled", async () => {
|
|
35
|
+
const target = callStrategy();
|
|
36
|
+
const strategy = new ConditionalCallStrategy(target, {
|
|
37
|
+
enabled: false,
|
|
38
|
+
disabledResponse: { data: "disabled" },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await expect(strategy.call("/items")).resolves.toEqual({
|
|
42
|
+
data: "disabled",
|
|
43
|
+
});
|
|
44
|
+
expect(target.call).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("throws the default disabled error", async () => {
|
|
48
|
+
const strategy = new ConditionalCallStrategy(callStrategy(), {
|
|
49
|
+
enabled: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await expect(strategy.get("/items")).rejects.toThrow("disabled");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("throws a custom disabled error", async () => {
|
|
56
|
+
const strategy = new ConditionalCallStrategy(callStrategy(), {
|
|
57
|
+
enabled: false,
|
|
58
|
+
disabledError: () => new Error("custom disabled"),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await expect(strategy.post("/items")).rejects.toThrow("custom disabled");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws a custom disabled error instance", async () => {
|
|
65
|
+
const strategy = new ConditionalCallStrategy(callStrategy(), {
|
|
66
|
+
enabled: false,
|
|
67
|
+
disabledError: new Error("disabled instance"),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await expect(strategy.call("/items")).rejects.toThrow("disabled instance");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("FallbackCallStrategy", () => {
|
|
75
|
+
it("requires at least one strategy", () => {
|
|
76
|
+
expect(() => new FallbackCallStrategy([])).toThrow("at least one");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("falls back to the next strategy after a failure", async () => {
|
|
80
|
+
const first = callStrategy({
|
|
81
|
+
call: jest.fn(async () => {
|
|
82
|
+
throw new Error("primary failed");
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
const second = callStrategy();
|
|
86
|
+
const strategy = new FallbackCallStrategy([first, second]);
|
|
87
|
+
|
|
88
|
+
await expect(strategy.call("/items")).resolves.toEqual({ data: "call" });
|
|
89
|
+
expect(first.call).toHaveBeenCalled();
|
|
90
|
+
expect(second.call).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("stops fallback when the predicate rejects it", async () => {
|
|
94
|
+
const error = new Error("do not retry");
|
|
95
|
+
const strategy = new FallbackCallStrategy(
|
|
96
|
+
[
|
|
97
|
+
callStrategy({
|
|
98
|
+
get: jest.fn(async () => {
|
|
99
|
+
throw error;
|
|
100
|
+
}),
|
|
101
|
+
}),
|
|
102
|
+
callStrategy(),
|
|
103
|
+
],
|
|
104
|
+
{
|
|
105
|
+
shouldFallback: () => false,
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await expect(strategy.get("/items")).rejects.toThrow(error);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("throws the last error after all strategies fail", async () => {
|
|
113
|
+
const strategy = new FallbackCallStrategy([
|
|
114
|
+
callStrategy({
|
|
115
|
+
post: jest.fn(async () => {
|
|
116
|
+
throw new Error("first failed");
|
|
117
|
+
}),
|
|
118
|
+
}),
|
|
119
|
+
callStrategy({
|
|
120
|
+
post: jest.fn(async () => {
|
|
121
|
+
throw new Error("last failed");
|
|
122
|
+
}),
|
|
123
|
+
}),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
await expect(strategy.post("/items")).rejects.toThrow("last failed");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("ShadowCallStrategy", () => {
|
|
131
|
+
it("requires at least one shadow strategy", () => {
|
|
132
|
+
expect(() => new ShadowCallStrategy(callStrategy(), [])).toThrow(
|
|
133
|
+
"at least one"
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns the primary response and triggers shadows", async () => {
|
|
138
|
+
const primary = callStrategy();
|
|
139
|
+
const shadow = callStrategy();
|
|
140
|
+
const strategy = new ShadowCallStrategy(primary, [shadow]);
|
|
141
|
+
|
|
142
|
+
await expect(strategy.call("/items")).resolves.toEqual({ data: "call" });
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
144
|
+
|
|
145
|
+
expect(primary.call).toHaveBeenCalled();
|
|
146
|
+
expect(shadow.call).toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("ignores fire-and-forget shadow failures", async () => {
|
|
150
|
+
const strategy = new ShadowCallStrategy(callStrategy(), [
|
|
151
|
+
callStrategy({
|
|
152
|
+
call: jest.fn(async () => {
|
|
153
|
+
throw new Error("shadow failed");
|
|
154
|
+
}),
|
|
155
|
+
}),
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
await expect(strategy.call("/items")).resolves.toEqual({ data: "call" });
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("throws shadow failures when configured to await and throw", async () => {
|
|
163
|
+
const strategy = new ShadowCallStrategy(
|
|
164
|
+
callStrategy(),
|
|
165
|
+
[
|
|
166
|
+
callStrategy({
|
|
167
|
+
get: jest.fn(async () => {
|
|
168
|
+
throw new Error("shadow failed");
|
|
169
|
+
}),
|
|
170
|
+
}),
|
|
171
|
+
],
|
|
172
|
+
{
|
|
173
|
+
awaitShadows: true,
|
|
174
|
+
shadowErrorMode: "throw",
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
await expect(strategy.get("/items")).rejects.toThrow("shadow failed");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("awaits and ignores shadow failures when configured", async () => {
|
|
182
|
+
const strategy = new ShadowCallStrategy(
|
|
183
|
+
callStrategy(),
|
|
184
|
+
[
|
|
185
|
+
callStrategy({
|
|
186
|
+
post: jest.fn(async () => {
|
|
187
|
+
throw new Error("shadow failed");
|
|
188
|
+
}),
|
|
189
|
+
}),
|
|
190
|
+
],
|
|
191
|
+
{
|
|
192
|
+
awaitShadows: true,
|
|
193
|
+
shadowErrorMode: "ignore",
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
await expect(strategy.post("/items")).resolves.toEqual({ data: "post" });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("awaits shadows automatically when throw mode is configured", async () => {
|
|
201
|
+
const strategy = new ShadowCallStrategy(
|
|
202
|
+
callStrategy(),
|
|
203
|
+
[
|
|
204
|
+
callStrategy({
|
|
205
|
+
call: jest.fn(async () => {
|
|
206
|
+
throw new Error("shadow failed");
|
|
207
|
+
}),
|
|
208
|
+
}),
|
|
209
|
+
],
|
|
210
|
+
{
|
|
211
|
+
shadowErrorMode: "throw",
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
await expect(strategy.call("/items")).rejects.toThrow("shadow failed");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expect,
|
|
3
|
+
jest,
|
|
4
|
+
describe,
|
|
5
|
+
it,
|
|
6
|
+
beforeEach,
|
|
7
|
+
beforeAll,
|
|
8
|
+
afterAll,
|
|
9
|
+
afterEach,
|
|
10
|
+
} from '@jest/globals';
|
|
11
|
+
|
|
12
|
+
import { createMock } from '@golevelup/ts-jest';
|
|
13
|
+
import { HttpAdapterHost } from '@nestjs/common';
|
|
14
|
+
import { ContextCallServiceFactory } from '../context-call.service.js';
|
|
15
|
+
import { NestLocalCallStrategy } from '../index.js';
|
|
16
|
+
|
|
17
|
+
describe('ContextCallServiceFactory', () => {
|
|
18
|
+
let httpAdapterHost: HttpAdapterHost;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
httpAdapterHost = createMock<HttpAdapterHost>();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should be defined', () => {
|
|
25
|
+
expect(ContextCallServiceFactory).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return a class', () => {
|
|
29
|
+
const ContextCallService = ContextCallServiceFactory(
|
|
30
|
+
new NestLocalCallStrategy(httpAdapterHost),
|
|
31
|
+
);
|
|
32
|
+
expect(ContextCallService).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should return a class that can be instantiated', () => {
|
|
36
|
+
const ContextCallService = ContextCallServiceFactory(
|
|
37
|
+
new NestLocalCallStrategy(httpAdapterHost),
|
|
38
|
+
);
|
|
39
|
+
const instance = new ContextCallService();
|
|
40
|
+
expect(instance).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return a class that can set and get a strategy', () => {
|
|
44
|
+
const ContextCallService = ContextCallServiceFactory(
|
|
45
|
+
new NestLocalCallStrategy(httpAdapterHost),
|
|
46
|
+
);
|
|
47
|
+
const instance = new ContextCallService();
|
|
48
|
+
const strategy = new NestLocalCallStrategy(httpAdapterHost);
|
|
49
|
+
instance.setStrategy(strategy);
|
|
50
|
+
expect(instance.getStrategy()).toBe(strategy);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expect,
|
|
3
|
+
jest,
|
|
4
|
+
describe,
|
|
5
|
+
it,
|
|
6
|
+
beforeEach,
|
|
7
|
+
beforeAll,
|
|
8
|
+
afterAll,
|
|
9
|
+
afterEach,
|
|
10
|
+
} from '@jest/globals';
|
|
11
|
+
|
|
12
|
+
import { createMock } from '@golevelup/ts-jest';
|
|
13
|
+
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
14
|
+
import { ContextEventServiceFactory } from '../context-event.service.js';
|
|
15
|
+
import { NestLocalEventStrategy } from '../index.js';
|
|
16
|
+
|
|
17
|
+
describe('ContextEventServiceFactory', () => {
|
|
18
|
+
let eventEmitter: EventEmitter2;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
eventEmitter = createMock<EventEmitter2>();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should be defined', () => {
|
|
25
|
+
expect(ContextEventServiceFactory).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return a class', () => {
|
|
29
|
+
const ContextCallService = ContextEventServiceFactory(
|
|
30
|
+
new NestLocalEventStrategy(eventEmitter),
|
|
31
|
+
);
|
|
32
|
+
expect(ContextCallService).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should return a class that can be instantiated', () => {
|
|
36
|
+
const ContextCallService = ContextEventServiceFactory(
|
|
37
|
+
new NestLocalEventStrategy(eventEmitter),
|
|
38
|
+
);
|
|
39
|
+
const instance = new ContextCallService();
|
|
40
|
+
expect(instance).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return a class that can set and get a strategy', () => {
|
|
44
|
+
const ContextCallService = ContextEventServiceFactory(
|
|
45
|
+
new NestLocalEventStrategy(eventEmitter),
|
|
46
|
+
);
|
|
47
|
+
const instance = new ContextCallService();
|
|
48
|
+
const strategy = new NestLocalEventStrategy(eventEmitter);
|
|
49
|
+
instance.setStrategy(strategy);
|
|
50
|
+
expect(instance.getStrategy()).toBe(strategy);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { jest, describe, expect, it } from "@jest/globals";
|
|
2
|
+
import type { IEventStrategy } from "../context-event.interface.js";
|
|
3
|
+
import { CompositeEventStrategy } from "../strategies/composite-event.strategy.js";
|
|
4
|
+
import { ConditionalEventStrategy } from "../strategies/conditional-event.strategy.js";
|
|
5
|
+
|
|
6
|
+
function eventStrategy(
|
|
7
|
+
overrides: Partial<IEventStrategy<object, boolean>> & {
|
|
8
|
+
onModuleDestroy?: () => Promise<void>;
|
|
9
|
+
} = {}
|
|
10
|
+
) {
|
|
11
|
+
return {
|
|
12
|
+
emit: jest.fn(() => true),
|
|
13
|
+
emitAsync: jest.fn(async () => true),
|
|
14
|
+
...overrides,
|
|
15
|
+
} satisfies IEventStrategy<object, boolean> & {
|
|
16
|
+
onModuleDestroy?: () => Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("CompositeEventStrategy", () => {
|
|
21
|
+
it("requires at least one strategy", () => {
|
|
22
|
+
expect(() => new CompositeEventStrategy([])).toThrow("at least one");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("emits to every strategy synchronously", () => {
|
|
26
|
+
const first = eventStrategy();
|
|
27
|
+
const second = eventStrategy({ emit: jest.fn(() => false) });
|
|
28
|
+
const strategy = new CompositeEventStrategy([first, second]);
|
|
29
|
+
|
|
30
|
+
const result = strategy.emit("event.name", { ok: true });
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual([true, false]);
|
|
33
|
+
expect(first.emit).toHaveBeenCalledWith(
|
|
34
|
+
"event.name",
|
|
35
|
+
{ ok: true },
|
|
36
|
+
undefined
|
|
37
|
+
);
|
|
38
|
+
expect(second.emit).toHaveBeenCalledWith(
|
|
39
|
+
"event.name",
|
|
40
|
+
{ ok: true },
|
|
41
|
+
undefined
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("throws sync errors by default", () => {
|
|
46
|
+
const error = new Error("sync failure");
|
|
47
|
+
const strategy = new CompositeEventStrategy([
|
|
48
|
+
eventStrategy({
|
|
49
|
+
emit: jest.fn(() => {
|
|
50
|
+
throw error;
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
expect(() => strategy.emit("event.name", {})).toThrow(error);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("ignores sync errors when configured", () => {
|
|
59
|
+
const strategy = new CompositeEventStrategy(
|
|
60
|
+
[
|
|
61
|
+
eventStrategy({
|
|
62
|
+
emit: jest.fn(() => {
|
|
63
|
+
throw new Error("sync failure");
|
|
64
|
+
}),
|
|
65
|
+
}),
|
|
66
|
+
eventStrategy(),
|
|
67
|
+
],
|
|
68
|
+
{ errorMode: "ignore" }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(strategy.emit("event.name", {})).toEqual([true]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("emits to every strategy asynchronously", async () => {
|
|
75
|
+
const first = eventStrategy();
|
|
76
|
+
const second = eventStrategy({ emitAsync: jest.fn(async () => false) });
|
|
77
|
+
const strategy = new CompositeEventStrategy([first, second]);
|
|
78
|
+
|
|
79
|
+
await expect(
|
|
80
|
+
strategy.emitAsync("event.name", { ok: true })
|
|
81
|
+
).resolves.toEqual([true, false]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("throws async errors by default", async () => {
|
|
85
|
+
const error = new Error("async failure");
|
|
86
|
+
const strategy = new CompositeEventStrategy([
|
|
87
|
+
eventStrategy({
|
|
88
|
+
emitAsync: jest.fn(async () => {
|
|
89
|
+
throw error;
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
await expect(strategy.emitAsync("event.name", {})).rejects.toThrow(error);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("ignores async errors when configured", async () => {
|
|
98
|
+
const strategy = new CompositeEventStrategy(
|
|
99
|
+
[
|
|
100
|
+
eventStrategy({
|
|
101
|
+
emitAsync: jest.fn(async () => {
|
|
102
|
+
throw new Error("async failure");
|
|
103
|
+
}),
|
|
104
|
+
}),
|
|
105
|
+
eventStrategy(),
|
|
106
|
+
],
|
|
107
|
+
{ errorMode: "ignore" }
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await expect(strategy.emitAsync("event.name", {})).resolves.toEqual([true]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("destroys nested strategies that implement module destroy", async () => {
|
|
114
|
+
const onModuleDestroy = jest.fn(async () => undefined);
|
|
115
|
+
const strategy = new CompositeEventStrategy([
|
|
116
|
+
eventStrategy({ onModuleDestroy }),
|
|
117
|
+
eventStrategy(),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
await strategy.onModuleDestroy();
|
|
121
|
+
|
|
122
|
+
expect(onModuleDestroy).toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("ConditionalEventStrategy", () => {
|
|
127
|
+
it("emits when enabled", () => {
|
|
128
|
+
const target = eventStrategy();
|
|
129
|
+
const strategy = new ConditionalEventStrategy(target, {
|
|
130
|
+
enabled: () => true,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(strategy.emit("event.name", {})).toBe(true);
|
|
134
|
+
expect(target.emit).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns the disabled result when disabled", () => {
|
|
138
|
+
const target = eventStrategy();
|
|
139
|
+
const strategy = new ConditionalEventStrategy(target, {
|
|
140
|
+
enabled: false,
|
|
141
|
+
disabledResult: false,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(strategy.emit("event.name", {})).toBe(false);
|
|
145
|
+
expect(target.emit).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("skips events rejected by the predicate", async () => {
|
|
149
|
+
const target = eventStrategy();
|
|
150
|
+
const strategy = new ConditionalEventStrategy(target, {
|
|
151
|
+
shouldEmit: () => false,
|
|
152
|
+
disabledResult: false,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await expect(strategy.emitAsync("event.name", {})).resolves.toBe(false);
|
|
156
|
+
expect(target.emitAsync).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("delegates async events when enabled and accepted", async () => {
|
|
160
|
+
const target = eventStrategy();
|
|
161
|
+
const strategy = new ConditionalEventStrategy(target, {
|
|
162
|
+
shouldEmit: () => true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await expect(strategy.emitAsync("event.name", {})).resolves.toBe(true);
|
|
166
|
+
expect(target.emitAsync).toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("destroys the wrapped strategy when supported", async () => {
|
|
170
|
+
const onModuleDestroy = jest.fn(async () => undefined);
|
|
171
|
+
const strategy = new ConditionalEventStrategy(
|
|
172
|
+
eventStrategy({ onModuleDestroy })
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await strategy.onModuleDestroy();
|
|
176
|
+
|
|
177
|
+
expect(onModuleDestroy).toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("ignores destroy when the wrapped strategy has no lifecycle hook", async () => {
|
|
181
|
+
const strategy = new ConditionalEventStrategy(eventStrategy());
|
|
182
|
+
|
|
183
|
+
await expect(strategy.onModuleDestroy()).resolves.toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
|
|
3
|
+
import { filterHeaders } from '../header-whitelist.helper.js';
|
|
4
|
+
|
|
5
|
+
describe('whitelistHeaders', () => {
|
|
6
|
+
it('should be defined', () => {
|
|
7
|
+
expect(filterHeaders).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should filter headers', () => {
|
|
11
|
+
const headers = {
|
|
12
|
+
'x-header': 'x-header',
|
|
13
|
+
'x-header-2': 'x-header-2',
|
|
14
|
+
'x-header-3': 'x-header-3',
|
|
15
|
+
};
|
|
16
|
+
const whitelist = ['x-header', 'x-header-2'];
|
|
17
|
+
const filteredHeaders = filterHeaders(headers, whitelist);
|
|
18
|
+
expect(filteredHeaders).toEqual({
|
|
19
|
+
'x-header': 'x-header',
|
|
20
|
+
'x-header-2': 'x-header-2',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return undefined if headers are undefined', () => {
|
|
25
|
+
const headers = undefined;
|
|
26
|
+
const whitelist = ['x-header', 'x-header-2'];
|
|
27
|
+
const filteredHeaders = filterHeaders(headers, whitelist);
|
|
28
|
+
expect(filteredHeaders).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expect,
|
|
3
|
+
jest,
|
|
4
|
+
describe,
|
|
5
|
+
it,
|
|
6
|
+
beforeEach,
|
|
7
|
+
beforeAll,
|
|
8
|
+
afterAll,
|
|
9
|
+
afterEach,
|
|
10
|
+
} from '@jest/globals';
|
|
11
|
+
import { HttpAbstractStrategy } from '../strategies/http-abstract-call.strategy.js';
|
|
12
|
+
|
|
13
|
+
describe('HttpAbstractStrategy', () => {
|
|
14
|
+
it('should be defined', () => {
|
|
15
|
+
expect(HttpAbstractStrategy).toBeDefined();
|
|
16
|
+
});
|
|
17
|
+
});
|