mirror of
https://github.com/nestjs/nest.git
synced 2026-02-22 23:41:40 +00:00
Compare commits
207 Commits
v10.4.3
...
feat/allow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab660c7edd | ||
|
|
21279a9c49 | ||
|
|
03a61e9728 | ||
|
|
212d07fcbf | ||
|
|
bdc9b4409a | ||
|
|
c66d1fb5f7 | ||
|
|
9356509274 | ||
|
|
129fe79f6b | ||
|
|
4f55f88bff | ||
|
|
397390083f | ||
|
|
bc31b5cff5 | ||
|
|
fb6025eb76 | ||
|
|
3267135f2d | ||
|
|
234d8d5600 | ||
|
|
774ccbe1b2 | ||
|
|
ad97a73ab2 | ||
|
|
00bb6309c1 | ||
|
|
c239f9a037 | ||
|
|
a669981c81 | ||
|
|
47941ba4d9 | ||
|
|
e89bb8e4a2 | ||
|
|
ca86bf6374 | ||
|
|
09a193337a | ||
|
|
33ef121d6b | ||
|
|
d2ac397115 | ||
|
|
9129e4f858 | ||
|
|
f37dafc132 | ||
|
|
3ad835c05d | ||
|
|
e965b4a141 | ||
|
|
feb2135373 | ||
|
|
9ea618e0a8 | ||
|
|
259b5dd828 | ||
|
|
66c3ab23ee | ||
|
|
f2b6cc0e93 | ||
|
|
f983f03665 | ||
|
|
9a48dc123e | ||
|
|
07adf37156 | ||
|
|
ea92f5fb3a | ||
|
|
3dfc7fc68e | ||
|
|
33e32d9d5d | ||
|
|
2ab99739cd | ||
|
|
9ae6a4c19f | ||
|
|
35846843e1 | ||
|
|
945d134b66 | ||
|
|
e231f9887e | ||
|
|
2874478b95 | ||
|
|
de13eec632 | ||
|
|
e9d57dd795 | ||
|
|
3cdaaf0bed | ||
|
|
676461ff4e | ||
|
|
cd7079bcc0 | ||
|
|
66c4810b0f | ||
|
|
23e602b712 | ||
|
|
e767bd364e | ||
|
|
bcda8fd791 | ||
|
|
fb32ef6d46 | ||
|
|
51a03bbcf2 | ||
|
|
f191f0f1fd | ||
|
|
84bf5706e2 | ||
|
|
03d8bcc21c | ||
|
|
e75eb1d14a | ||
|
|
790bafa46e | ||
|
|
f13e32ad33 | ||
|
|
1ed97489e3 | ||
|
|
7de7320550 | ||
|
|
3081f544f4 | ||
|
|
5bc7f91d90 | ||
|
|
6ea78d52fb | ||
|
|
38bfa9a13a | ||
|
|
f273041594 | ||
|
|
da8ebded25 | ||
|
|
34c2ccabe8 | ||
|
|
0f38439aa6 | ||
|
|
5cdea22c39 | ||
|
|
fd563c0ae9 | ||
|
|
9b690d95a2 | ||
|
|
6228af1444 | ||
|
|
8d0a3cd457 | ||
|
|
c63717db9d | ||
|
|
9cb84e78f8 | ||
|
|
5ccc88fe8a | ||
|
|
bc4667c15a | ||
|
|
a544552f72 | ||
|
|
954616da7d | ||
|
|
c6b95f54a7 | ||
|
|
af4e1d0a33 | ||
|
|
2c8982a2c7 | ||
|
|
7843db3984 | ||
|
|
42e0798d8c | ||
|
|
d9c8df7d40 | ||
|
|
025b32f342 | ||
|
|
c28f92e0c3 | ||
|
|
bc3ba4861d | ||
|
|
8996a10235 | ||
|
|
2c6eea08a9 | ||
|
|
f63e7adc80 | ||
|
|
4d3d39158a | ||
|
|
198eaf7e5c | ||
|
|
94a1ca14a9 | ||
|
|
ed644e9877 | ||
|
|
bb2f2c4bb5 | ||
|
|
0ea48d2d7a | ||
|
|
508d2f3428 | ||
|
|
7cf4708a05 | ||
|
|
8c2e03dcc9 | ||
|
|
6ada824161 | ||
|
|
d2ca9ec757 | ||
|
|
78b3f0c912 | ||
|
|
980eb5bb0a | ||
|
|
160c3b37d8 | ||
|
|
d0f401ab6e | ||
|
|
89840cee57 | ||
|
|
94237439c4 | ||
|
|
9ee022495a | ||
|
|
e1c1945a45 | ||
|
|
0d5ad26268 | ||
|
|
93514fdc70 | ||
|
|
a46a7dbe12 | ||
|
|
9825529f40 | ||
|
|
826ff4b426 | ||
|
|
fd740b61a6 | ||
|
|
c936deccad | ||
|
|
47cda92922 | ||
|
|
d70d0ba209 | ||
|
|
0fbc2a65d1 | ||
|
|
fa56e58847 | ||
|
|
44c4f8fa8d | ||
|
|
1460c6b468 | ||
|
|
a1336bf60c | ||
|
|
4d46bd8f75 | ||
|
|
5ae7e0f214 | ||
|
|
166d5840c1 | ||
|
|
03e122a273 | ||
|
|
0682d390cb | ||
|
|
3c24de6e1f | ||
|
|
091722e179 | ||
|
|
7dca5bdf42 | ||
|
|
f9c5140a47 | ||
|
|
29fca46d55 | ||
|
|
d378fff036 | ||
|
|
b389a536fb | ||
|
|
6ca325476a | ||
|
|
2409e3c48b | ||
|
|
43e374c07b | ||
|
|
fd3dddf57a | ||
|
|
a6e9c9306f | ||
|
|
50ad923137 | ||
|
|
725e958e5d | ||
|
|
a26fca197d | ||
|
|
bc36e3c32a | ||
|
|
fd5697dd79 | ||
|
|
bd71e16a84 | ||
|
|
203395ce73 | ||
|
|
093860ad2b | ||
|
|
2b3cfb6d22 | ||
|
|
3e00cc01e3 | ||
|
|
508619c391 | ||
|
|
fefb01ec8e | ||
|
|
325be0a257 | ||
|
|
74e81c0b3b | ||
|
|
6d0ff386e3 | ||
|
|
1b40f2c90e | ||
|
|
c27b43a992 | ||
|
|
9690825f97 | ||
|
|
e7282af96d | ||
|
|
f23184d0cd | ||
|
|
f9c2a11a39 | ||
|
|
9d9b03366f | ||
|
|
51ba90249d | ||
|
|
545061c0b2 | ||
|
|
336815dfea | ||
|
|
6581bcf6c2 | ||
|
|
3149bbca59 | ||
|
|
a77a55291d | ||
|
|
d68a7c1e55 | ||
|
|
b5e34f4cfc | ||
|
|
5f45310ab6 | ||
|
|
3c162055e0 | ||
|
|
53c15f8ecd | ||
|
|
8a55668746 | ||
|
|
02a6e98108 | ||
|
|
834a49ddf1 | ||
|
|
96bf1d9bb7 | ||
|
|
d20a1e580f | ||
|
|
09cfc8279f | ||
|
|
b90e19f15f | ||
|
|
aaebb6f278 | ||
|
|
fec6e82d52 | ||
|
|
b62b9255cf | ||
|
|
3fd4220052 | ||
|
|
7d5adfcc1f | ||
|
|
ef12d355ff | ||
|
|
52b2c72124 | ||
|
|
1ccc7f9d91 | ||
|
|
50e1d705c3 | ||
|
|
b3f85e9c04 | ||
|
|
c4a905b996 | ||
|
|
094c35cf79 | ||
|
|
30110d49e0 | ||
|
|
fb72a8ec9f | ||
|
|
0d7a768b89 | ||
|
|
ed8699769d | ||
|
|
0cbc15405d | ||
|
|
c47be164f3 | ||
|
|
61791e5115 | ||
|
|
2f2252264b | ||
|
|
d04886d816 |
@@ -6,8 +6,8 @@ cd "$(dirname "$0")"
|
||||
|
||||
cd /tmp/
|
||||
sudo apt-get install build-essential libssl-dev git -y
|
||||
git clone https://github.com/wg/wrk.git wrk
|
||||
git clone --depth=1 https://github.com/wg/wrk.git wrk
|
||||
cd wrk
|
||||
sudo make
|
||||
# move the executable to somewhere in your PATH, ex:
|
||||
sudo cp wrk /usr/local/bin
|
||||
sudo cp wrk /usr/local/bin
|
||||
|
||||
@@ -94,8 +94,9 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<td><a href="https://www.mercedes-benz.com/" target="_blank"><img src="https://nestjs.com/img/logos/mercedes-logo.png" width="100" valign="middle" /></a></td>
|
||||
<td><a href="https://www.dinii.jp/" target="_blank"><img src="https://nestjs.com/img/logos/dinii-logo.png" width="65" valign="middle" /></a></td>
|
||||
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
|
||||
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
|
||||
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-dark-logo.svg#2" width="150" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ describe('Hello world (express instance)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('/HEAD should respond to with a 200', () => {
|
||||
return request(server).head('/hello').expect(200);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -87,6 +87,15 @@ describe('Hello world (fastify adapter)', () => {
|
||||
.then(({ payload }) => expect(payload).to.be.eql('Hello world!'));
|
||||
});
|
||||
|
||||
it('/HEAD should respond to with a 200', () => {
|
||||
return app
|
||||
.inject({
|
||||
method: 'HEAD',
|
||||
url: '/hello',
|
||||
})
|
||||
.then(({ statusCode }) => expect(statusCode).to.be.eq(200));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -43,10 +43,7 @@ describe('OnModuleDestroy', () => {
|
||||
it('should sort modules by distance (topological sort) - DESC order', async () => {
|
||||
@Injectable()
|
||||
class BB implements OnModuleDestroy {
|
||||
public field: string;
|
||||
async onModuleDestroy() {
|
||||
this.field = 'b-field';
|
||||
}
|
||||
onModuleDestroy = Sinon.spy();
|
||||
}
|
||||
|
||||
@Module({
|
||||
@@ -57,13 +54,10 @@ describe('OnModuleDestroy', () => {
|
||||
|
||||
@Injectable()
|
||||
class AA implements OnModuleDestroy {
|
||||
public field: string;
|
||||
constructor(private bb: BB) {}
|
||||
|
||||
async onModuleDestroy() {
|
||||
this.field = this.bb.field + '_a-field';
|
||||
}
|
||||
onModuleDestroy = Sinon.spy();
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [B],
|
||||
providers: [AA],
|
||||
@@ -78,7 +72,8 @@ describe('OnModuleDestroy', () => {
|
||||
await app.init();
|
||||
await app.close();
|
||||
|
||||
const instance = module.get(AA);
|
||||
expect(instance.field).to.equal('b-field_a-field');
|
||||
const aa = module.get(AA);
|
||||
const bb = module.get(BB);
|
||||
Sinon.assert.callOrder(aa.onModuleDestroy, bb.onModuleDestroy);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,11 +39,39 @@ describe('OnModuleInit', () => {
|
||||
});
|
||||
|
||||
it('should sort modules by distance (topological sort) - DESC order', async () => {
|
||||
@Injectable()
|
||||
class CC implements OnModuleInit {
|
||||
public field: string;
|
||||
|
||||
async onModuleInit() {
|
||||
this.field = 'c-field';
|
||||
}
|
||||
}
|
||||
|
||||
@Module({})
|
||||
class C {
|
||||
static forRoot() {
|
||||
return {
|
||||
module: C,
|
||||
global: true,
|
||||
providers: [
|
||||
{
|
||||
provide: CC,
|
||||
useValue: new CC(),
|
||||
},
|
||||
],
|
||||
exports: [CC],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class BB implements OnModuleInit {
|
||||
public field: string;
|
||||
constructor(private cc: CC) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.field = 'b-field';
|
||||
this.field = this.cc.field + '_b-field';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,14 +96,19 @@ describe('OnModuleInit', () => {
|
||||
})
|
||||
class A {}
|
||||
|
||||
@Module({
|
||||
imports: [A, C.forRoot()],
|
||||
})
|
||||
class AppModule {}
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [A],
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
const app = module.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
const instance = module.get(AA);
|
||||
expect(instance.field).to.equal('b-field_a-field');
|
||||
expect(instance.field).to.equal('c-field_b-field_a-field');
|
||||
});
|
||||
});
|
||||
|
||||
98
integration/microservices/e2e/sum-rpc-async.spec.ts
Normal file
98
integration/microservices/e2e/sum-rpc-async.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
Controller,
|
||||
INestMicroservice,
|
||||
Injectable,
|
||||
Module,
|
||||
} from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import {
|
||||
AsyncOptions,
|
||||
ClientTCP,
|
||||
ClientsModule,
|
||||
MessagePattern,
|
||||
MicroserviceOptions,
|
||||
Payload,
|
||||
TcpOptions,
|
||||
Transport,
|
||||
} from '@nestjs/microservices';
|
||||
import { expect } from 'chai';
|
||||
|
||||
let port: number;
|
||||
|
||||
do {
|
||||
port = Math.round(Math.random() * 10000);
|
||||
} while (port < 1000);
|
||||
|
||||
@Injectable()
|
||||
class RpcOptionsProvider {
|
||||
getOptions(): TcpOptions {
|
||||
return {
|
||||
transport: Transport.TCP,
|
||||
options: {
|
||||
port,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Controller()
|
||||
class RpcController {
|
||||
@MessagePattern({ cmd: 'sum' })
|
||||
sumPayload(@Payload() payload: number[]) {
|
||||
return payload.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ClientsModule.register([
|
||||
{
|
||||
name: 'RPC_CLIENT',
|
||||
transport: Transport.TCP,
|
||||
options: {
|
||||
port,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
controllers: [RpcController],
|
||||
providers: [RpcOptionsProvider],
|
||||
})
|
||||
class RpcModule {}
|
||||
|
||||
describe('RPC Async transport', () => {
|
||||
let app: INestMicroservice;
|
||||
let client: ClientTCP;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await NestFactory.createMicroservice<
|
||||
AsyncOptions<MicroserviceOptions>
|
||||
>(RpcModule, {
|
||||
logger: false,
|
||||
inject: [RpcOptionsProvider],
|
||||
useFactory: (optionsProvider: RpcOptionsProvider) =>
|
||||
optionsProvider.getOptions(),
|
||||
});
|
||||
|
||||
await app.listen();
|
||||
client = app.get('RPC_CLIENT', { strict: false });
|
||||
});
|
||||
|
||||
it(`/POST`, done => {
|
||||
let retData = 0;
|
||||
client.send({ cmd: 'sum' }, [1, 2, 3, 4, 5]).subscribe({
|
||||
next: val => (retData += val),
|
||||
error: done,
|
||||
complete: () => {
|
||||
expect(retData).to.eq(15);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Module, Injectable } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { Injectable, Module } from '@nestjs/common';
|
||||
import {
|
||||
ClientsModule,
|
||||
Transport,
|
||||
ClientsModuleOptionsFactory,
|
||||
ClientOptions,
|
||||
ClientsModule,
|
||||
ClientsModuleOptionsFactory,
|
||||
ClientTCP,
|
||||
RpcException,
|
||||
Transport,
|
||||
} from '@nestjs/microservices';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -218,6 +218,61 @@ describe('WebSocketGateway (WsAdapter)', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should set messageParser by using setMessageParser method', async () => {
|
||||
const testingModule = await Test.createTestingModule({
|
||||
providers: [ApplicationGateway],
|
||||
}).compile();
|
||||
app = testingModule.createNestApplication();
|
||||
|
||||
const wsAdapter = new WsAdapter(app);
|
||||
wsAdapter.setMessageParser(data => {
|
||||
const [event, payload] = JSON.parse(data.toString());
|
||||
return { event, data: payload };
|
||||
});
|
||||
app.useWebSocketAdapter(wsAdapter);
|
||||
await app.listen(3000);
|
||||
|
||||
ws = new WebSocket('ws://localhost:8080');
|
||||
await new Promise(resolve => ws.on('open', resolve));
|
||||
|
||||
ws.send(JSON.stringify(['push', { test: 'test' }]));
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
ws.close();
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set messageParser by using constructor options', async () => {
|
||||
const testingModule = await Test.createTestingModule({
|
||||
providers: [ApplicationGateway],
|
||||
}).compile();
|
||||
app = testingModule.createNestApplication();
|
||||
|
||||
const wsAdapter = new WsAdapter(app, {
|
||||
messageParser: data => {
|
||||
const [event, payload] = JSON.parse(data.toString());
|
||||
return { event, data: payload };
|
||||
},
|
||||
});
|
||||
app.useWebSocketAdapter(wsAdapter);
|
||||
await app.listen(3000);
|
||||
|
||||
ws = new WebSocket('ws://localhost:8080');
|
||||
await new Promise(resolve => ws.on('open', resolve));
|
||||
|
||||
ws.send(JSON.stringify(['push', { test: 'test' }]));
|
||||
await new Promise<void>(resolve =>
|
||||
ws.on('message', data => {
|
||||
expect(JSON.parse(data).data.test).to.be.eql('test');
|
||||
ws.close();
|
||||
resolve();
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "10.4.3"
|
||||
"version": "10.4.7"
|
||||
}
|
||||
|
||||
1297
package-lock.json
generated
1297
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/core",
|
||||
"version": "10.3.10",
|
||||
"version": "10.4.5",
|
||||
"description": "Modern, fast, powerful node.js web framework",
|
||||
"homepage": "https://nestjs.com",
|
||||
"repository": {
|
||||
@@ -19,7 +19,7 @@
|
||||
"build:dev": "tsc -b -v packages --watch",
|
||||
"prebuild:prod": "npm run clean",
|
||||
"build:prod": "tsc -b -v packages",
|
||||
"changelog": "lerna-changelog --from=v10.4.1",
|
||||
"changelog": "lerna-changelog",
|
||||
"clean": "tsc -b --clean packages",
|
||||
"move:samples": "gulp move:samples",
|
||||
"move:node_modules": "gulp move:node_modules",
|
||||
@@ -63,7 +63,7 @@
|
||||
"class-validator": "0.14.1",
|
||||
"cli-color": "2.0.4",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.21.0",
|
||||
"express": "4.21.1",
|
||||
"fast-json-stringify": "6.0.0",
|
||||
"fast-safe-stringify": "2.1.1",
|
||||
"graphql-subscriptions": "2.0.0",
|
||||
@@ -72,7 +72,7 @@
|
||||
"path-to-regexp": "3.2.0",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rxjs": "7.8.1",
|
||||
"socket.io": "4.7.5",
|
||||
"socket.io": "4.8.0",
|
||||
"tslib": "2.7.0",
|
||||
"uid": "2.0.2",
|
||||
"uuid": "10.0.0"
|
||||
@@ -102,7 +102,7 @@
|
||||
"@types/express": "4.17.21",
|
||||
"@types/gulp": "4.0.17",
|
||||
"@types/http-errors": "2.0.4",
|
||||
"@types/mocha": "10.0.7",
|
||||
"@types/mocha": "10.0.8",
|
||||
"@types/node": "22.5.5",
|
||||
"@types/sinon": "17.0.3",
|
||||
"@types/supertest": "2.0.16",
|
||||
@@ -119,12 +119,12 @@
|
||||
"chai": "4.5.0",
|
||||
"chai-as-promised": "7.1.2",
|
||||
"clang-format": "1.8.0",
|
||||
"concurrently": "8.2.2",
|
||||
"concurrently": "9.0.1",
|
||||
"conventional-changelog": "6.0.0",
|
||||
"core-js": "3.38.1",
|
||||
"coveralls": "3.1.1",
|
||||
"delete-empty": "3.0.0",
|
||||
"engine.io-client": "6.6.0",
|
||||
"engine.io-client": "6.6.1",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
@@ -147,7 +147,7 @@
|
||||
"kafkajs": "2.2.4",
|
||||
"lerna": "2.11.0",
|
||||
"lerna-changelog": "2.2.0",
|
||||
"light-my-request": "6.0.0",
|
||||
"light-my-request": "6.1.0",
|
||||
"lint-staged": "15.2.10",
|
||||
"markdown-table": "2.0.0",
|
||||
"mocha": "10.7.3",
|
||||
@@ -156,14 +156,14 @@
|
||||
"multer": "1.4.4",
|
||||
"mysql2": "3.11.3",
|
||||
"nats": "2.28.2",
|
||||
"nodemon": "3.1.4",
|
||||
"nodemon": "3.1.5",
|
||||
"nyc": "14.1.1",
|
||||
"prettier": "3.3.3",
|
||||
"redis": "4.7.0",
|
||||
"rxjs-compat": "6.6.7",
|
||||
"sinon": "18.0.0",
|
||||
"sinon": "19.0.2",
|
||||
"sinon-chai": "3.7.0",
|
||||
"socket.io-client": "4.7.5",
|
||||
"socket.io-client": "4.8.0",
|
||||
"subscriptions-transport-ws": "0.11.0",
|
||||
"supertest": "7.0.0",
|
||||
"ts-morph": "23.0.0",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circfleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
@@ -96,6 +96,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
|
||||
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
PROPERTY_DEPS_METADATA,
|
||||
SELF_DECLARED_DEPS_METADATA,
|
||||
} from '../../constants';
|
||||
import { ForwardReference, InjectionToken } from '../../interfaces';
|
||||
import { isUndefined } from '../../utils/shared.utils';
|
||||
|
||||
/**
|
||||
@@ -34,8 +35,8 @@ import { isUndefined } from '../../utils/shared.utils';
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export function Inject<T = any>(
|
||||
token?: T,
|
||||
export function Inject(
|
||||
token?: InjectionToken | ForwardReference,
|
||||
): PropertyDecorator & ParameterDecorator {
|
||||
const injectCallHasArguments = arguments.length > 0;
|
||||
|
||||
|
||||
@@ -16,12 +16,8 @@ export type ParamDecoratorEnhancer = ParameterDecorator;
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export function createParamDecorator<
|
||||
FactoryData = any,
|
||||
FactoryInput = any,
|
||||
FactoryOutput = any,
|
||||
>(
|
||||
factory: CustomParamFactory<FactoryData, FactoryInput, FactoryOutput>,
|
||||
export function createParamDecorator<FactoryData = any, FactoryOutput = any>(
|
||||
factory: CustomParamFactory<FactoryData, FactoryOutput>,
|
||||
enhancers: ParamDecoratorEnhancer[] = [],
|
||||
): (
|
||||
...dataOrPipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]
|
||||
|
||||
@@ -118,3 +118,66 @@ export const All = createMappingDecorator(RequestMethod.ALL);
|
||||
* @publicApi
|
||||
*/
|
||||
export const Search = createMappingDecorator(RequestMethod.SEARCH);
|
||||
|
||||
/**
|
||||
* Route handler (method) Decorator. Routes Webdav PROPFIND requests to the specified path.
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export const Propfind = createMappingDecorator(RequestMethod.PROPFIND);
|
||||
|
||||
/**
|
||||
* Route handler (method) Decorator. Routes Webdav PROPPATCH requests to the specified path.
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export const Proppatch = createMappingDecorator(RequestMethod.PROPPATCH);
|
||||
|
||||
/**
|
||||
* Route handler (method) Decorator. Routes Webdav MKCOL requests to the specified path.
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export const Mkcol = createMappingDecorator(RequestMethod.MKCOL);
|
||||
|
||||
/**
|
||||
* Route handler (method) Decorator. Routes Webdav COPY requests to the specified path.
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export const Copy = createMappingDecorator(RequestMethod.COPY);
|
||||
|
||||
/**
|
||||
* Route handler (method) Decorator. Routes Webdav MOVE requests to the specified path.
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export const Move = createMappingDecorator(RequestMethod.MOVE);
|
||||
|
||||
/**
|
||||
* Route handler (method) Decorator. Routes Webdav LOCK requests to the specified path.
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export const Lock = createMappingDecorator(RequestMethod.LOCK);
|
||||
|
||||
/**
|
||||
* Route handler (method) Decorator. Routes Webdav UNLOCK requests to the specified path.
|
||||
*
|
||||
* @see [Routing](https://docs.nestjs.com/controllers#routing)
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export const Unlock = createMappingDecorator(RequestMethod.UNLOCK);
|
||||
|
||||
@@ -13,6 +13,9 @@ export enum HttpStatus {
|
||||
NO_CONTENT = 204,
|
||||
RESET_CONTENT = 205,
|
||||
PARTIAL_CONTENT = 206,
|
||||
MULTI_STATUS = 207,
|
||||
ALREADY_REPORTED = 208,
|
||||
CONTENT_DIFFERENT = 210,
|
||||
AMBIGUOUS = 300,
|
||||
MOVED_PERMANENTLY = 301,
|
||||
FOUND = 302,
|
||||
@@ -41,13 +44,17 @@ export enum HttpStatus {
|
||||
I_AM_A_TEAPOT = 418,
|
||||
MISDIRECTED = 421,
|
||||
UNPROCESSABLE_ENTITY = 422,
|
||||
LOCKED = 423,
|
||||
FAILED_DEPENDENCY = 424,
|
||||
PRECONDITION_REQUIRED = 428,
|
||||
TOO_MANY_REQUESTS = 429,
|
||||
UNRECOVERABLE_ERROR = 456,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
NOT_IMPLEMENTED = 501,
|
||||
BAD_GATEWAY = 502,
|
||||
SERVICE_UNAVAILABLE = 503,
|
||||
GATEWAY_TIMEOUT = 504,
|
||||
HTTP_VERSION_NOT_SUPPORTED = 505,
|
||||
INSUFFICIENT_STORAGE = 507,
|
||||
LOOP_DETECTED = 508,
|
||||
}
|
||||
|
||||
@@ -8,4 +8,11 @@ export enum RequestMethod {
|
||||
OPTIONS,
|
||||
HEAD,
|
||||
SEARCH,
|
||||
PROPFIND,
|
||||
PROPPATCH,
|
||||
MKCOL,
|
||||
COPY,
|
||||
MOVE,
|
||||
LOCK,
|
||||
UNLOCK,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
HttpExceptionBody,
|
||||
HttpExceptionBodyMessage,
|
||||
} from '../interfaces/http/http-exception-body.interface';
|
||||
import { isObject, isString } from '../utils/shared.utils';
|
||||
import { isNumber, isObject, isString } from '../utils/shared.utils';
|
||||
|
||||
export interface HttpExceptionOptions {
|
||||
/** original cause of the error */
|
||||
@@ -115,17 +115,14 @@ export class HttpException extends Error {
|
||||
message: HttpExceptionBodyMessage,
|
||||
statusCode: number,
|
||||
): HttpExceptionBody;
|
||||
|
||||
public static createBody(
|
||||
message: HttpExceptionBodyMessage,
|
||||
error: string,
|
||||
statusCode: number,
|
||||
): HttpExceptionBody;
|
||||
|
||||
public static createBody<Body extends Record<string, unknown>>(
|
||||
custom: Body,
|
||||
): Body;
|
||||
|
||||
public static createBody<Body extends Record<string, unknown>>(
|
||||
arg0: null | HttpExceptionBodyMessage | Body,
|
||||
arg1?: HttpExceptionBodyMessage | string,
|
||||
@@ -138,7 +135,7 @@ export class HttpException extends Error {
|
||||
};
|
||||
}
|
||||
|
||||
if (isString(arg0) || Array.isArray(arg0)) {
|
||||
if (isString(arg0) || Array.isArray(arg0) || isNumber(arg0)) {
|
||||
return {
|
||||
message: arg0,
|
||||
error: arg1 as string,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Readable } from 'stream';
|
||||
import { types } from 'util';
|
||||
import { HttpStatus } from '../enums';
|
||||
import { Logger } from '../services';
|
||||
import { isFunction } from '../utils/shared.utils';
|
||||
import { StreamableFileOptions, StreamableHandlerResponse } from './interfaces';
|
||||
import { Logger } from '../services';
|
||||
|
||||
/**
|
||||
* @see [Streaming files](https://docs.nestjs.com/techniques/streaming-files)
|
||||
@@ -31,7 +31,7 @@ export class StreamableFile {
|
||||
};
|
||||
|
||||
protected logError: (err: Error) => void = (err: Error) => {
|
||||
this.logger.error(err.message, err.stack);
|
||||
this.logger.error(err);
|
||||
};
|
||||
|
||||
constructor(buffer: Uint8Array, options?: StreamableFileOptions);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Type } from '../type.interface';
|
||||
import { ClassTransformOptions } from './class-transform-options.interface';
|
||||
|
||||
export interface TransformerPackage {
|
||||
plainToClass<T>(
|
||||
plainToInstance<T>(
|
||||
cls: Type<T>,
|
||||
plain: unknown,
|
||||
options?: ClassTransformOptions,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ExecutionContext } from './execution-context.interface';
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export type CustomParamFactory<TData = any, TInput = any, TOutput = any> = (
|
||||
export type CustomParamFactory<TData = any, TOutput = any> = (
|
||||
data: TData,
|
||||
input: TInput,
|
||||
context: ExecutionContext,
|
||||
) => TOutput;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type HttpExceptionBodyMessage = string | string[];
|
||||
export type HttpExceptionBodyMessage = string | string[] | number;
|
||||
|
||||
export interface HttpExceptionBody {
|
||||
message: HttpExceptionBodyMessage;
|
||||
|
||||
@@ -47,6 +47,20 @@ export interface HttpServer<
|
||||
put(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
patch(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
patch(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
propfind?(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
propfind?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
proppatch?(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
proppatch?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
mkcol?(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
mkcol?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
copy?(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
copy?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
move?(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
move?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
lock?(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
lock?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
unlock?(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
unlock?(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
all(path: string, handler: RequestHandler<TRequest, TResponse>): any;
|
||||
all(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
options(handler: RequestHandler<TRequest, TResponse>): any;
|
||||
|
||||
@@ -35,7 +35,6 @@ export interface ModuleMetadata {
|
||||
*/
|
||||
exports?: Array<
|
||||
| DynamicModule
|
||||
| Promise<DynamicModule>
|
||||
| string
|
||||
| symbol
|
||||
| Provider
|
||||
|
||||
@@ -44,4 +44,13 @@ export class NestApplicationContextOptions {
|
||||
* @default false
|
||||
*/
|
||||
snapshot?: boolean;
|
||||
|
||||
/**
|
||||
* Determines what algorithm use to generate module ids.
|
||||
* When set to `deep-hash`, the module id is generated based on the serialized module definition.
|
||||
* When set to `reference`, each module obtains a unique id based on its reference.
|
||||
*
|
||||
* @default 'reference'
|
||||
*/
|
||||
moduleIdGeneratorAlgorithm?: 'deep-hash' | 'reference';
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { ShutdownSignal } from '../enums/shutdown-signal.enum';
|
||||
import { LoggerService, LogLevel } from '../services/logger.service';
|
||||
import { DynamicModule } from './modules';
|
||||
import { NestApplicationContextOptions } from './nest-application-context-options.interface';
|
||||
import { Type } from './type.interface';
|
||||
|
||||
export type SelectOptions = Pick<NestApplicationContextOptions, 'abortOnError'>;
|
||||
|
||||
export interface GetOrResolveOptions {
|
||||
/**
|
||||
* If enabled, lookup will only be performed in the host module.
|
||||
@@ -27,7 +30,10 @@ export interface INestApplicationContext {
|
||||
* Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module.
|
||||
* @returns {INestApplicationContext}
|
||||
*/
|
||||
select<T>(module: Type<T> | DynamicModule): INestApplicationContext;
|
||||
select<T>(
|
||||
module: Type<T> | DynamicModule,
|
||||
options?: SelectOptions,
|
||||
): INestApplicationContext;
|
||||
|
||||
/**
|
||||
* Retrieves an instance of either injectable or controller, otherwise, throws exception.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { ExceptionFilter } from './exceptions/exception-filter.interface';
|
||||
import { CanActivate } from './features/can-activate.interface';
|
||||
import { NestInterceptor } from './features/nest-interceptor.interface';
|
||||
@@ -19,8 +20,8 @@ export interface INestMicroservice extends INestApplicationContext {
|
||||
listen(): Promise<any>;
|
||||
|
||||
/**
|
||||
* Register Ws Adapter which will be used inside Gateways.
|
||||
* Use when you want to override default `socket.io` library.
|
||||
* Registers a web socket adapter that will be used for Gateways.
|
||||
* Use to override the default `socket.io` library.
|
||||
*
|
||||
* @param {WebSocketAdapter} adapter
|
||||
* @returns {this}
|
||||
@@ -28,37 +29,64 @@ export interface INestMicroservice extends INestApplicationContext {
|
||||
useWebSocketAdapter(adapter: WebSocketAdapter): this;
|
||||
|
||||
/**
|
||||
* Registers exception filters as global filters (will be used within every message pattern handler)
|
||||
* Registers global exception filters (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...ExceptionFilter} filters
|
||||
*/
|
||||
useGlobalFilters(...filters: ExceptionFilter[]): this;
|
||||
|
||||
/**
|
||||
* Registers pipes as global pipes (will be used within every message pattern handler)
|
||||
* Registers global pipes (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...PipeTransform} pipes
|
||||
*/
|
||||
useGlobalPipes(...pipes: PipeTransform<any>[]): this;
|
||||
|
||||
/**
|
||||
* Registers interceptors as global interceptors (will be used within every message pattern handler)
|
||||
* Registers global interceptors (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...NestInterceptor} interceptors
|
||||
*/
|
||||
useGlobalInterceptors(...interceptors: NestInterceptor[]): this;
|
||||
|
||||
/**
|
||||
* Registers guards as global guards (will be used within every message pattern handler)
|
||||
* Registers global guards (will be used for every pattern handler).
|
||||
*
|
||||
* @param {...CanActivate} guards
|
||||
*/
|
||||
useGlobalGuards(...guards: CanActivate[]): this;
|
||||
|
||||
/**
|
||||
* Terminates the application
|
||||
* Terminates the application.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns an observable that emits status changes.
|
||||
*
|
||||
* @returns {Observable<string>}
|
||||
*/
|
||||
status: Observable<string>;
|
||||
|
||||
/**
|
||||
* Registers an event listener for the given event.
|
||||
* @param event Event name
|
||||
* @param callback Callback to be executed when the event is emitted
|
||||
*/
|
||||
on<
|
||||
EventsMap extends Record<string, Function> = Record<string, Function>,
|
||||
EventKey extends keyof EventsMap = keyof EventsMap,
|
||||
EventCallback extends EventsMap[EventKey] = EventsMap[EventKey],
|
||||
>(
|
||||
event: EventKey,
|
||||
callback: EventCallback,
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Returns an instance of the underlying server/broker instance,
|
||||
* or a group of servers if there are more than one.
|
||||
*/
|
||||
unwrap<T>(): T;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/common",
|
||||
"version": "10.4.3",
|
||||
"version": "10.4.7",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"homepage": "https://nestjs.com",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
export * from './default-value.pipe';
|
||||
export * from './file';
|
||||
export * from './parse-array.pipe';
|
||||
export * from './parse-bool.pipe';
|
||||
export * from './parse-int.pipe';
|
||||
export * from './parse-float.pipe';
|
||||
export * from './parse-date.pipe';
|
||||
export * from './parse-enum.pipe';
|
||||
export * from './parse-float.pipe';
|
||||
export * from './parse-int.pipe';
|
||||
export * from './parse-uuid.pipe';
|
||||
export * from './validation.pipe';
|
||||
export * from './file';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
PipeTransform,
|
||||
} from '../interfaces/features/pipe-transform.interface';
|
||||
import { HttpErrorByCode } from '../utils/http-error-by-code.util';
|
||||
import { isNil, isUndefined, isString } from '../utils/shared.utils';
|
||||
import { isNil, isString, isUndefined } from '../utils/shared.utils';
|
||||
import { ValidationPipe, ValidationPipeOptions } from './validation.pipe';
|
||||
|
||||
const VALIDATION_ERROR_MESSAGE = 'Validation failed (parsable array expected)';
|
||||
@@ -21,9 +21,26 @@ export interface ParseArrayOptions
|
||||
ValidationPipeOptions,
|
||||
'transform' | 'validateCustomDecorators' | 'exceptionFactory'
|
||||
> {
|
||||
/**
|
||||
* Type for items to be converted into
|
||||
*/
|
||||
items?: Type<unknown>;
|
||||
/**
|
||||
* Items separator to split string by
|
||||
* @default ','
|
||||
*/
|
||||
separator?: string;
|
||||
/**
|
||||
* If true, the pipe will return null or undefined if the value is not provided
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
/**
|
||||
* A factory function that returns an exception object to be thrown
|
||||
* if validation fails.
|
||||
* @param error Error message or object
|
||||
* @returns The exception object
|
||||
*/
|
||||
exceptionFactory?: (error: any) => any;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,21 @@ import { isNil } from '../utils/shared.utils';
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ParseBoolPipeOptions {
|
||||
/**
|
||||
* The HTTP status code to be used in the response when the validation fails.
|
||||
*/
|
||||
errorHttpStatusCode?: ErrorHttpStatusCode;
|
||||
/**
|
||||
* A factory function that returns an exception object to be thrown
|
||||
* if validation fails.
|
||||
* @param error Error message
|
||||
* @returns The exception object
|
||||
*/
|
||||
exceptionFactory?: (error: string) => any;
|
||||
/**
|
||||
* If true, the pipe will return null or undefined if the value is not provided
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
|
||||
74
packages/common/pipes/parse-date.pipe.ts
Normal file
74
packages/common/pipes/parse-date.pipe.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Injectable } from '../decorators/core/injectable.decorator';
|
||||
import { HttpStatus } from '../enums/http-status.enum';
|
||||
import { PipeTransform } from '../interfaces/features/pipe-transform.interface';
|
||||
import {
|
||||
ErrorHttpStatusCode,
|
||||
HttpErrorByCode,
|
||||
} from '../utils/http-error-by-code.util';
|
||||
import { isNil } from '../utils/shared.utils';
|
||||
|
||||
export interface ParseDatePipeOptions {
|
||||
/**
|
||||
* If true, the pipe will return null or undefined if the value is not provided
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
/**
|
||||
* Default value for the date
|
||||
*/
|
||||
default?: () => Date;
|
||||
/**
|
||||
* The HTTP status code to be used in the response when the validation fails.
|
||||
*/
|
||||
errorHttpStatusCode?: ErrorHttpStatusCode;
|
||||
/**
|
||||
* A factory function that returns an exception object to be thrown
|
||||
* if validation fails.
|
||||
* @param error Error message
|
||||
* @returns The exception object
|
||||
*/
|
||||
exceptionFactory?: (error: string) => any;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ParseDatePipe
|
||||
implements PipeTransform<string | number | undefined | null>
|
||||
{
|
||||
protected exceptionFactory: (error: string) => any;
|
||||
|
||||
constructor(private readonly options: ParseDatePipeOptions = {}) {
|
||||
const { exceptionFactory, errorHttpStatusCode = HttpStatus.BAD_REQUEST } =
|
||||
options;
|
||||
|
||||
this.exceptionFactory =
|
||||
exceptionFactory ||
|
||||
(error => new HttpErrorByCode[errorHttpStatusCode](error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that accesses and performs optional transformation on argument for
|
||||
* in-flight requests.
|
||||
*
|
||||
* @param value currently processed route argument
|
||||
* @param metadata contains metadata about the currently processed route argument
|
||||
*/
|
||||
transform(value: string | number | undefined | null): Date {
|
||||
if (this.options.optional && isNil(value)) {
|
||||
return this.options.default
|
||||
? this.options.default()
|
||||
: (value as undefined | null);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
throw this.exceptionFactory('Validation failed (no Date provided)');
|
||||
}
|
||||
|
||||
const transformedValue = new Date(value);
|
||||
|
||||
if (isNaN(transformedValue.getTime())) {
|
||||
throw this.exceptionFactory('Validation failed (invalid date format)');
|
||||
}
|
||||
|
||||
return transformedValue;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,21 @@ import { isNil } from '../utils/shared.utils';
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ParseEnumPipeOptions {
|
||||
/**
|
||||
* If true, the pipe will return null or undefined if the value is not provided
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
/**
|
||||
* The HTTP status code to be used in the response when the validation fails.
|
||||
*/
|
||||
errorHttpStatusCode?: ErrorHttpStatusCode;
|
||||
/**
|
||||
* A factory function that returns an exception object to be thrown
|
||||
* if validation fails.
|
||||
* @param error Error message
|
||||
* @returns The exception object
|
||||
*/
|
||||
exceptionFactory?: (error: string) => any;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,21 @@ import { isNil } from '../utils/shared.utils';
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ParseFloatPipeOptions {
|
||||
/**
|
||||
* The HTTP status code to be used in the response when the validation fails.
|
||||
*/
|
||||
errorHttpStatusCode?: ErrorHttpStatusCode;
|
||||
/**
|
||||
* A factory function that returns an exception object to be thrown
|
||||
* if validation fails.
|
||||
* @param error Error message
|
||||
* @returns The exception object
|
||||
*/
|
||||
exceptionFactory?: (error: string) => any;
|
||||
/**
|
||||
* If true, the pipe will return null or undefined if the value is not provided
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,21 @@ import { isNil } from '../utils/shared.utils';
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ParseIntPipeOptions {
|
||||
/**
|
||||
* The HTTP status code to be used in the response when the validation fails.
|
||||
*/
|
||||
errorHttpStatusCode?: ErrorHttpStatusCode;
|
||||
/**
|
||||
* A factory function that returns an exception object to be thrown
|
||||
* if validation fails.
|
||||
* @param error Error message
|
||||
* @returns The exception object
|
||||
*/
|
||||
exceptionFactory?: (error: string) => any;
|
||||
/**
|
||||
* If true, the pipe will return null or undefined if the value is not provided
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,25 @@ import { isNil, isString } from '../utils/shared.utils';
|
||||
* @publicApi
|
||||
*/
|
||||
export interface ParseUUIDPipeOptions {
|
||||
/**
|
||||
* UUID version to validate
|
||||
*/
|
||||
version?: '3' | '4' | '5' | '7';
|
||||
/**
|
||||
* The HTTP status code to be used in the response when the validation fails.
|
||||
*/
|
||||
errorHttpStatusCode?: ErrorHttpStatusCode;
|
||||
/**
|
||||
* A factory function that returns an exception object to be thrown
|
||||
* if validation fails.
|
||||
* @param error Error message
|
||||
* @returns The exception object
|
||||
*/
|
||||
exceptionFactory?: (errors: string) => any;
|
||||
/**
|
||||
* If true, the pipe will return null or undefined if the value is not provided
|
||||
* @default false
|
||||
*/
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ export class ValidationPipe implements PipeTransform<any> {
|
||||
const isNil = value !== originalValue;
|
||||
const isPrimitive = this.isPrimitive(value);
|
||||
this.stripProtoKeys(value);
|
||||
let entity = classTransformer.plainToClass(
|
||||
let entity = classTransformer.plainToInstance(
|
||||
metatype,
|
||||
value,
|
||||
this.transformOptions,
|
||||
@@ -203,6 +203,12 @@ export class ValidationPipe implements PipeTransform<any> {
|
||||
return value === true || value === 'true';
|
||||
}
|
||||
if (metatype === Number) {
|
||||
if (isUndefined(value)) {
|
||||
// This is a workaround to deal with optional numeric values since
|
||||
// optional numerics shouldn't be parsed to a valid number when
|
||||
// they were not defined
|
||||
return undefined;
|
||||
}
|
||||
return +value;
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -94,7 +94,11 @@ export class ClassSerializerInterceptor implements NestInterceptor {
|
||||
if (plainOrClass instanceof options.type) {
|
||||
return classTransformer.classToPlain(plainOrClass, options);
|
||||
}
|
||||
const instance = classTransformer.plainToClass(options.type, plainOrClass);
|
||||
const instance = classTransformer.plainToInstance(
|
||||
options.type,
|
||||
plainOrClass,
|
||||
options,
|
||||
);
|
||||
return classTransformer.classToPlain(instance, options);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { inspect, InspectOptions } from 'util';
|
||||
import { Injectable, Optional } from '../decorators/core';
|
||||
import { clc, yellow } from '../utils/cli-colors.util';
|
||||
import {
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
import { LoggerService, LogLevel } from './logger.service';
|
||||
import { isLogLevelEnabled } from './utils';
|
||||
|
||||
const DEFAULT_DEPTH = 5;
|
||||
|
||||
export interface ConsoleLoggerOptions {
|
||||
/**
|
||||
* Enabled log levels.
|
||||
@@ -16,8 +19,73 @@ export interface ConsoleLoggerOptions {
|
||||
logLevels?: LogLevel[];
|
||||
/**
|
||||
* If enabled, will print timestamp (time difference) between current and previous log message.
|
||||
* Note: This option is not used when `json` is enabled.
|
||||
*/
|
||||
timestamp?: boolean;
|
||||
/**
|
||||
* A prefix to be used for each log message.
|
||||
* Note: This option is not used when `json` is enabled.
|
||||
*/
|
||||
prefix?: string;
|
||||
/**
|
||||
* If enabled, will print the log message in JSON format.
|
||||
*/
|
||||
json?: boolean;
|
||||
/**
|
||||
* If enabled, will print the log message in color.
|
||||
* Default true if json is disabled, false otherwise
|
||||
*/
|
||||
colors?: boolean;
|
||||
/**
|
||||
* The context of the logger.
|
||||
*/
|
||||
context?: string;
|
||||
/**
|
||||
* If enabled, will print the log message in a single line, even if it is an object with multiple properties.
|
||||
* If set to a number, the most n inner elements are united on a single line as long as all properties fit into breakLength. Short array elements are also grouped together.
|
||||
* Default true when `json` is enabled, false otherwise.
|
||||
*/
|
||||
compact?: boolean | number;
|
||||
/**
|
||||
* Specifies the maximum number of Array, TypedArray, Map, Set, WeakMap, and WeakSet elements to include when formatting.
|
||||
* Set to null or Infinity to show all elements. Set to 0 or negative to show no elements.
|
||||
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
|
||||
* @default 100
|
||||
*/
|
||||
maxArrayLength?: number;
|
||||
/**
|
||||
* Specifies the maximum number of characters to include when formatting.
|
||||
* Set to null or Infinity to show all elements. Set to 0 or negative to show no characters.
|
||||
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
|
||||
* @default 10000.
|
||||
*/
|
||||
maxStringLength?: number;
|
||||
/**
|
||||
* If enabled, will sort keys while formatting objects.
|
||||
* Can also be a custom sorting function.
|
||||
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
|
||||
* @default false
|
||||
*/
|
||||
sorted?: boolean | ((a: string, b: string) => number);
|
||||
/**
|
||||
* Specifies the number of times to recurse while formatting object. T
|
||||
* This is useful for inspecting large objects. To recurse up to the maximum call stack size pass Infinity or null.
|
||||
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
|
||||
* @default 5
|
||||
*/
|
||||
depth?: number;
|
||||
/**
|
||||
* If true, object's non-enumerable symbols and properties are included in the formatted result.
|
||||
* WeakMap and WeakSet entries are also included as well as user defined prototype properties
|
||||
* @default false
|
||||
*/
|
||||
showHidden?: boolean;
|
||||
/**
|
||||
* The length at which input values are split across multiple lines. Set to Infinity to format the input as a single line (in combination with "compact" set to true).
|
||||
* Default Infinity when "compact" is true, 80 otherwise.
|
||||
* Ignored when `json` is enabled, colors are disabled, and `compact` is set to true as it produces a parseable JSON output.
|
||||
*/
|
||||
breakLength?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_LOG_LEVELS: LogLevel[] = [
|
||||
@@ -40,22 +108,54 @@ const dateTimeFormatter = new Intl.DateTimeFormat(undefined, {
|
||||
|
||||
@Injectable()
|
||||
export class ConsoleLogger implements LoggerService {
|
||||
private static lastTimestampAt?: number;
|
||||
private originalContext?: string;
|
||||
/**
|
||||
* The options of the logger.
|
||||
*/
|
||||
protected options: ConsoleLoggerOptions;
|
||||
/**
|
||||
* The context of the logger (can be set manually or automatically inferred).
|
||||
*/
|
||||
protected context?: string;
|
||||
/**
|
||||
* The original context of the logger (set in the constructor).
|
||||
*/
|
||||
protected originalContext?: string;
|
||||
/**
|
||||
* The options used for the "inspect" method.
|
||||
*/
|
||||
protected inspectOptions: InspectOptions;
|
||||
/**
|
||||
* The last timestamp at which the log message was printed.
|
||||
*/
|
||||
protected static lastTimestampAt?: number;
|
||||
|
||||
constructor();
|
||||
constructor(context: string);
|
||||
constructor(options: ConsoleLoggerOptions);
|
||||
constructor(context: string, options: ConsoleLoggerOptions);
|
||||
constructor(
|
||||
@Optional()
|
||||
protected context?: string,
|
||||
contextOrOptions?: string | ConsoleLoggerOptions,
|
||||
@Optional()
|
||||
protected options: ConsoleLoggerOptions = {},
|
||||
options?: ConsoleLoggerOptions,
|
||||
) {
|
||||
if (!options.logLevels) {
|
||||
options.logLevels = DEFAULT_LOG_LEVELS;
|
||||
}
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [context, opts] = isString(contextOrOptions)
|
||||
? [contextOrOptions, options]
|
||||
: !!options
|
||||
? [undefined, options]
|
||||
: [contextOrOptions?.context, contextOrOptions];
|
||||
|
||||
opts = opts ?? {};
|
||||
opts.logLevels ??= DEFAULT_LOG_LEVELS;
|
||||
opts.colors ??= opts.colors ?? (opts.json ? false : true);
|
||||
opts.prefix ??= 'Nest';
|
||||
|
||||
this.options = opts;
|
||||
this.inspectOptions = this.getInspectOptions();
|
||||
|
||||
if (context) {
|
||||
this.context = context;
|
||||
this.originalContext = context;
|
||||
}
|
||||
}
|
||||
@@ -91,7 +191,7 @@ export class ConsoleLogger implements LoggerService {
|
||||
const { messages, context, stack } =
|
||||
this.getContextAndStackAndMessagesToPrint([message, ...optionalParams]);
|
||||
|
||||
this.printMessages(messages, context, 'error', 'stderr');
|
||||
this.printMessages(messages, context, 'error', 'stderr', stack);
|
||||
this.printStackTrace(stack);
|
||||
}
|
||||
|
||||
@@ -203,8 +303,18 @@ export class ConsoleLogger implements LoggerService {
|
||||
context = '',
|
||||
logLevel: LogLevel = 'log',
|
||||
writeStreamType?: 'stdout' | 'stderr',
|
||||
errorStack?: unknown,
|
||||
) {
|
||||
messages.forEach(message => {
|
||||
if (this.options.json) {
|
||||
this.printAsJson(message, {
|
||||
context,
|
||||
logLevel,
|
||||
writeStreamType,
|
||||
errorStack,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const pidMessage = this.formatPid(process.pid);
|
||||
const contextMessage = this.formatContext(context);
|
||||
const timestampDiff = this.updateAndGetTimestampDiff();
|
||||
@@ -222,12 +332,57 @@ export class ConsoleLogger implements LoggerService {
|
||||
});
|
||||
}
|
||||
|
||||
protected printAsJson(
|
||||
message: unknown,
|
||||
options: {
|
||||
context: string;
|
||||
logLevel: LogLevel;
|
||||
writeStreamType?: 'stdout' | 'stderr';
|
||||
errorStack?: unknown;
|
||||
},
|
||||
) {
|
||||
type JsonLogObject = {
|
||||
level: LogLevel;
|
||||
pid: number;
|
||||
timestamp: number;
|
||||
message: unknown;
|
||||
context?: string;
|
||||
stack?: unknown;
|
||||
};
|
||||
|
||||
const logObject: JsonLogObject = {
|
||||
level: options.logLevel,
|
||||
pid: process.pid,
|
||||
timestamp: Date.now(),
|
||||
message,
|
||||
};
|
||||
|
||||
if (options.context) {
|
||||
logObject.context = options.context;
|
||||
}
|
||||
|
||||
if (options.errorStack) {
|
||||
logObject.stack = options.errorStack;
|
||||
}
|
||||
|
||||
const formattedMessage =
|
||||
!this.options.colors && this.inspectOptions.compact === true
|
||||
? JSON.stringify(logObject, this.stringifyReplacer)
|
||||
: inspect(logObject, this.inspectOptions);
|
||||
process[options.writeStreamType ?? 'stdout'].write(`${formattedMessage}\n`);
|
||||
}
|
||||
|
||||
protected formatPid(pid: number) {
|
||||
return `[Nest] ${pid} - `;
|
||||
return `[${this.options.prefix}] ${pid} - `;
|
||||
}
|
||||
|
||||
protected formatContext(context: string): string {
|
||||
return context ? yellow(`[${context}] `) : '';
|
||||
if (!context) {
|
||||
return '';
|
||||
}
|
||||
|
||||
context = `[${context}] `;
|
||||
return this.options.colors ? yellow(context) : context;
|
||||
}
|
||||
|
||||
protected formatMessage(
|
||||
@@ -256,23 +411,30 @@ export class ConsoleLogger implements LoggerService {
|
||||
return this.stringifyMessage(message(), logLevel);
|
||||
}
|
||||
|
||||
return isPlainObject(message) || Array.isArray(message)
|
||||
? `${this.colorize('Object:', logLevel)}\n${JSON.stringify(
|
||||
message,
|
||||
(key, value) =>
|
||||
typeof value === 'bigint' ? value.toString() : value,
|
||||
2,
|
||||
)}\n`
|
||||
: this.colorize(message as string, logLevel);
|
||||
if (typeof message === 'string') {
|
||||
return this.colorize(message, logLevel);
|
||||
}
|
||||
|
||||
const outputText = inspect(message, this.inspectOptions);
|
||||
if (isPlainObject(message)) {
|
||||
return `Object(${Object.keys(message).length}) ${outputText}`;
|
||||
}
|
||||
if (Array.isArray(message)) {
|
||||
return `Array(${message.length}) ${outputText}`;
|
||||
}
|
||||
return outputText;
|
||||
}
|
||||
|
||||
protected colorize(message: string, logLevel: LogLevel) {
|
||||
if (!this.options.colors || this.options.json) {
|
||||
return message;
|
||||
}
|
||||
const color = this.getColorByLogLevel(logLevel);
|
||||
return color(message);
|
||||
}
|
||||
|
||||
protected printStackTrace(stack: string) {
|
||||
if (!stack) {
|
||||
if (!stack || this.options.json) {
|
||||
return;
|
||||
}
|
||||
process.stderr.write(`${stack}\n`);
|
||||
@@ -289,7 +451,58 @@ export class ConsoleLogger implements LoggerService {
|
||||
}
|
||||
|
||||
protected formatTimestampDiff(timestampDiff: number) {
|
||||
return yellow(` +${timestampDiff}ms`);
|
||||
const formattedDiff = ` +${timestampDiff}ms`;
|
||||
return this.options.colors ? yellow(formattedDiff) : formattedDiff;
|
||||
}
|
||||
|
||||
protected getInspectOptions() {
|
||||
let breakLength = this.options.breakLength;
|
||||
if (typeof breakLength === 'undefined') {
|
||||
breakLength = this.options.colors
|
||||
? this.options.compact
|
||||
? Infinity
|
||||
: undefined
|
||||
: this.options.compact === false
|
||||
? undefined
|
||||
: Infinity; // default breakLength to Infinity if inline is not set and colors is false
|
||||
}
|
||||
|
||||
const inspectOptions: InspectOptions = {
|
||||
depth: this.options.depth ?? DEFAULT_DEPTH,
|
||||
sorted: this.options.sorted,
|
||||
showHidden: this.options.showHidden,
|
||||
compact: this.options.compact ?? (this.options.json ? true : false),
|
||||
colors: this.options.colors,
|
||||
breakLength,
|
||||
};
|
||||
|
||||
if (this.options.maxArrayLength) {
|
||||
inspectOptions.maxArrayLength = this.options.maxArrayLength;
|
||||
}
|
||||
if (this.options.maxStringLength) {
|
||||
inspectOptions.maxStringLength = this.options.maxStringLength;
|
||||
}
|
||||
|
||||
return inspectOptions;
|
||||
}
|
||||
|
||||
protected stringifyReplacer(key: string, value: unknown) {
|
||||
// Mimic util.inspect behavior for JSON logger with compact on and colors off
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString();
|
||||
}
|
||||
if (typeof value === 'symbol') {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
if (
|
||||
value instanceof Map ||
|
||||
value instanceof Set ||
|
||||
value instanceof Error
|
||||
) {
|
||||
return `${inspect(value, this.inspectOptions)}`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private getContextAndMessagesToPrint(args: unknown[]) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai';
|
||||
import { applyDecorators, UseGuards } from '../../decorators';
|
||||
import { GUARDS_METADATA } from '../../constants';
|
||||
import { applyDecorators, UseGuards } from '../../decorators';
|
||||
import { CanActivate } from '../../interfaces';
|
||||
|
||||
describe('applyDecorators', () => {
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { expect } from 'chai';
|
||||
import { Body, HostParam, Param, Query, Search } from '../../decorators';
|
||||
import { RequestMethod } from '../../enums/request-method.enum';
|
||||
import { All, Delete, Get, ParseIntPipe, Patch, Post, Put } from '../../index';
|
||||
import {
|
||||
All,
|
||||
Delete,
|
||||
Get,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Propfind,
|
||||
Proppatch,
|
||||
Mkcol,
|
||||
Move,
|
||||
Copy,
|
||||
Lock,
|
||||
Unlock,
|
||||
} from '../../index';
|
||||
import { ROUTE_ARGS_METADATA } from '../../constants';
|
||||
import { RouteParamtypes } from '../../enums/route-paramtypes.enum';
|
||||
|
||||
@@ -415,3 +430,409 @@ describe('Inheritance', () => {
|
||||
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
|
||||
});
|
||||
});
|
||||
|
||||
describe('@PropFind', () => {
|
||||
const requestPath = 'test';
|
||||
const requestProps = {
|
||||
path: requestPath,
|
||||
method: RequestMethod.PROPFIND,
|
||||
};
|
||||
|
||||
const requestPathUsingArray = ['foo', 'bar'];
|
||||
const requestPropsUsingArray = {
|
||||
path: requestPathUsingArray,
|
||||
method: RequestMethod.PROPFIND,
|
||||
};
|
||||
|
||||
it('should enhance class with expected request metadata', () => {
|
||||
class Test {
|
||||
@Propfind(requestPath)
|
||||
public static test() {}
|
||||
|
||||
@Propfind(requestPathUsingArray)
|
||||
public static testUsingArray() {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const method = Reflect.getMetadata('method', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql(requestPath);
|
||||
expect(method).to.be.eql(requestProps.method);
|
||||
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
|
||||
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
|
||||
});
|
||||
|
||||
it('should set path on "/" by default', () => {
|
||||
class Test {
|
||||
@Propfind()
|
||||
public static test(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
|
||||
@Propfind([])
|
||||
public static testUsingArray(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql('/');
|
||||
expect(pathUsingArray).to.be.eql('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@PropPatch', () => {
|
||||
const requestPath = 'test';
|
||||
const requestProps = {
|
||||
path: requestPath,
|
||||
method: RequestMethod.PROPPATCH,
|
||||
};
|
||||
|
||||
const requestPathUsingArray = ['foo', 'bar'];
|
||||
const requestPropsUsingArray = {
|
||||
path: requestPathUsingArray,
|
||||
method: RequestMethod.PROPPATCH,
|
||||
};
|
||||
|
||||
it('should enhance class with expected request metadata', () => {
|
||||
class Test {
|
||||
@Proppatch(requestPath)
|
||||
public static test() {}
|
||||
|
||||
@Proppatch(requestPathUsingArray)
|
||||
public static testUsingArray() {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const method = Reflect.getMetadata('method', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql(requestPath);
|
||||
expect(method).to.be.eql(requestProps.method);
|
||||
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
|
||||
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
|
||||
});
|
||||
|
||||
it('should set path on "/" by default', () => {
|
||||
class Test {
|
||||
@Proppatch()
|
||||
public static test(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
|
||||
@Proppatch([])
|
||||
public static testUsingArray(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql('/');
|
||||
expect(pathUsingArray).to.be.eql('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@MkCol', () => {
|
||||
const requestPath = 'test';
|
||||
const requestProps = {
|
||||
path: requestPath,
|
||||
method: RequestMethod.MKCOL,
|
||||
};
|
||||
|
||||
const requestPathUsingArray = ['foo', 'bar'];
|
||||
const requestPropsUsingArray = {
|
||||
path: requestPathUsingArray,
|
||||
method: RequestMethod.MKCOL,
|
||||
};
|
||||
|
||||
it('should enhance class with expected request metadata', () => {
|
||||
class Test {
|
||||
@Mkcol(requestPath)
|
||||
public static test() {}
|
||||
|
||||
@Mkcol(requestPathUsingArray)
|
||||
public static testUsingArray() {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const method = Reflect.getMetadata('method', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql(requestPath);
|
||||
expect(method).to.be.eql(requestProps.method);
|
||||
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
|
||||
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
|
||||
});
|
||||
|
||||
it('should set path on "/" by default', () => {
|
||||
class Test {
|
||||
@Mkcol()
|
||||
public static test(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
|
||||
@Mkcol([])
|
||||
public static testUsingArray(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql('/');
|
||||
expect(pathUsingArray).to.be.eql('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Copy', () => {
|
||||
const requestPath = 'test';
|
||||
const requestProps = {
|
||||
path: requestPath,
|
||||
method: RequestMethod.COPY,
|
||||
};
|
||||
|
||||
const requestPathUsingArray = ['foo', 'bar'];
|
||||
const requestPropsUsingArray = {
|
||||
path: requestPathUsingArray,
|
||||
method: RequestMethod.COPY,
|
||||
};
|
||||
|
||||
it('should enhance class with expected request metadata', () => {
|
||||
class Test {
|
||||
@Copy(requestPath)
|
||||
public static test() {}
|
||||
|
||||
@Copy(requestPathUsingArray)
|
||||
public static testUsingArray() {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const method = Reflect.getMetadata('method', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql(requestPath);
|
||||
expect(method).to.be.eql(requestProps.method);
|
||||
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
|
||||
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
|
||||
});
|
||||
|
||||
it('should set path on "/" by default', () => {
|
||||
class Test {
|
||||
@Copy()
|
||||
public static test(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
|
||||
@Copy([])
|
||||
public static testUsingArray(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql('/');
|
||||
expect(pathUsingArray).to.be.eql('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Move', () => {
|
||||
const requestPath = 'test';
|
||||
const requestProps = {
|
||||
path: requestPath,
|
||||
method: RequestMethod.MOVE,
|
||||
};
|
||||
|
||||
const requestPathUsingArray = ['foo', 'bar'];
|
||||
const requestPropsUsingArray = {
|
||||
path: requestPathUsingArray,
|
||||
method: RequestMethod.MOVE,
|
||||
};
|
||||
|
||||
it('should enhance class with expected request metadata', () => {
|
||||
class Test {
|
||||
@Move(requestPath)
|
||||
public static test() {}
|
||||
|
||||
@Move(requestPathUsingArray)
|
||||
public static testUsingArray() {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const method = Reflect.getMetadata('method', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql(requestPath);
|
||||
expect(method).to.be.eql(requestProps.method);
|
||||
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
|
||||
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
|
||||
});
|
||||
|
||||
it('should set path on "/" by default', () => {
|
||||
class Test {
|
||||
@Move()
|
||||
public static test(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
|
||||
@Move([])
|
||||
public static testUsingArray(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql('/');
|
||||
expect(pathUsingArray).to.be.eql('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Lock', () => {
|
||||
const requestPath = 'test';
|
||||
const requestProps = {
|
||||
path: requestPath,
|
||||
method: RequestMethod.LOCK,
|
||||
};
|
||||
|
||||
const requestPathUsingArray = ['foo', 'bar'];
|
||||
const requestPropsUsingArray = {
|
||||
path: requestPathUsingArray,
|
||||
method: RequestMethod.LOCK,
|
||||
};
|
||||
|
||||
it('should enhance class with expected request metadata', () => {
|
||||
class Test {
|
||||
@Lock(requestPath)
|
||||
public static test() {}
|
||||
|
||||
@Lock(requestPathUsingArray)
|
||||
public static testUsingArray() {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const method = Reflect.getMetadata('method', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql(requestPath);
|
||||
expect(method).to.be.eql(requestProps.method);
|
||||
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
|
||||
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
|
||||
});
|
||||
|
||||
it('should set path on "/" by default', () => {
|
||||
class Test {
|
||||
@Lock()
|
||||
public static test(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
|
||||
@Lock([])
|
||||
public static testUsingArray(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql('/');
|
||||
expect(pathUsingArray).to.be.eql('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('@Unlock', () => {
|
||||
const requestPath = 'test';
|
||||
const requestProps = {
|
||||
path: requestPath,
|
||||
method: RequestMethod.UNLOCK,
|
||||
};
|
||||
|
||||
const requestPathUsingArray = ['foo', 'bar'];
|
||||
const requestPropsUsingArray = {
|
||||
path: requestPathUsingArray,
|
||||
method: RequestMethod.UNLOCK,
|
||||
};
|
||||
|
||||
it('should enhance class with expected request metadata', () => {
|
||||
class Test {
|
||||
@Unlock(requestPath)
|
||||
public static test() {}
|
||||
|
||||
@Unlock(requestPathUsingArray)
|
||||
public static testUsingArray() {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const method = Reflect.getMetadata('method', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
const methodUsingArray = Reflect.getMetadata('method', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql(requestPath);
|
||||
expect(method).to.be.eql(requestProps.method);
|
||||
expect(pathUsingArray).to.be.eql(requestPathUsingArray);
|
||||
expect(methodUsingArray).to.be.eql(requestPropsUsingArray.method);
|
||||
});
|
||||
|
||||
it('should set path on "/" by default', () => {
|
||||
class Test {
|
||||
@Unlock()
|
||||
public static test(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
|
||||
@Unlock([])
|
||||
public static testUsingArray(
|
||||
@Query() query,
|
||||
@Param() params,
|
||||
@HostParam() hostParams,
|
||||
) {}
|
||||
}
|
||||
|
||||
const path = Reflect.getMetadata('path', Test.test);
|
||||
const pathUsingArray = Reflect.getMetadata('path', Test.testUsingArray);
|
||||
|
||||
expect(path).to.be.eql('/');
|
||||
expect(pathUsingArray).to.be.eql('/');
|
||||
});
|
||||
});
|
||||
|
||||
71
packages/common/test/pipes/parse-date.pipe.spec.ts
Normal file
71
packages/common/test/pipes/parse-date.pipe.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { expect } from 'chai';
|
||||
import { BadRequestException } from '../../exceptions';
|
||||
import { ParseDatePipe } from '../../pipes/parse-date.pipe';
|
||||
|
||||
describe('ParseDatePipe', () => {
|
||||
let target: ParseDatePipe;
|
||||
|
||||
beforeEach(() => {
|
||||
target = new ParseDatePipe();
|
||||
});
|
||||
|
||||
describe('transform', () => {
|
||||
describe('when validation passes', () => {
|
||||
it('should return a valid date object', () => {
|
||||
const date = new Date().toISOString();
|
||||
|
||||
const transformedDate = target.transform(date);
|
||||
expect(transformedDate).to.be.instanceOf(Date);
|
||||
expect(transformedDate.toISOString()).to.equal(date);
|
||||
|
||||
const asNumber = transformedDate.getTime();
|
||||
const transformedNumber = target.transform(asNumber);
|
||||
expect(transformedNumber).to.be.instanceOf(Date);
|
||||
expect(transformedNumber.getTime()).to.equal(asNumber);
|
||||
});
|
||||
|
||||
it('should not throw an error if the value is undefined/null and optional is true', () => {
|
||||
const target = new ParseDatePipe({ optional: true });
|
||||
const value = target.transform(undefined);
|
||||
expect(value).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
describe('when default value is provided', () => {
|
||||
it('should return the default value if the value is undefined/null', () => {
|
||||
const defaultValue = new Date();
|
||||
const target = new ParseDatePipe({
|
||||
optional: true,
|
||||
default: () => defaultValue,
|
||||
});
|
||||
const value = target.transform(undefined);
|
||||
expect(value).to.equal(defaultValue);
|
||||
});
|
||||
});
|
||||
describe('when validation fails', () => {
|
||||
it('should throw an error', () => {
|
||||
try {
|
||||
target.transform('123abc');
|
||||
expect.fail();
|
||||
} catch (error) {
|
||||
expect(error).to.be.instanceOf(BadRequestException);
|
||||
expect(error.message).to.equal(
|
||||
'Validation failed (invalid date format)',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('when empty value', () => {
|
||||
it('should throw an error', () => {
|
||||
try {
|
||||
target.transform('');
|
||||
expect.fail();
|
||||
} catch (error) {
|
||||
expect(error).to.be.instanceOf(BadRequestException);
|
||||
expect(error.message).to.equal(
|
||||
'Validation failed (no Date provided)',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -205,6 +205,18 @@ describe('ValidationPipe', () => {
|
||||
}),
|
||||
).to.be.equal(+value);
|
||||
});
|
||||
it('should parse undefined to undefined', async () => {
|
||||
target = new ValidationPipe({ transform: true });
|
||||
const value = undefined;
|
||||
|
||||
expect(
|
||||
await target.transform(value, {
|
||||
metatype: Number,
|
||||
data: 'test',
|
||||
type: 'query',
|
||||
}),
|
||||
).to.be.undefined;
|
||||
});
|
||||
});
|
||||
describe('when input is a path parameter (number)', () => {
|
||||
it('should parse to number', async () => {
|
||||
@@ -219,6 +231,18 @@ describe('ValidationPipe', () => {
|
||||
}),
|
||||
).to.be.equal(+value);
|
||||
});
|
||||
it('should parse undefined to undefined', async () => {
|
||||
target = new ValidationPipe({ transform: true });
|
||||
const value = undefined;
|
||||
|
||||
expect(
|
||||
await target.transform(value, {
|
||||
metatype: Number,
|
||||
data: 'test',
|
||||
type: 'param',
|
||||
}),
|
||||
).to.be.undefined;
|
||||
});
|
||||
});
|
||||
describe('when input is a query parameter (boolean)', () => {
|
||||
it('should parse the string "true" to the boolean true', async () => {
|
||||
@@ -478,6 +502,56 @@ describe('ValidationPipe', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('option: "validateCustomDecorators" when metadata.type is not `body`', () => {
|
||||
describe('when is set to `true`', () => {
|
||||
it('should transform and validate', async () => {
|
||||
const target = new ValidationPipe({
|
||||
validateCustomDecorators: true,
|
||||
});
|
||||
|
||||
const metadata: ArgumentMetadata = {
|
||||
type: 'custom',
|
||||
metatype: TestModel,
|
||||
data: '',
|
||||
};
|
||||
|
||||
const testObj = { prop1: 'value1', prop2: 'value2' };
|
||||
expect(await target.transform(testObj, metadata)).to.not.be.undefined;
|
||||
});
|
||||
});
|
||||
describe('when is set to `false`', () => {
|
||||
it('should throw an error', async () => {
|
||||
const target = new ValidationPipe({
|
||||
validateCustomDecorators: false,
|
||||
});
|
||||
|
||||
const metadata: ArgumentMetadata = {
|
||||
type: 'custom',
|
||||
metatype: TestModel,
|
||||
data: '',
|
||||
};
|
||||
|
||||
const objNotFollowingTestModel = { prop1: undefined, prop2: 'value2' };
|
||||
expect(await target.transform(objNotFollowingTestModel, metadata)).to
|
||||
.not.be.undefined;
|
||||
});
|
||||
});
|
||||
describe('when is not supplied', () => {
|
||||
it('should transform and validate', async () => {
|
||||
const target = new ValidationPipe({});
|
||||
|
||||
const metadata: ArgumentMetadata = {
|
||||
type: 'custom',
|
||||
metatype: TestModel,
|
||||
data: '',
|
||||
};
|
||||
|
||||
const testObj = { prop1: 'value1', prop2: 'value2' };
|
||||
expect(await target.transform(testObj, metadata)).to.not.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('option: "errorHttpStatusCode"', () => {
|
||||
describe('when validation fails', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -125,9 +125,12 @@ describe('Logger', () => {
|
||||
Logger.error(error);
|
||||
|
||||
expect(processStderrWriteSpy.calledOnce).to.be.true;
|
||||
expect(processStderrWriteSpy.firstCall.firstArg).to.include(`Object:`);
|
||||
|
||||
expect(processStderrWriteSpy.firstCall.firstArg).to.include(
|
||||
`{\n "randomError": true\n}`,
|
||||
`Object(${Object.keys(error).length})`,
|
||||
);
|
||||
expect(processStderrWriteSpy.firstCall.firstArg).to.include(
|
||||
`randomError: \x1b[33mtrue`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -181,6 +184,153 @@ describe('Logger', () => {
|
||||
expect(processStderrWriteSpy.thirdCall.firstArg).to.equal(stack + '\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the default logger is used and json mode is enabled', () => {
|
||||
const logger = new ConsoleLogger({ json: true });
|
||||
|
||||
let processStdoutWriteSpy: sinon.SinonSpy;
|
||||
let processStderrWriteSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
|
||||
processStderrWriteSpy = sinon.spy(process.stderr, 'write');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
processStdoutWriteSpy.restore();
|
||||
processStderrWriteSpy.restore();
|
||||
});
|
||||
|
||||
it('should print error with stack as JSON to the console', () => {
|
||||
const errorMessage = 'error message';
|
||||
const error = new Error(errorMessage);
|
||||
|
||||
logger.error(error.message, error.stack);
|
||||
|
||||
const json = JSON.parse(processStderrWriteSpy.firstCall?.firstArg);
|
||||
|
||||
expect(json.pid).to.equal(process.pid);
|
||||
expect(json.level).to.equal('error');
|
||||
expect(json.message).to.equal(errorMessage);
|
||||
});
|
||||
it('should log out to stdout as JSON', () => {
|
||||
const message = 'message 1';
|
||||
|
||||
logger.log(message);
|
||||
|
||||
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
|
||||
|
||||
expect(json.pid).to.equal(process.pid);
|
||||
expect(json.level).to.equal('log');
|
||||
expect(json.message).to.equal(message);
|
||||
});
|
||||
it('should log out an error to stderr as JSON', () => {
|
||||
const message = 'message 1';
|
||||
|
||||
logger.error(message);
|
||||
|
||||
const json = JSON.parse(processStderrWriteSpy.firstCall?.firstArg);
|
||||
|
||||
expect(json.pid).to.equal(process.pid);
|
||||
expect(json.level).to.equal('error');
|
||||
expect(json.message).to.equal(message);
|
||||
});
|
||||
it('should log Map object', () => {
|
||||
const map = new Map([
|
||||
['key1', 'value1'],
|
||||
['key2', 'value2'],
|
||||
]);
|
||||
|
||||
logger.log(map);
|
||||
|
||||
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
|
||||
|
||||
expect(json.pid).to.equal(process.pid);
|
||||
expect(json.level).to.equal('log');
|
||||
expect(json.message).to.equal(
|
||||
`Map(2) { 'key1' => 'value1', 'key2' => 'value2' }`,
|
||||
);
|
||||
});
|
||||
it('should log Set object', () => {
|
||||
const set = new Set(['value1', 'value2']);
|
||||
|
||||
logger.log(set);
|
||||
|
||||
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
|
||||
|
||||
expect(json.pid).to.equal(process.pid);
|
||||
expect(json.level).to.equal('log');
|
||||
expect(json.message).to.equal(`Set(2) { 'value1', 'value2' }`);
|
||||
});
|
||||
it('should log bigint', () => {
|
||||
const bigInt = BigInt(9007199254740991);
|
||||
|
||||
logger.log(bigInt);
|
||||
|
||||
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
|
||||
|
||||
expect(json.pid).to.equal(process.pid);
|
||||
expect(json.level).to.equal('log');
|
||||
expect(json.message).to.equal('9007199254740991');
|
||||
});
|
||||
it('should log symbol', () => {
|
||||
const symbol = Symbol('test');
|
||||
|
||||
logger.log(symbol);
|
||||
|
||||
const json = JSON.parse(processStdoutWriteSpy.firstCall?.firstArg);
|
||||
|
||||
expect(json.pid).to.equal(process.pid);
|
||||
expect(json.level).to.equal('log');
|
||||
expect(json.message).to.equal('Symbol(test)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the default logger is used, json mode is enabled and compact is false (utils.inspect)', () => {
|
||||
const logger = new ConsoleLogger({ json: true, compact: false });
|
||||
|
||||
let processStdoutWriteSpy: sinon.SinonSpy;
|
||||
let processStderrWriteSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
processStdoutWriteSpy = sinon.spy(process.stdout, 'write');
|
||||
processStderrWriteSpy = sinon.spy(process.stderr, 'write');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
processStdoutWriteSpy.restore();
|
||||
processStderrWriteSpy.restore();
|
||||
});
|
||||
|
||||
it('should log out to stdout as JSON (utils.inspect)', () => {
|
||||
const message = 'message 1';
|
||||
|
||||
logger.log(message);
|
||||
|
||||
const json = convertInspectToJSON(
|
||||
processStdoutWriteSpy.firstCall?.firstArg,
|
||||
);
|
||||
|
||||
expect(json.pid).to.equal(process.pid);
|
||||
expect(json.level).to.equal('log');
|
||||
expect(json.message).to.equal(message);
|
||||
});
|
||||
|
||||
it('should log out an error to stderr as JSON (utils.inspect)', () => {
|
||||
const message = 'message 1';
|
||||
|
||||
logger.error(message);
|
||||
|
||||
const json = convertInspectToJSON(
|
||||
processStderrWriteSpy.firstCall?.firstArg,
|
||||
);
|
||||
|
||||
expect(json.pid).to.equal(process.pid);
|
||||
expect(json.level).to.equal('error');
|
||||
expect(json.message).to.equal(message);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when logging is disabled', () => {
|
||||
let processStdoutWriteSpy: sinon.SinonSpy;
|
||||
let previousLoggerRef: LoggerService;
|
||||
@@ -568,6 +718,7 @@ describe('Logger', () => {
|
||||
expect(processStdoutWriteSpy.called).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when custom logger is being used', () => {
|
||||
class CustomLogger implements LoggerService {
|
||||
log(message: any, context?: string) {}
|
||||
@@ -723,7 +874,7 @@ describe('Logger', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const consoleLogger = new CustomConsoleLogger();
|
||||
const consoleLogger = new CustomConsoleLogger({ colors: false });
|
||||
const consoleLoggerSpy = sinon.spy(
|
||||
consoleLogger,
|
||||
'stringifyMessage' as keyof ConsoleLogger,
|
||||
@@ -739,30 +890,40 @@ describe('Logger', () => {
|
||||
|
||||
expect(consoleLoggerSpy.getCall(0).returnValue).to.equal('str1');
|
||||
expect(consoleLoggerSpy.getCall(1).returnValue).to.equal(
|
||||
`Object:
|
||||
{
|
||||
"key": "str2"
|
||||
}
|
||||
`,
|
||||
`Object(1) {
|
||||
key: 'str2'
|
||||
}`,
|
||||
);
|
||||
expect(consoleLoggerSpy.getCall(2).returnValue).to.equal(
|
||||
`Object:
|
||||
[
|
||||
"str3"
|
||||
]
|
||||
`,
|
||||
`Array(1) [
|
||||
'str3'
|
||||
]`,
|
||||
);
|
||||
expect(consoleLoggerSpy.getCall(3).returnValue).to.equal(
|
||||
`Object:
|
||||
[
|
||||
`Array(1) [
|
||||
{
|
||||
"key": "str4"
|
||||
key: 'str4'
|
||||
}
|
||||
]
|
||||
`,
|
||||
]`,
|
||||
);
|
||||
expect(consoleLoggerSpy.getCall(4).returnValue).to.equal(null);
|
||||
expect(consoleLoggerSpy.getCall(5).returnValue).to.equal(1);
|
||||
expect(consoleLoggerSpy.getCall(4).returnValue).to.equal('null');
|
||||
expect(consoleLoggerSpy.getCall(5).returnValue).to.equal('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function convertInspectToJSON(inspectOutput: string) {
|
||||
const jsonLikeString = inspectOutput
|
||||
.replace(/'([^']+)'/g, '"$1"') // single-quoted strings
|
||||
.replace(/([a-zA-Z0-9_]+):/g, '"$1":') // unquoted object keys
|
||||
.replace(/\bundefined\b/g, 'null')
|
||||
.replace(/\[Function(: [^\]]+)?\]/g, '"[Function]"')
|
||||
.replace(/\[Circular\]/g, '"[Circular]"');
|
||||
|
||||
try {
|
||||
return JSON.parse(jsonLikeString);
|
||||
} catch (error) {
|
||||
console.error('Error parsing the modified inspect output:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circfleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
@@ -96,6 +96,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
|
||||
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -62,6 +62,48 @@ export abstract class AbstractHttpAdapter<
|
||||
return this.instance.patch(...args);
|
||||
}
|
||||
|
||||
public propfind(handler: RequestHandler);
|
||||
public propfind(path: any, handler: RequestHandler);
|
||||
public propfind(...args: any[]) {
|
||||
return this.instance.propfind(...args);
|
||||
}
|
||||
|
||||
public proppatch(handler: RequestHandler);
|
||||
public proppatch(path: any, handler: RequestHandler);
|
||||
public proppatch(...args: any[]) {
|
||||
return this.instance.proppatch(...args);
|
||||
}
|
||||
|
||||
public mkcol(handler: RequestHandler);
|
||||
public mkcol(path: any, handler: RequestHandler);
|
||||
public mkcol(...args: any[]) {
|
||||
return this.instance.mkcol(...args);
|
||||
}
|
||||
|
||||
public copy(handler: RequestHandler);
|
||||
public copy(path: any, handler: RequestHandler);
|
||||
public copy(...args: any[]) {
|
||||
return this.instance.copy(...args);
|
||||
}
|
||||
|
||||
public move(handler: RequestHandler);
|
||||
public move(path: any, handler: RequestHandler);
|
||||
public move(...args: any[]) {
|
||||
return this.instance.move(...args);
|
||||
}
|
||||
|
||||
public lock(handler: RequestHandler);
|
||||
public lock(path: any, handler: RequestHandler);
|
||||
public lock(...args: any[]) {
|
||||
return this.instance.lock(...args);
|
||||
}
|
||||
|
||||
public unlock(handler: RequestHandler);
|
||||
public unlock(path: any, handler: RequestHandler);
|
||||
public unlock(...args: any[]) {
|
||||
return this.instance.unlock(...args);
|
||||
}
|
||||
|
||||
public all(handler: RequestHandler);
|
||||
public all(path: any, handler: RequestHandler);
|
||||
public all(...args: any[]) {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { RuntimeException } from './exceptions/runtime.exception';
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
|
||||
export class ExceptionHandler {
|
||||
private static readonly logger = new Logger(ExceptionHandler.name);
|
||||
|
||||
public handle(exception: RuntimeException | Error) {
|
||||
if (!(exception instanceof RuntimeException)) {
|
||||
ExceptionHandler.logger.error(exception.message, exception.stack);
|
||||
return;
|
||||
}
|
||||
ExceptionHandler.logger.error(exception.what(), exception.stack);
|
||||
public handle(exception: Error) {
|
||||
ExceptionHandler.logger.error(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +68,6 @@ export class BaseExceptionFilter<T = any> implements ExceptionFilter<T> {
|
||||
applicationRef.end(response);
|
||||
}
|
||||
|
||||
if (this.isExceptionObject(exception)) {
|
||||
return BaseExceptionFilter.logger.error(
|
||||
exception.message,
|
||||
exception.stack,
|
||||
);
|
||||
}
|
||||
return BaseExceptionFilter.logger.error(exception);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ export class ExternalExceptionFilter<T = any, R = any> {
|
||||
|
||||
catch(exception: T, host: ArgumentsHost): R | Promise<R> {
|
||||
if (exception instanceof Error && !(exception instanceof HttpException)) {
|
||||
ExternalExceptionFilter.logger.error(exception.message, exception.stack);
|
||||
ExternalExceptionFilter.logger.error(exception);
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ const REQUEST_METHOD_MAP = {
|
||||
[RequestMethod.OPTIONS]: 'options',
|
||||
[RequestMethod.HEAD]: 'head',
|
||||
[RequestMethod.SEARCH]: 'search',
|
||||
[RequestMethod.PROPFIND]: 'propfind',
|
||||
[RequestMethod.PROPPATCH]: 'proppatch',
|
||||
[RequestMethod.MKCOL]: 'mkcol',
|
||||
[RequestMethod.COPY]: 'copy',
|
||||
[RequestMethod.MOVE]: 'move',
|
||||
[RequestMethod.LOCK]: 'lock',
|
||||
[RequestMethod.UNLOCK]: 'unlock',
|
||||
} as const satisfies Record<RequestMethod, keyof HttpServer>;
|
||||
|
||||
export class RouterMethodFactory {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ForwardReference,
|
||||
Type,
|
||||
} from '@nestjs/common/interfaces';
|
||||
import { ModuleTokenFactory } from './module-token-factory';
|
||||
import { ModuleOpaqueKeyFactory } from './opaque-key-factory/interfaces/module-opaque-key-factory.interface';
|
||||
|
||||
export interface ModuleFactory {
|
||||
type: Type<any>;
|
||||
@@ -12,36 +12,59 @@ export interface ModuleFactory {
|
||||
}
|
||||
|
||||
export class ModuleCompiler {
|
||||
constructor(private readonly moduleTokenFactory = new ModuleTokenFactory()) {}
|
||||
constructor(
|
||||
private readonly _moduleOpaqueKeyFactory: ModuleOpaqueKeyFactory,
|
||||
) {}
|
||||
|
||||
get moduleOpaqueKeyFactory(): ModuleOpaqueKeyFactory {
|
||||
return this._moduleOpaqueKeyFactory;
|
||||
}
|
||||
|
||||
public async compile(
|
||||
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
|
||||
moduleClsOrDynamic:
|
||||
| Type
|
||||
| DynamicModule
|
||||
| ForwardReference
|
||||
| Promise<DynamicModule>,
|
||||
): Promise<ModuleFactory> {
|
||||
const { type, dynamicMetadata } = this.extractMetadata(await metatype);
|
||||
const token = this.moduleTokenFactory.create(type, dynamicMetadata);
|
||||
moduleClsOrDynamic = await moduleClsOrDynamic;
|
||||
|
||||
const { type, dynamicMetadata } = this.extractMetadata(moduleClsOrDynamic);
|
||||
const token = dynamicMetadata
|
||||
? this._moduleOpaqueKeyFactory.createForDynamic(
|
||||
type,
|
||||
dynamicMetadata,
|
||||
moduleClsOrDynamic as DynamicModule | ForwardReference,
|
||||
)
|
||||
: this._moduleOpaqueKeyFactory.createForStatic(
|
||||
type,
|
||||
moduleClsOrDynamic as Type,
|
||||
);
|
||||
|
||||
return { type, dynamicMetadata, token };
|
||||
}
|
||||
|
||||
public extractMetadata(
|
||||
metatype: Type<any> | ForwardReference | DynamicModule,
|
||||
moduleClsOrDynamic: Type | ForwardReference | DynamicModule,
|
||||
): {
|
||||
type: Type<any>;
|
||||
dynamicMetadata?: Partial<DynamicModule> | undefined;
|
||||
type: Type;
|
||||
dynamicMetadata: Omit<DynamicModule, 'module'> | undefined;
|
||||
} {
|
||||
if (!this.isDynamicModule(metatype)) {
|
||||
if (!this.isDynamicModule(moduleClsOrDynamic)) {
|
||||
return {
|
||||
type: (metatype as ForwardReference)?.forwardRef
|
||||
? (metatype as ForwardReference).forwardRef()
|
||||
: metatype,
|
||||
type: (moduleClsOrDynamic as ForwardReference)?.forwardRef
|
||||
? (moduleClsOrDynamic as ForwardReference).forwardRef()
|
||||
: moduleClsOrDynamic,
|
||||
dynamicMetadata: undefined,
|
||||
};
|
||||
}
|
||||
const { module: type, ...dynamicMetadata } = metatype;
|
||||
const { module: type, ...dynamicMetadata } = moduleClsOrDynamic;
|
||||
return { type, dynamicMetadata };
|
||||
}
|
||||
|
||||
public isDynamicModule(
|
||||
module: Type<any> | DynamicModule | ForwardReference,
|
||||
): module is DynamicModule {
|
||||
return !!(module as DynamicModule).module;
|
||||
moduleClsOrDynamic: Type | DynamicModule | ForwardReference,
|
||||
): moduleClsOrDynamic is DynamicModule {
|
||||
return !!(moduleClsOrDynamic as DynamicModule).module;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
GLOBAL_MODULE_METADATA,
|
||||
} from '@nestjs/common/constants';
|
||||
import { Injectable, Type } from '@nestjs/common/interfaces';
|
||||
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
|
||||
import { ApplicationConfig } from '../application-config';
|
||||
import { DiscoverableMetaHostCollection } from '../discovery/discoverable-meta-host-collection';
|
||||
import {
|
||||
@@ -19,16 +20,16 @@ import { ContextId } from './instance-wrapper';
|
||||
import { InternalCoreModule } from './internal-core-module/internal-core-module';
|
||||
import { InternalProvidersStorage } from './internal-providers-storage';
|
||||
import { Module } from './module';
|
||||
import { ModuleTokenFactory } from './module-token-factory';
|
||||
import { ModulesContainer } from './modules-container';
|
||||
import { ByReferenceModuleOpaqueKeyFactory } from './opaque-key-factory/by-reference-module-opaque-key-factory';
|
||||
import { DeepHashedModuleOpaqueKeyFactory } from './opaque-key-factory/deep-hashed-module-opaque-key-factory';
|
||||
import { ModuleOpaqueKeyFactory } from './opaque-key-factory/interfaces/module-opaque-key-factory.interface';
|
||||
|
||||
type ModuleMetatype = Type<any> | DynamicModule | Promise<DynamicModule>;
|
||||
type ModuleScope = Type<any>[];
|
||||
|
||||
export class NestContainer {
|
||||
private readonly globalModules = new Set<Module>();
|
||||
private readonly moduleTokenFactory = new ModuleTokenFactory();
|
||||
private readonly moduleCompiler = new ModuleCompiler(this.moduleTokenFactory);
|
||||
private readonly modules = new ModulesContainer();
|
||||
private readonly dynamicModulesMetadata = new Map<
|
||||
string,
|
||||
@@ -36,11 +37,27 @@ export class NestContainer {
|
||||
>();
|
||||
private readonly internalProvidersStorage = new InternalProvidersStorage();
|
||||
private readonly _serializedGraph = new SerializedGraph();
|
||||
private moduleCompiler: ModuleCompiler;
|
||||
private internalCoreModule: Module;
|
||||
|
||||
constructor(
|
||||
private readonly _applicationConfig: ApplicationConfig = undefined,
|
||||
) {}
|
||||
private readonly _applicationConfig:
|
||||
| ApplicationConfig
|
||||
| undefined = undefined,
|
||||
private readonly _contextOptions:
|
||||
| NestApplicationContextOptions
|
||||
| undefined = undefined,
|
||||
) {
|
||||
const moduleOpaqueKeyFactory =
|
||||
this._contextOptions?.moduleIdGeneratorAlgorithm === 'deep-hash'
|
||||
? new DeepHashedModuleOpaqueKeyFactory()
|
||||
: new ByReferenceModuleOpaqueKeyFactory({
|
||||
keyGenerationStrategy: this._contextOptions?.snapshot
|
||||
? 'shallow'
|
||||
: 'random',
|
||||
});
|
||||
this.moduleCompiler = new ModuleCompiler(moduleOpaqueKeyFactory);
|
||||
}
|
||||
|
||||
get serializedGraph(): SerializedGraph {
|
||||
return this._serializedGraph;
|
||||
@@ -321,8 +338,8 @@ export class NestContainer {
|
||||
this.modules[InternalCoreModule.name] = moduleRef;
|
||||
}
|
||||
|
||||
public getModuleTokenFactory(): ModuleTokenFactory {
|
||||
return this.moduleTokenFactory;
|
||||
public getModuleTokenFactory(): ModuleOpaqueKeyFactory {
|
||||
return this.moduleCompiler.moduleOpaqueKeyFactory;
|
||||
}
|
||||
|
||||
public registerRequestProvider<T = any>(request: T, contextId: ContextId) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Logger } from '@nestjs/common';
|
||||
import { ExternalContextCreator } from '../../helpers/external-context-creator';
|
||||
import { HttpAdapterHost } from '../../helpers/http-adapter-host';
|
||||
import { GraphInspector } from '../../inspector/graph-inspector';
|
||||
import { InitializeOnPreviewAllowlist } from '../../inspector/initialize-on-preview.allowlist';
|
||||
import { SerializedGraph } from '../../inspector/serialized-graph';
|
||||
import { ModuleOverride } from '../../interfaces/module-override.interface';
|
||||
import { DependenciesScanner } from '../../scanner';
|
||||
@@ -42,6 +43,8 @@ export class InternalCoreModuleFactory {
|
||||
);
|
||||
};
|
||||
|
||||
InitializeOnPreviewAllowlist.add(InternalCoreModule);
|
||||
|
||||
return InternalCoreModule.register([
|
||||
{
|
||||
provide: ExternalContextCreator,
|
||||
|
||||
@@ -653,7 +653,12 @@ export class Module {
|
||||
|
||||
private generateUuid(): string {
|
||||
const prefix = 'M_';
|
||||
const key = this.name?.toString() ?? this.token?.toString();
|
||||
const key = this.token
|
||||
? this.token.includes(':')
|
||||
? this.token.split(':')[1]
|
||||
: this.token
|
||||
: this.name;
|
||||
|
||||
return key ? UuidFactory.get(`${prefix}_${key}`) : randomStringGenerator();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
|
||||
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
|
||||
import { Type } from '@nestjs/common/interfaces/type.interface';
|
||||
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
|
||||
import { createHash } from 'crypto';
|
||||
import { ModuleOpaqueKeyFactory } from './interfaces/module-opaque-key-factory.interface';
|
||||
|
||||
const K_MODULE_ID = Symbol('K_MODULE_ID');
|
||||
|
||||
export class ByReferenceModuleOpaqueKeyFactory
|
||||
implements ModuleOpaqueKeyFactory
|
||||
{
|
||||
private readonly keyGenerationStrategy: 'random' | 'shallow';
|
||||
|
||||
constructor(options?: { keyGenerationStrategy: 'random' | 'shallow' }) {
|
||||
this.keyGenerationStrategy = options?.keyGenerationStrategy ?? 'random';
|
||||
}
|
||||
|
||||
public createForStatic(
|
||||
moduleCls: Type,
|
||||
originalRef: Type | ForwardReference = moduleCls,
|
||||
): string {
|
||||
return this.getOrCreateModuleId(moduleCls, undefined, originalRef);
|
||||
}
|
||||
|
||||
public createForDynamic(
|
||||
moduleCls: Type<unknown>,
|
||||
dynamicMetadata: Omit<DynamicModule, 'module'>,
|
||||
originalRef: DynamicModule | ForwardReference,
|
||||
): string {
|
||||
return this.getOrCreateModuleId(moduleCls, dynamicMetadata, originalRef);
|
||||
}
|
||||
|
||||
private getOrCreateModuleId(
|
||||
moduleCls: Type<unknown>,
|
||||
dynamicMetadata: Partial<DynamicModule> | undefined,
|
||||
originalRef: Type | DynamicModule | ForwardReference,
|
||||
): string {
|
||||
if (originalRef[K_MODULE_ID]) {
|
||||
return originalRef[K_MODULE_ID];
|
||||
}
|
||||
|
||||
let moduleId: string;
|
||||
if (this.keyGenerationStrategy === 'random') {
|
||||
moduleId = this.generateRandomString();
|
||||
} else {
|
||||
moduleId = dynamicMetadata
|
||||
? `${this.generateRandomString()}:${this.hashString(moduleCls.name + JSON.stringify(dynamicMetadata))}`
|
||||
: `${this.generateRandomString()}:${this.hashString(moduleCls.toString())}`;
|
||||
}
|
||||
|
||||
originalRef[K_MODULE_ID] = moduleId;
|
||||
return moduleId;
|
||||
}
|
||||
|
||||
private hashString(value: string): string {
|
||||
return createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
private generateRandomString(): string {
|
||||
return randomStringGenerator();
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,28 @@
|
||||
import { DynamicModule, Logger } from '@nestjs/common';
|
||||
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
|
||||
import { Type } from '@nestjs/common/interfaces/type.interface';
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
|
||||
import { isFunction, isSymbol } from '@nestjs/common/utils/shared.utils';
|
||||
import { createHash } from 'crypto';
|
||||
import stringify from 'fast-safe-stringify';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { ModuleOpaqueKeyFactory } from './interfaces/module-opaque-key-factory.interface';
|
||||
|
||||
const CLASS_STR = 'class ';
|
||||
const CLASS_STR_LEN = CLASS_STR.length;
|
||||
|
||||
export class ModuleTokenFactory {
|
||||
private readonly moduleTokenCache = new Map<string, string>();
|
||||
export class DeepHashedModuleOpaqueKeyFactory
|
||||
implements ModuleOpaqueKeyFactory
|
||||
{
|
||||
private readonly moduleIdsCache = new WeakMap<Type<unknown>, string>();
|
||||
private readonly logger = new Logger(ModuleTokenFactory.name, {
|
||||
private readonly moduleTokenCache = new Map<string, string>();
|
||||
private readonly logger = new Logger(DeepHashedModuleOpaqueKeyFactory.name, {
|
||||
timestamp: true,
|
||||
});
|
||||
|
||||
public create(
|
||||
metatype: Type<unknown>,
|
||||
dynamicModuleMetadata?: Partial<DynamicModule> | undefined,
|
||||
): string {
|
||||
const moduleId = this.getModuleId(metatype);
|
||||
public createForStatic(moduleCls: Type): string {
|
||||
const moduleId = this.getModuleId(moduleCls);
|
||||
const moduleName = this.getModuleName(moduleCls);
|
||||
|
||||
if (!dynamicModuleMetadata) {
|
||||
return this.getStaticModuleToken(moduleId, this.getModuleName(metatype));
|
||||
}
|
||||
const opaqueToken = {
|
||||
id: moduleId,
|
||||
module: this.getModuleName(metatype),
|
||||
dynamic: dynamicModuleMetadata,
|
||||
};
|
||||
const start = performance.now();
|
||||
const opaqueTokenString = this.getStringifiedOpaqueToken(opaqueToken);
|
||||
const timeSpentInMs = performance.now() - start;
|
||||
|
||||
if (timeSpentInMs > 10) {
|
||||
const formattedTimeSpent = timeSpentInMs.toFixed(2);
|
||||
this.logger.warn(
|
||||
`The module "${opaqueToken.module}" is taking ${formattedTimeSpent}ms to serialize, this may be caused by larger objects statically assigned to the module. More details: https://github.com/nestjs/nest/issues/12738`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.hashString(opaqueTokenString);
|
||||
}
|
||||
|
||||
public getStaticModuleToken(moduleId: string, moduleName: string): string {
|
||||
const key = `${moduleId}_${moduleName}`;
|
||||
if (this.moduleTokenCache.has(key)) {
|
||||
return this.moduleTokenCache.get(key);
|
||||
@@ -55,6 +33,31 @@ export class ModuleTokenFactory {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public createForDynamic(
|
||||
moduleCls: Type<unknown>,
|
||||
dynamicMetadata: Omit<DynamicModule, 'module'>,
|
||||
): string {
|
||||
const moduleId = this.getModuleId(moduleCls);
|
||||
const moduleName = this.getModuleName(moduleCls);
|
||||
const opaqueToken = {
|
||||
id: moduleId,
|
||||
module: moduleName,
|
||||
dynamic: dynamicMetadata,
|
||||
};
|
||||
const start = performance.now();
|
||||
const opaqueTokenString = this.getStringifiedOpaqueToken(opaqueToken);
|
||||
const timeSpentInMs = performance.now() - start;
|
||||
|
||||
if (timeSpentInMs > 10) {
|
||||
const formattedTimeSpent = timeSpentInMs.toFixed(2);
|
||||
this.logger.warn(
|
||||
`The module "${opaqueToken.module}" is taking ${formattedTimeSpent}ms to serialize, this may be caused by larger objects statically assigned to the module. Consider changing the "moduleIdGeneratorAlgorithm" option to "reference" to improve the performance.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.hashString(opaqueTokenString);
|
||||
}
|
||||
|
||||
public getStringifiedOpaqueToken(opaqueToken: object | undefined): string {
|
||||
// Uses safeStringify instead of JSON.stringify to support circular dynamic modules
|
||||
// The replacer function is also required in order to obtain real class names
|
||||
@@ -0,0 +1,26 @@
|
||||
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
|
||||
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
|
||||
import { Type } from '@nestjs/common/interfaces/type.interface';
|
||||
|
||||
export interface ModuleOpaqueKeyFactory {
|
||||
/**
|
||||
* Creates a unique opaque key for the given static module.
|
||||
* @param moduleCls A static module class.
|
||||
* @param originalRef Original object reference. In most cases, it's the same as `moduleCls`.
|
||||
*/
|
||||
createForStatic(
|
||||
moduleCls: Type,
|
||||
originalRef: Type | ForwardReference,
|
||||
): string;
|
||||
/**
|
||||
* Creates a unique opaque key for the given dynamic module.
|
||||
* @param moduleCls A dynamic module class reference.
|
||||
* @param dynamicMetadata Dynamic module metadata.
|
||||
* @param originalRef Original object reference.
|
||||
*/
|
||||
createForDynamic(
|
||||
moduleCls: Type<unknown>,
|
||||
dynamicMetadata: Omit<DynamicModule, 'module'>,
|
||||
originalRef: DynamicModule | ForwardReference,
|
||||
): string;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Abstract,
|
||||
DynamicModule,
|
||||
GetOrResolveOptions,
|
||||
SelectOptions,
|
||||
Type,
|
||||
} from '@nestjs/common/interfaces';
|
||||
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
|
||||
@@ -50,7 +51,7 @@ export class NestApplicationContext<
|
||||
|
||||
private shouldFlushLogsOnOverride = false;
|
||||
private readonly activeShutdownSignals = new Array<string>();
|
||||
private readonly moduleCompiler = new ModuleCompiler();
|
||||
private readonly moduleCompiler: ModuleCompiler;
|
||||
private shutdownCleanupRef?: (...args: unknown[]) => unknown;
|
||||
private _instanceLinksHost: InstanceLinksHost;
|
||||
private _moduleRefsForHooksByDistance?: Array<Module>;
|
||||
@@ -70,6 +71,7 @@ export class NestApplicationContext<
|
||||
) {
|
||||
super();
|
||||
this.injector = new Injector();
|
||||
this.moduleCompiler = container.getModuleCompiler();
|
||||
|
||||
if (this.appOptions.preview) {
|
||||
this.printInPreviewModeWarning();
|
||||
@@ -87,6 +89,7 @@ export class NestApplicationContext<
|
||||
*/
|
||||
public select<T>(
|
||||
moduleType: Type<T> | DynamicModule,
|
||||
selectOptions?: SelectOptions,
|
||||
): INestApplicationContext {
|
||||
const modulesContainer = this.container.getModules();
|
||||
const contextModuleCtor = this.contextModule.metatype;
|
||||
@@ -95,15 +98,30 @@ export class NestApplicationContext<
|
||||
const moduleTokenFactory = this.container.getModuleTokenFactory();
|
||||
const { type, dynamicMetadata } =
|
||||
this.moduleCompiler.extractMetadata(moduleType);
|
||||
const token = moduleTokenFactory.create(type, dynamicMetadata);
|
||||
const token = dynamicMetadata
|
||||
? moduleTokenFactory.createForDynamic(
|
||||
type,
|
||||
dynamicMetadata,
|
||||
moduleType as DynamicModule,
|
||||
)
|
||||
: moduleTokenFactory.createForStatic(type, moduleType as Type);
|
||||
|
||||
const selectedModule = modulesContainer.get(token);
|
||||
if (!selectedModule) {
|
||||
throw new UnknownModuleException(type.name);
|
||||
}
|
||||
|
||||
const options =
|
||||
typeof selectOptions?.abortOnError !== 'undefined'
|
||||
? {
|
||||
...this.appOptions,
|
||||
...selectOptions,
|
||||
}
|
||||
: this.appOptions;
|
||||
|
||||
return new NestApplicationContext(
|
||||
this.container,
|
||||
this.appOptions,
|
||||
options,
|
||||
selectedModule,
|
||||
scope,
|
||||
);
|
||||
@@ -384,7 +402,10 @@ export class NestApplicationContext<
|
||||
* modules and its children.
|
||||
*/
|
||||
protected async callDestroyHook(): Promise<void> {
|
||||
const modulesSortedByDistance = this.getModulesToTriggerHooksOn();
|
||||
const modulesSortedByDistance = [
|
||||
...this.getModulesToTriggerHooksOn(),
|
||||
].reverse();
|
||||
|
||||
for (const module of modulesSortedByDistance) {
|
||||
await callModuleDestroyHook(module);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import {
|
||||
INestApplication,
|
||||
INestApplicationContext,
|
||||
INestMicroservice,
|
||||
DynamicModule,
|
||||
ForwardReference,
|
||||
Type,
|
||||
} from '@nestjs/common';
|
||||
import { NestMicroserviceOptions } from '@nestjs/common/interfaces/microservices/nest-microservice-options.interface';
|
||||
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
|
||||
@@ -27,6 +30,12 @@ import { NestApplication } from './nest-application';
|
||||
import { NestApplicationContext } from './nest-application-context';
|
||||
import { DependenciesScanner } from './scanner';
|
||||
|
||||
type IEntryNestModule =
|
||||
| Type<any>
|
||||
| DynamicModule
|
||||
| ForwardReference
|
||||
| Promise<IEntryNestModule>;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
@@ -47,7 +56,7 @@ export class NestFactoryStatic {
|
||||
* contains a reference to the NestApplication instance.
|
||||
*/
|
||||
public async create<T extends INestApplication = INestApplication>(
|
||||
module: any,
|
||||
module: IEntryNestModule,
|
||||
options?: NestApplicationOptions,
|
||||
): Promise<T>;
|
||||
/**
|
||||
@@ -62,12 +71,12 @@ export class NestFactoryStatic {
|
||||
* contains a reference to the NestApplication instance.
|
||||
*/
|
||||
public async create<T extends INestApplication = INestApplication>(
|
||||
module: any,
|
||||
module: IEntryNestModule,
|
||||
httpAdapter: AbstractHttpAdapter,
|
||||
options?: NestApplicationOptions,
|
||||
): Promise<T>;
|
||||
public async create<T extends INestApplication = INestApplication>(
|
||||
moduleCls: any,
|
||||
moduleCls: IEntryNestModule,
|
||||
serverOrOptions?: AbstractHttpAdapter | NestApplicationOptions,
|
||||
options?: NestApplicationOptions,
|
||||
): Promise<T> {
|
||||
@@ -112,7 +121,7 @@ export class NestFactoryStatic {
|
||||
* contains a reference to the NestMicroservice instance.
|
||||
*/
|
||||
public async createMicroservice<T extends object>(
|
||||
moduleCls: any,
|
||||
moduleCls: IEntryNestModule,
|
||||
options?: NestMicroserviceOptions & T,
|
||||
): Promise<INestMicroservice> {
|
||||
const { NestMicroservice } = loadPackage(
|
||||
@@ -154,7 +163,7 @@ export class NestFactoryStatic {
|
||||
* contains a reference to the NestApplicationContext instance.
|
||||
*/
|
||||
public async createApplicationContext(
|
||||
moduleCls: any,
|
||||
moduleCls: IEntryNestModule,
|
||||
options?: NestApplicationContextOptions,
|
||||
): Promise<INestApplicationContext> {
|
||||
const applicationConfig = new ApplicationConfig();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/core",
|
||||
"version": "10.4.3",
|
||||
"version": "10.4.7",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -36,7 +36,7 @@
|
||||
"uid": "2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "10.4.3"
|
||||
"@nestjs/common": "10.4.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
|
||||
@@ -92,10 +92,10 @@ export class DependenciesScanner {
|
||||
overrides: options?.overrides,
|
||||
});
|
||||
await this.scanModulesForDependencies();
|
||||
this.calculateModulesDistance();
|
||||
|
||||
this.addScopedEnhancersMetadata();
|
||||
this.container.bindGlobalScope();
|
||||
|
||||
this.calculateModulesDistance();
|
||||
}
|
||||
|
||||
public async scanForModules({
|
||||
@@ -389,12 +389,16 @@ export class DependenciesScanner {
|
||||
// Skip "InternalCoreModule" from calculating distance
|
||||
modulesGenerator.next();
|
||||
|
||||
const modulesStack = [];
|
||||
const calculateDistance = (moduleRef: Module, distance = 1) => {
|
||||
if (!moduleRef || modulesStack.includes(moduleRef)) {
|
||||
const calculateDistance = (
|
||||
moduleRef: Module,
|
||||
distance = 1,
|
||||
modulesStack = [],
|
||||
) => {
|
||||
const localModulesStack = [...modulesStack];
|
||||
if (!moduleRef || localModulesStack.includes(moduleRef)) {
|
||||
return;
|
||||
}
|
||||
modulesStack.push(moduleRef);
|
||||
localModulesStack.push(moduleRef);
|
||||
|
||||
const moduleImports = moduleRef.imports;
|
||||
moduleImports.forEach(importedModuleRef => {
|
||||
@@ -402,7 +406,7 @@ export class DependenciesScanner {
|
||||
if (distance > importedModuleRef.distance) {
|
||||
importedModuleRef.distance = distance;
|
||||
}
|
||||
calculateDistance(importedModuleRef, distance + 1);
|
||||
calculateDistance(importedModuleRef, distance + 1, localModulesStack);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -127,14 +127,10 @@ export class Reflector {
|
||||
* @param targets context (decorated objects) to retrieve metadata from
|
||||
*
|
||||
*/
|
||||
public getAll<T extends ReflectableDecorator<any>>(
|
||||
decorator: T,
|
||||
public getAll<TParam = any, TTransformed = TParam>(
|
||||
decorator: ReflectableDecorator<TParam, TTransformed>,
|
||||
targets: (Type<any> | Function)[],
|
||||
): T extends ReflectableDecorator<infer R>
|
||||
? R extends Array<any>
|
||||
? R
|
||||
: R[]
|
||||
: unknown;
|
||||
): TTransformed extends Array<any> ? TTransformed : TTransformed[];
|
||||
/**
|
||||
* Retrieve metadata for a specified key for a specified set of targets.
|
||||
*
|
||||
@@ -169,10 +165,14 @@ export class Reflector {
|
||||
* @param targets context (decorated objects) to retrieve metadata from
|
||||
*
|
||||
*/
|
||||
public getAllAndMerge<T extends ReflectableDecorator<any>>(
|
||||
decorator: T,
|
||||
public getAllAndMerge<TParam = any, TTransformed = TParam>(
|
||||
decorator: ReflectableDecorator<TParam, TTransformed>,
|
||||
targets: (Type<any> | Function)[],
|
||||
): T extends ReflectableDecorator<infer R> ? R : unknown;
|
||||
): TTransformed extends Array<any>
|
||||
? TTransformed
|
||||
: TTransformed extends object
|
||||
? TTransformed
|
||||
: TTransformed[];
|
||||
/**
|
||||
* Retrieve metadata for a specified key for a specified set of targets and merge results.
|
||||
*
|
||||
@@ -203,6 +203,13 @@ export class Reflector {
|
||||
if (isEmpty(metadataCollection)) {
|
||||
return metadataCollection as TResult;
|
||||
}
|
||||
if (metadataCollection.length === 1) {
|
||||
const value = metadataCollection[0];
|
||||
if (isObject(value)) {
|
||||
return value as TResult;
|
||||
}
|
||||
return metadataCollection as TResult;
|
||||
}
|
||||
return metadataCollection.reduce((a, b) => {
|
||||
if (Array.isArray(a)) {
|
||||
return a.concat(b);
|
||||
@@ -224,10 +231,10 @@ export class Reflector {
|
||||
* @param targets context (decorated objects) to retrieve metadata from
|
||||
*
|
||||
*/
|
||||
public getAllAndOverride<T extends ReflectableDecorator<any>>(
|
||||
decorator: T,
|
||||
public getAllAndOverride<TParam = any, TTransformed = TParam>(
|
||||
decorator: ReflectableDecorator<TParam, TTransformed>,
|
||||
targets: (Type<any> | Function)[],
|
||||
): T extends ReflectableDecorator<infer R> ? R : unknown;
|
||||
): TTransformed;
|
||||
/**
|
||||
* Retrieve metadata for a specified key for a specified set of targets and return a first not undefined value.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as sinon from 'sinon';
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { ExceptionHandler } from '../../../errors/exception-handler';
|
||||
import { RuntimeException } from '../../../errors/exceptions/runtime.exception';
|
||||
import { InvalidMiddlewareException } from '../../../errors/exceptions/invalid-middleware.exception';
|
||||
|
||||
describe('ExceptionHandler', () => {
|
||||
let instance: ExceptionHandler;
|
||||
@@ -10,7 +9,7 @@ describe('ExceptionHandler', () => {
|
||||
instance = new ExceptionHandler();
|
||||
});
|
||||
describe('handle', () => {
|
||||
let logger;
|
||||
let logger: { error: Function };
|
||||
let errorSpy: sinon.SinonSpy;
|
||||
beforeEach(() => {
|
||||
logger = {
|
||||
@@ -19,16 +18,10 @@ describe('ExceptionHandler', () => {
|
||||
(ExceptionHandler as any).logger = logger;
|
||||
errorSpy = sinon.spy(logger, 'error');
|
||||
});
|
||||
it('when exception is instanceof RuntimeException', () => {
|
||||
it('should call the logger.error method with the thrown exception passed as an argument', () => {
|
||||
const exception = new RuntimeException('msg');
|
||||
instance.handle(exception);
|
||||
expect(errorSpy.calledWith(exception.message, exception.stack)).to.be
|
||||
.true;
|
||||
});
|
||||
it('when exception is not instanceof RuntimeException', () => {
|
||||
const exception = new InvalidMiddlewareException('msg');
|
||||
instance.handle(exception);
|
||||
expect(errorSpy.calledWith(exception.what(), exception.stack)).to.be.true;
|
||||
expect(errorSpy.calledWith(exception)).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,13 @@ describe('RouterMethodFactory', () => {
|
||||
patch: () => {},
|
||||
options: () => {},
|
||||
head: () => {},
|
||||
propfind: () => {},
|
||||
proppatch: () => {},
|
||||
mkcol: () => {},
|
||||
copy: () => {},
|
||||
move: () => {},
|
||||
lock: () => {},
|
||||
unlock: () => {},
|
||||
all: () => {},
|
||||
};
|
||||
beforeEach(() => {
|
||||
@@ -29,6 +36,17 @@ describe('RouterMethodFactory', () => {
|
||||
expect(factory.get(target, RequestMethod.PATCH)).to.equal(target.patch);
|
||||
expect(factory.get(target, RequestMethod.OPTIONS)).to.equal(target.options);
|
||||
expect(factory.get(target, RequestMethod.HEAD)).to.equal(target.head);
|
||||
expect(factory.get(target, RequestMethod.PROPFIND)).to.equal(
|
||||
target.propfind,
|
||||
);
|
||||
expect(factory.get(target, RequestMethod.PROPPATCH)).to.equal(
|
||||
target.proppatch,
|
||||
);
|
||||
expect(factory.get(target, RequestMethod.MKCOL)).to.equal(target.mkcol);
|
||||
expect(factory.get(target, RequestMethod.COPY)).to.equal(target.copy);
|
||||
expect(factory.get(target, RequestMethod.MOVE)).to.equal(target.move);
|
||||
expect(factory.get(target, RequestMethod.LOCK)).to.equal(target.lock);
|
||||
expect(factory.get(target, RequestMethod.UNLOCK)).to.equal(target.unlock);
|
||||
expect(factory.get(target, -1 as any)).to.equal(target.use);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
import { expect } from 'chai';
|
||||
import { ModuleCompiler } from '../../injector/compiler';
|
||||
import { ByReferenceModuleOpaqueKeyFactory } from '../../injector/opaque-key-factory/by-reference-module-opaque-key-factory';
|
||||
|
||||
describe('ModuleCompiler', () => {
|
||||
let compiler: ModuleCompiler;
|
||||
beforeEach(() => {
|
||||
compiler = new ModuleCompiler();
|
||||
compiler = new ModuleCompiler(new ByReferenceModuleOpaqueKeyFactory());
|
||||
});
|
||||
|
||||
describe('extractMetadata', () => {
|
||||
describe('when module is a dynamic module', () => {
|
||||
it('should return object with "type" and "dynamicMetadata" property', async () => {
|
||||
it('should return object with "type" and "dynamicMetadata" property', () => {
|
||||
const obj = { module: 'test', providers: [] };
|
||||
const { module, ...dynamicMetadata } = obj;
|
||||
expect(await compiler.extractMetadata(obj as any)).to.be.deep.equal({
|
||||
expect(compiler.extractMetadata(obj as any)).to.be.deep.equal({
|
||||
type: module,
|
||||
dynamicMetadata,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when module is a not dynamic module', () => {
|
||||
it('should return object with "type" property', async () => {
|
||||
it('should return object with "type" property', () => {
|
||||
const type = 'test';
|
||||
expect(await compiler.extractMetadata(type as any)).to.be.deep.equal({
|
||||
expect(compiler.extractMetadata(type as any)).to.be.deep.equal({
|
||||
type,
|
||||
dynamicMetadata: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NoopHttpAdapter } from '../utils/noop-adapter.spec';
|
||||
|
||||
describe('NestContainer', () => {
|
||||
let container: NestContainer;
|
||||
let untypedContainer: any;
|
||||
|
||||
@Module({})
|
||||
class TestModule {}
|
||||
@@ -19,6 +20,7 @@ describe('NestContainer', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
container = new NestContainer();
|
||||
untypedContainer = container as any;
|
||||
});
|
||||
|
||||
it('should "addProvider" throw "UnknownModuleException" when module is not stored in collection', () => {
|
||||
@@ -53,7 +55,7 @@ describe('NestContainer', () => {
|
||||
|
||||
describe('clear', () => {
|
||||
it('should call `clear` on modules collection', () => {
|
||||
const clearSpy = sinon.spy((container as any).modules, 'clear');
|
||||
const clearSpy = sinon.spy(untypedContainer.modules, 'clear');
|
||||
container.clear();
|
||||
expect(clearSpy.called).to.be.true;
|
||||
});
|
||||
@@ -63,7 +65,7 @@ describe('NestContainer', () => {
|
||||
it('should not add module if already exists in collection', async () => {
|
||||
const modules = new Map();
|
||||
const setSpy = sinon.spy(modules, 'set');
|
||||
(container as any).modules = modules;
|
||||
untypedContainer.modules = modules;
|
||||
|
||||
await container.addModule(TestModule as any, []);
|
||||
await container.addModule(TestModule as any, []);
|
||||
@@ -89,7 +91,7 @@ describe('NestContainer', () => {
|
||||
|
||||
const modules = new Map();
|
||||
const setSpy = sinon.spy(modules, 'set');
|
||||
(container as any).modules = modules;
|
||||
untypedContainer.modules = modules;
|
||||
|
||||
await container.addModule(TestModule as any, []);
|
||||
await container.replaceModule(
|
||||
@@ -174,7 +176,7 @@ describe('NestContainer', () => {
|
||||
beforeEach(() => {
|
||||
token = 'token';
|
||||
collection = new Map();
|
||||
(container as any).dynamicModulesMetadata = collection;
|
||||
untypedContainer.dynamicModulesMetadata = collection;
|
||||
});
|
||||
describe('when dynamic metadata exists', () => {
|
||||
it('should add to the dynamic metadata collection', () => {
|
||||
@@ -215,7 +217,7 @@ describe('NestContainer', () => {
|
||||
describe('get applicationConfig', () => {
|
||||
it('should return ApplicationConfig instance', () => {
|
||||
expect(container.applicationConfig).to.be.eql(
|
||||
(container as any)._applicationConfig,
|
||||
untypedContainer._applicationConfig,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -225,7 +227,7 @@ describe('NestContainer', () => {
|
||||
const httpAdapter = new NoopHttpAdapter({});
|
||||
container.setHttpAdapter(httpAdapter);
|
||||
|
||||
const internalStorage = (container as any).internalProvidersStorage;
|
||||
const internalStorage = untypedContainer.internalProvidersStorage;
|
||||
expect(internalStorage.httpAdapter).to.be.eql(httpAdapter);
|
||||
});
|
||||
});
|
||||
@@ -244,7 +246,7 @@ describe('NestContainer', () => {
|
||||
it('should register core module ref', () => {
|
||||
const ref = {} as any;
|
||||
container.registerCoreModuleRef(ref);
|
||||
expect((container as any).internalCoreModule).to.be.eql(ref);
|
||||
expect(untypedContainer.internalCoreModule).to.be.eql(ref);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,8 @@ import { InstanceWrapper } from '../../injector/instance-wrapper';
|
||||
import { Module } from '../../injector/module';
|
||||
|
||||
describe('Module', () => {
|
||||
let module: Module;
|
||||
let moduleRef: Module;
|
||||
let untypedModuleRef: any;
|
||||
let container: NestContainer;
|
||||
|
||||
@ModuleDecorator({})
|
||||
@@ -22,23 +23,24 @@ describe('Module', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
container = new NestContainer();
|
||||
module = new Module(TestModule, container);
|
||||
moduleRef = new Module(TestModule, container);
|
||||
untypedModuleRef = moduleRef as any;
|
||||
});
|
||||
|
||||
it('should add controller', () => {
|
||||
const collection = new Map();
|
||||
const setSpy = sinon.spy(collection, 'set');
|
||||
(module as any)._controllers = collection;
|
||||
untypedModuleRef._controllers = collection;
|
||||
|
||||
@Controller({ scope: Scope.REQUEST, durable: true })
|
||||
class Test {}
|
||||
|
||||
module.addController(Test);
|
||||
moduleRef.addController(Test);
|
||||
expect(
|
||||
setSpy.calledWith(
|
||||
Test,
|
||||
new InstanceWrapper({
|
||||
host: module,
|
||||
host: moduleRef,
|
||||
token: Test,
|
||||
name: 'Test',
|
||||
scope: Scope.REQUEST,
|
||||
@@ -54,14 +56,14 @@ describe('Module', () => {
|
||||
it('should add injectable', () => {
|
||||
const collection = new Map();
|
||||
const setSpy = sinon.spy(collection, 'set');
|
||||
(module as any)._injectables = collection;
|
||||
untypedModuleRef._injectables = collection;
|
||||
|
||||
module.addInjectable(TestProvider, 'interceptor', TestModule);
|
||||
moduleRef.addInjectable(TestProvider, 'interceptor', TestModule);
|
||||
expect(
|
||||
setSpy.calledWith(
|
||||
TestProvider,
|
||||
new InstanceWrapper({
|
||||
host: module,
|
||||
host: moduleRef,
|
||||
name: 'TestProvider',
|
||||
token: TestProvider,
|
||||
scope: undefined,
|
||||
@@ -77,9 +79,9 @@ describe('Module', () => {
|
||||
|
||||
describe('when injectable is custom provided', () => {
|
||||
it('should call `addCustomProvider`', () => {
|
||||
const addCustomProviderSpy = sinon.spy(module, 'addCustomProvider');
|
||||
const addCustomProviderSpy = sinon.spy(moduleRef, 'addCustomProvider');
|
||||
|
||||
module.addInjectable({ provide: 'test' } as any, 'guard');
|
||||
moduleRef.addInjectable({ provide: 'test' } as any, 'guard');
|
||||
expect(addCustomProviderSpy.called).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -87,14 +89,14 @@ describe('Module', () => {
|
||||
it('should add provider', () => {
|
||||
const collection = new Map();
|
||||
const setSpy = sinon.spy(collection, 'set');
|
||||
(module as any)._providers = collection;
|
||||
untypedModuleRef._providers = collection;
|
||||
|
||||
module.addProvider(TestProvider);
|
||||
moduleRef.addProvider(TestProvider);
|
||||
expect(
|
||||
setSpy.calledWith(
|
||||
TestProvider,
|
||||
new InstanceWrapper({
|
||||
host: module,
|
||||
host: moduleRef,
|
||||
name: 'TestProvider',
|
||||
token: TestProvider,
|
||||
scope: undefined,
|
||||
@@ -109,81 +111,81 @@ describe('Module', () => {
|
||||
|
||||
it('should call "addCustomProvider" when "provide" property exists', () => {
|
||||
const addCustomProvider = sinon.spy();
|
||||
module.addCustomProvider = addCustomProvider;
|
||||
moduleRef.addCustomProvider = addCustomProvider;
|
||||
|
||||
const provider = { provide: 'test', useValue: 'test' };
|
||||
|
||||
module.addProvider(provider as any);
|
||||
moduleRef.addProvider(provider as any);
|
||||
expect((addCustomProvider as sinon.SinonSpy).called).to.be.true;
|
||||
});
|
||||
|
||||
it('should call "addCustomClass" when "useClass" property exists', () => {
|
||||
const addCustomClass = sinon.spy();
|
||||
module.addCustomClass = addCustomClass;
|
||||
moduleRef.addCustomClass = addCustomClass;
|
||||
|
||||
const provider = { provide: 'test', useClass: () => null };
|
||||
|
||||
module.addCustomProvider(provider as any, new Map());
|
||||
moduleRef.addCustomProvider(provider as any, new Map());
|
||||
expect((addCustomClass as sinon.SinonSpy).called).to.be.true;
|
||||
});
|
||||
|
||||
it('should call "addCustomValue" when "useValue" property exists', () => {
|
||||
const addCustomValue = sinon.spy();
|
||||
module.addCustomValue = addCustomValue;
|
||||
moduleRef.addCustomValue = addCustomValue;
|
||||
|
||||
const provider = { provide: 'test', useValue: () => null };
|
||||
|
||||
module.addCustomProvider(provider as any, new Map());
|
||||
moduleRef.addCustomProvider(provider as any, new Map());
|
||||
expect((addCustomValue as sinon.SinonSpy).called).to.be.true;
|
||||
});
|
||||
|
||||
it('should call "addCustomValue" when "useValue" property exists but its value is `undefined`', () => {
|
||||
const addCustomValue = sinon.spy();
|
||||
module.addCustomValue = addCustomValue;
|
||||
moduleRef.addCustomValue = addCustomValue;
|
||||
|
||||
const provider = { provide: 'test', useValue: undefined };
|
||||
|
||||
module.addCustomProvider(provider as any, new Map());
|
||||
moduleRef.addCustomProvider(provider as any, new Map());
|
||||
expect((addCustomValue as sinon.SinonSpy).called).to.be.true;
|
||||
});
|
||||
|
||||
it('should call "addCustomFactory" when "useFactory" property exists', () => {
|
||||
const addCustomFactory = sinon.spy();
|
||||
module.addCustomFactory = addCustomFactory;
|
||||
moduleRef.addCustomFactory = addCustomFactory;
|
||||
|
||||
const provider = { provide: 'test', useFactory: () => null };
|
||||
|
||||
module.addCustomProvider(provider as any, new Map());
|
||||
moduleRef.addCustomProvider(provider as any, new Map());
|
||||
expect((addCustomFactory as sinon.SinonSpy).called).to.be.true;
|
||||
});
|
||||
|
||||
it('should call "addCustomUseExisting" when "useExisting" property exists', () => {
|
||||
const addCustomUseExisting = sinon.spy();
|
||||
module.addCustomUseExisting = addCustomUseExisting;
|
||||
moduleRef.addCustomUseExisting = addCustomUseExisting;
|
||||
|
||||
const provider = { provide: 'test', useExisting: () => null };
|
||||
|
||||
module.addCustomUseExisting(provider as any, new Map());
|
||||
moduleRef.addCustomUseExisting(provider as any, new Map());
|
||||
expect((addCustomUseExisting as sinon.SinonSpy).called).to.be.true;
|
||||
});
|
||||
|
||||
describe('addCustomClass', () => {
|
||||
const type = { name: 'TypeTest' };
|
||||
const provider = { provide: type, useClass: type, durable: true };
|
||||
let setSpy;
|
||||
let setSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
const collection = new Map();
|
||||
setSpy = sinon.spy(collection, 'set');
|
||||
(module as any)._providers = collection;
|
||||
untypedModuleRef._providers = collection;
|
||||
});
|
||||
it('should store provider', () => {
|
||||
module.addCustomClass(provider as any, (module as any)._providers);
|
||||
moduleRef.addCustomClass(provider as any, untypedModuleRef._providers);
|
||||
expect(
|
||||
setSpy.calledWith(
|
||||
provider.provide,
|
||||
new InstanceWrapper({
|
||||
host: module,
|
||||
host: moduleRef,
|
||||
token: type as any,
|
||||
name: provider.provide.name,
|
||||
scope: undefined,
|
||||
@@ -199,23 +201,23 @@ describe('Module', () => {
|
||||
});
|
||||
|
||||
describe('addCustomValue', () => {
|
||||
let setSpy;
|
||||
let setSpy: sinon.SinonSpy;
|
||||
const value = () => ({});
|
||||
const provider = { provide: value, useValue: value };
|
||||
|
||||
beforeEach(() => {
|
||||
const collection = new Map();
|
||||
setSpy = sinon.spy(collection, 'set');
|
||||
(module as any)._providers = collection;
|
||||
untypedModuleRef._providers = collection;
|
||||
});
|
||||
|
||||
it('should store provider', () => {
|
||||
module.addCustomValue(provider as any, (module as any)._providers);
|
||||
moduleRef.addCustomValue(provider as any, untypedModuleRef._providers);
|
||||
expect(
|
||||
setSpy.calledWith(
|
||||
provider.provide,
|
||||
new InstanceWrapper({
|
||||
host: module,
|
||||
host: moduleRef,
|
||||
token: provider.provide,
|
||||
name: provider.provide.name,
|
||||
scope: Scope.DEFAULT,
|
||||
@@ -235,20 +237,20 @@ describe('Module', () => {
|
||||
const inject = [1, 2, 3];
|
||||
const provider = { provide: type, useFactory: type, inject, durable: true };
|
||||
|
||||
let setSpy;
|
||||
let setSpy: sinon.SinonSpy;
|
||||
beforeEach(() => {
|
||||
const collection = new Map();
|
||||
setSpy = sinon.spy(collection, 'set');
|
||||
(module as any)._providers = collection;
|
||||
untypedModuleRef._providers = collection;
|
||||
});
|
||||
it('should store provider', () => {
|
||||
module.addCustomFactory(provider as any, (module as any)._providers);
|
||||
moduleRef.addCustomFactory(provider as any, untypedModuleRef._providers);
|
||||
|
||||
expect(
|
||||
setSpy.calledWith(
|
||||
provider.provide,
|
||||
new InstanceWrapper({
|
||||
host: module,
|
||||
host: moduleRef,
|
||||
token: provider.provide as any,
|
||||
name: provider.provide.name,
|
||||
scope: undefined,
|
||||
@@ -268,15 +270,18 @@ describe('Module', () => {
|
||||
const type = { name: 'TypeTest' };
|
||||
const provider = { provide: type, useExisting: type };
|
||||
|
||||
let setSpy;
|
||||
let setSpy: sinon.SinonSpy;
|
||||
beforeEach(() => {
|
||||
const collection = new Map();
|
||||
setSpy = sinon.spy(collection, 'set');
|
||||
(module as any)._providers = collection;
|
||||
untypedModuleRef._providers = collection;
|
||||
});
|
||||
it('should store provider', () => {
|
||||
module.addCustomUseExisting(provider as any, (module as any)._providers);
|
||||
const factoryFn = (module as any)._providers.get(
|
||||
moduleRef.addCustomUseExisting(
|
||||
provider as any,
|
||||
untypedModuleRef._providers,
|
||||
);
|
||||
const factoryFn = untypedModuleRef._providers.get(
|
||||
provider.provide,
|
||||
).metatype;
|
||||
|
||||
@@ -285,7 +290,7 @@ describe('Module', () => {
|
||||
setSpy.calledWith(
|
||||
token,
|
||||
new InstanceWrapper({
|
||||
host: module,
|
||||
host: moduleRef,
|
||||
token,
|
||||
name: provider.provide.name,
|
||||
metatype: factoryFn,
|
||||
@@ -304,41 +309,41 @@ describe('Module', () => {
|
||||
describe('when get instance', () => {
|
||||
describe('when metatype does not exists in providers collection', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub((module as any)._providers, 'has').returns(false);
|
||||
sinon.stub(untypedModuleRef._providers, 'has').returns(false);
|
||||
});
|
||||
it('should throw RuntimeException', () => {
|
||||
expect(() => module.instance).to.throws(RuntimeException);
|
||||
expect(() => moduleRef.instance).to.throws(RuntimeException);
|
||||
});
|
||||
});
|
||||
describe('when metatype exists in providers collection', () => {
|
||||
it('should return null', () => {
|
||||
expect(module.instance).to.be.eql(null);
|
||||
expect(moduleRef.instance).to.be.eql(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when exported provider is custom provided', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(module, 'validateExportedProvider').callsFake(o => o);
|
||||
sinon.stub(moduleRef, 'validateExportedProvider').callsFake(o => o);
|
||||
});
|
||||
it('should call `addCustomExportedProvider`', () => {
|
||||
const addCustomExportedProviderSpy = sinon.spy(
|
||||
module,
|
||||
moduleRef,
|
||||
'addCustomExportedProvider',
|
||||
);
|
||||
|
||||
module.addExportedProvider({ provide: 'test' } as any);
|
||||
moduleRef.addExportedProvider({ provide: 'test' } as any);
|
||||
expect(addCustomExportedProviderSpy.called).to.be.true;
|
||||
});
|
||||
it('should support symbols', () => {
|
||||
const addCustomExportedProviderSpy = sinon.spy(
|
||||
module,
|
||||
moduleRef,
|
||||
'addCustomExportedProvider',
|
||||
);
|
||||
const symb = Symbol('test');
|
||||
module.addExportedProvider({ provide: symb } as any);
|
||||
moduleRef.addExportedProvider({ provide: symb } as any);
|
||||
expect(addCustomExportedProviderSpy.called).to.be.true;
|
||||
expect((module as any)._exports.has(symb)).to.be.true;
|
||||
expect(untypedModuleRef._exports.has(symb)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -348,10 +353,10 @@ describe('Module', () => {
|
||||
const wrapper = {
|
||||
mergeWith: sinon.spy(),
|
||||
};
|
||||
sinon.stub(module, 'hasProvider').callsFake(() => true);
|
||||
sinon.stub(module.providers, 'get').callsFake(() => wrapper as any);
|
||||
sinon.stub(moduleRef, 'hasProvider').callsFake(() => true);
|
||||
sinon.stub(moduleRef.providers, 'get').callsFake(() => wrapper as any);
|
||||
|
||||
module.replace(null, { isProvider: true });
|
||||
moduleRef.replace(null, { isProvider: true });
|
||||
expect(wrapper.mergeWith.called).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -361,10 +366,12 @@ describe('Module', () => {
|
||||
mergeWith: sinon.spy(),
|
||||
isProvider: true,
|
||||
};
|
||||
sinon.stub(module, 'hasInjectable').callsFake(() => true);
|
||||
sinon.stub(module.injectables, 'get').callsFake(() => wrapper as any);
|
||||
sinon.stub(moduleRef, 'hasInjectable').callsFake(() => true);
|
||||
sinon
|
||||
.stub(moduleRef.injectables, 'get')
|
||||
.callsFake(() => wrapper as any);
|
||||
|
||||
module.replace(null, {});
|
||||
moduleRef.replace(null, {});
|
||||
expect(wrapper.mergeWith.called).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -373,61 +380,63 @@ describe('Module', () => {
|
||||
describe('imports', () => {
|
||||
it('should return relatedModules', () => {
|
||||
const test = ['test'];
|
||||
(module as any)._imports = test;
|
||||
untypedModuleRef._imports = test;
|
||||
|
||||
expect(module.imports).to.be.eql(test);
|
||||
expect(moduleRef.imports).to.be.eql(test);
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectables', () => {
|
||||
it('should return injectables', () => {
|
||||
const test = ['test'];
|
||||
(module as any)._injectables = test;
|
||||
expect(module.injectables).to.be.eql(test);
|
||||
untypedModuleRef._injectables = test;
|
||||
expect(moduleRef.injectables).to.be.eql(test);
|
||||
});
|
||||
});
|
||||
|
||||
describe('controllers', () => {
|
||||
it('should return controllers', () => {
|
||||
const test = ['test'];
|
||||
(module as any)._controllers = test;
|
||||
untypedModuleRef._controllers = test;
|
||||
|
||||
expect(module.controllers).to.be.eql(test);
|
||||
expect(moduleRef.controllers).to.be.eql(test);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exports', () => {
|
||||
it('should return exports', () => {
|
||||
const test = ['test'];
|
||||
(module as any)._exports = test;
|
||||
untypedModuleRef._exports = test;
|
||||
|
||||
expect(module.exports).to.be.eql(test);
|
||||
expect(moduleRef.exports).to.be.eql(test);
|
||||
});
|
||||
});
|
||||
|
||||
describe('providers', () => {
|
||||
it('should return providers', () => {
|
||||
const test = ['test'];
|
||||
(module as any)._providers = test;
|
||||
untypedModuleRef._providers = test;
|
||||
|
||||
expect(module.providers).to.be.eql(test);
|
||||
expect(moduleRef.providers).to.be.eql(test);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createModuleReferenceType', () => {
|
||||
let moduleRef: any;
|
||||
let customModuleRef: any;
|
||||
|
||||
beforeEach(() => {
|
||||
const Class = module.createModuleReferenceType();
|
||||
moduleRef = new Class();
|
||||
const Class = moduleRef.createModuleReferenceType();
|
||||
customModuleRef = new Class();
|
||||
});
|
||||
|
||||
it('should return metatype with "get" method', () => {
|
||||
expect(!!moduleRef.get).to.be.true;
|
||||
expect(!!customModuleRef.get).to.be.true;
|
||||
});
|
||||
describe('get', () => {
|
||||
it('should throw exception if not exists', () => {
|
||||
expect(() => moduleRef.get('fail')).to.throws(UnknownElementException);
|
||||
expect(() => customModuleRef.get('fail')).to.throws(
|
||||
UnknownElementException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -436,22 +445,22 @@ describe('Module', () => {
|
||||
|
||||
describe('when unit exists in provider collection', () => {
|
||||
it('should behave as identity', () => {
|
||||
(module as any)._providers = new Map([[token, true]]);
|
||||
expect(module.validateExportedProvider(token)).to.be.eql(token);
|
||||
untypedModuleRef._providers = new Map([[token, true]]);
|
||||
expect(moduleRef.validateExportedProvider(token)).to.be.eql(token);
|
||||
});
|
||||
});
|
||||
describe('when unit exists in related modules collection', () => {
|
||||
it('should behave as identity', () => {
|
||||
class Random {}
|
||||
(module as any)._imports = new Set([
|
||||
untypedModuleRef._imports = new Set([
|
||||
new Module(Random, new NestContainer()),
|
||||
]);
|
||||
expect(module.validateExportedProvider(Random)).to.be.eql(Random);
|
||||
expect(moduleRef.validateExportedProvider(Random)).to.be.eql(Random);
|
||||
});
|
||||
});
|
||||
describe('when unit does not exist in both provider and related modules collections', () => {
|
||||
it('should throw UnknownExportException', () => {
|
||||
expect(() => module.validateExportedProvider(token)).to.throws(
|
||||
expect(() => moduleRef.validateExportedProvider(token)).to.throws(
|
||||
UnknownExportException,
|
||||
);
|
||||
});
|
||||
@@ -462,13 +471,13 @@ describe('Module', () => {
|
||||
describe('when module has provider', () => {
|
||||
it('should return true', () => {
|
||||
const token = 'test';
|
||||
module.providers.set(token, new InstanceWrapper());
|
||||
expect(module.hasProvider(token)).to.be.true;
|
||||
moduleRef.providers.set(token, new InstanceWrapper());
|
||||
expect(moduleRef.hasProvider(token)).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('otherwise', () => {
|
||||
it('should return false', () => {
|
||||
expect(module.hasProvider('_')).to.be.false;
|
||||
expect(moduleRef.hasProvider('_')).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -477,33 +486,33 @@ describe('Module', () => {
|
||||
describe('when module has injectable', () => {
|
||||
it('should return true', () => {
|
||||
const token = 'test';
|
||||
module.injectables.set(token, new InstanceWrapper());
|
||||
expect(module.hasInjectable(token)).to.be.true;
|
||||
moduleRef.injectables.set(token, new InstanceWrapper());
|
||||
expect(moduleRef.hasInjectable(token)).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('otherwise', () => {
|
||||
it('should return false', () => {
|
||||
expect(module.hasInjectable('_')).to.be.false;
|
||||
expect(moduleRef.hasInjectable('_')).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getter "id"', () => {
|
||||
it('should return module id', () => {
|
||||
expect(module.id).to.be.equal(module['_id']);
|
||||
expect(moduleRef.id).to.be.equal(moduleRef['_id']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviderByKey', () => {
|
||||
describe('when does not exist', () => {
|
||||
it('should return undefined', () => {
|
||||
expect(module.getProviderByKey('test')).to.be.undefined;
|
||||
expect(moduleRef.getProviderByKey('test')).to.be.undefined;
|
||||
});
|
||||
});
|
||||
describe('otherwise', () => {
|
||||
it('should return instance wrapper', () => {
|
||||
module.addProvider(TestProvider);
|
||||
expect(module.getProviderByKey(TestProvider)).to.not.be.undefined;
|
||||
moduleRef.addProvider(TestProvider);
|
||||
expect(moduleRef.getProviderByKey(TestProvider)).to.not.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { ByReferenceModuleOpaqueKeyFactory } from '../../../injector/opaque-key-factory/by-reference-module-opaque-key-factory';
|
||||
|
||||
describe('ByReferenceModuleOpaqueKeyFactory', () => {
|
||||
const moduleId = 'constId';
|
||||
let factory: ByReferenceModuleOpaqueKeyFactory;
|
||||
|
||||
describe('when generating algorithm is random', () => {
|
||||
beforeEach(() => {
|
||||
factory = new ByReferenceModuleOpaqueKeyFactory();
|
||||
sinon.stub(factory as any, 'generateRandomString').returns(moduleId);
|
||||
});
|
||||
|
||||
describe('createForStatic', () => {
|
||||
class Module {}
|
||||
|
||||
it('should return expected token', () => {
|
||||
const type = Module;
|
||||
const token1 = factory.createForStatic(type);
|
||||
const token2 = factory.createForStatic(type);
|
||||
expect(token1).to.be.deep.eq(token2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createForDynamic', () => {
|
||||
class Module {}
|
||||
|
||||
it('should include dynamic metadata', () => {
|
||||
const dynamicModule = {
|
||||
module: Module,
|
||||
providers: [
|
||||
{
|
||||
provide: 'test',
|
||||
useValue: 'test',
|
||||
},
|
||||
],
|
||||
};
|
||||
const token1 = factory.createForDynamic(
|
||||
dynamicModule.module,
|
||||
{
|
||||
providers: dynamicModule.providers,
|
||||
},
|
||||
dynamicModule,
|
||||
);
|
||||
const token2 = factory.createForDynamic(
|
||||
dynamicModule.module,
|
||||
{
|
||||
providers: dynamicModule.providers,
|
||||
},
|
||||
dynamicModule,
|
||||
);
|
||||
|
||||
expect(token1).to.be.deep.eq(token2);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when generating algorithm is shallow', () => {
|
||||
beforeEach(() => {
|
||||
factory = new ByReferenceModuleOpaqueKeyFactory({
|
||||
keyGenerationStrategy: 'shallow',
|
||||
});
|
||||
sinon.stub(factory as any, 'generateRandomString').returns(moduleId);
|
||||
});
|
||||
|
||||
describe('createForStatic', () => {
|
||||
class Module {}
|
||||
|
||||
it('should return expected token', () => {
|
||||
const type = Module;
|
||||
const token1 = factory.createForStatic(type);
|
||||
const token2 = factory.createForStatic(type);
|
||||
|
||||
expect(token1).to.be.deep.eq(token2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createForDynamic', () => {
|
||||
class Module {}
|
||||
|
||||
it('should include dynamic metadata', () => {
|
||||
const dynamicModule = {
|
||||
module: Module,
|
||||
providers: [
|
||||
{
|
||||
provide: 'test',
|
||||
useValue: 'test',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const token1 = factory.createForDynamic(
|
||||
dynamicModule.module,
|
||||
{
|
||||
providers: dynamicModule.providers,
|
||||
},
|
||||
dynamicModule,
|
||||
);
|
||||
const token2 = factory.createForDynamic(
|
||||
dynamicModule.module,
|
||||
{
|
||||
providers: dynamicModule.providers,
|
||||
},
|
||||
dynamicModule,
|
||||
);
|
||||
|
||||
expect(token1).to.be.deep.eq(token2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,41 +1,48 @@
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { ModuleTokenFactory } from '../../injector/module-token-factory';
|
||||
import { DeepHashedModuleOpaqueKeyFactory } from '../../../injector/opaque-key-factory/deep-hashed-module-opaque-key-factory';
|
||||
|
||||
describe('ModuleTokenFactory', () => {
|
||||
describe('DeepHashedModuleOpaqueKeyFactory', () => {
|
||||
const moduleId = 'constId';
|
||||
let factory: ModuleTokenFactory;
|
||||
let factory: DeepHashedModuleOpaqueKeyFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
factory = new ModuleTokenFactory();
|
||||
factory = new DeepHashedModuleOpaqueKeyFactory();
|
||||
sinon.stub(factory, 'getModuleId').returns(moduleId);
|
||||
});
|
||||
describe('create', () => {
|
||||
describe('createForStatic', () => {
|
||||
class Module {}
|
||||
|
||||
it('should return expected token', () => {
|
||||
const type = Module;
|
||||
const token1 = factory.create(type, undefined);
|
||||
const token2 = factory.create(type, undefined);
|
||||
const token1 = factory.createForStatic(type);
|
||||
const token2 = factory.createForStatic(type);
|
||||
expect(token1).to.be.deep.eq(token2);
|
||||
});
|
||||
});
|
||||
describe('createForDynamic', () => {
|
||||
class Module {}
|
||||
|
||||
it('should include dynamic metadata', () => {
|
||||
const type = Module;
|
||||
const token1 = factory.create(type, {
|
||||
const token1 = factory.createForDynamic(type, {
|
||||
providers: [{}],
|
||||
} as any);
|
||||
const token2 = factory.create(type, {
|
||||
const token2 = factory.createForDynamic(type, {
|
||||
providers: [{}],
|
||||
} as any);
|
||||
|
||||
expect(token1).to.be.deep.eq(token2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModuleName', () => {
|
||||
it('should map module metatype to name', () => {
|
||||
const metatype = () => {};
|
||||
expect(factory.getModuleName(metatype as any)).to.be.eql(metatype.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStringifiedOpaqueToken', () => {
|
||||
describe('when metadata exists', () => {
|
||||
it('should return hash', () => {
|
||||
@@ -80,6 +87,7 @@ describe('ModuleTokenFactory', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metadata does not exist', () => {
|
||||
it('should return empty string', () => {
|
||||
expect(factory.getStringifiedOpaqueToken(undefined)).to.be.eql('');
|
||||
@@ -4,9 +4,13 @@ import { RouteParamsFactory } from '../../router/route-params-factory';
|
||||
|
||||
describe('RouteParamsFactory', () => {
|
||||
let factory: RouteParamsFactory;
|
||||
let untypedFactory: any;
|
||||
|
||||
beforeEach(() => {
|
||||
factory = new RouteParamsFactory();
|
||||
untypedFactory = factory as any;
|
||||
});
|
||||
|
||||
describe('exchangeKeyForValue', () => {
|
||||
const res = {};
|
||||
const next = () => ({});
|
||||
@@ -37,14 +41,14 @@ describe('RouteParamsFactory', () => {
|
||||
describe(`RouteParamtypes.NEXT`, () => {
|
||||
it('should return next object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(RouteParamtypes.NEXT, ...args),
|
||||
untypedFactory.exchangeKeyForValue(RouteParamtypes.NEXT, ...args),
|
||||
).to.be.eql(next);
|
||||
});
|
||||
});
|
||||
describe(`RouteParamtypes.RESPONSE`, () => {
|
||||
it('should return response object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(
|
||||
untypedFactory.exchangeKeyForValue(
|
||||
RouteParamtypes.RESPONSE,
|
||||
...args,
|
||||
),
|
||||
@@ -54,7 +58,7 @@ describe('RouteParamsFactory', () => {
|
||||
describe(`RouteParamtypes.REQUEST`, () => {
|
||||
it('should return request object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(
|
||||
untypedFactory.exchangeKeyForValue(
|
||||
RouteParamtypes.REQUEST,
|
||||
...args,
|
||||
),
|
||||
@@ -64,14 +68,14 @@ describe('RouteParamsFactory', () => {
|
||||
describe(`RouteParamtypes.BODY`, () => {
|
||||
it('should return body object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(RouteParamtypes.BODY, ...args),
|
||||
untypedFactory.exchangeKeyForValue(RouteParamtypes.BODY, ...args),
|
||||
).to.be.eql(req.body);
|
||||
});
|
||||
});
|
||||
describe(`RouteParamtypes.RAW_BODY`, () => {
|
||||
it('should return rawBody buffer', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(
|
||||
untypedFactory.exchangeKeyForValue(
|
||||
RouteParamtypes.RAW_BODY,
|
||||
...args,
|
||||
),
|
||||
@@ -81,7 +85,7 @@ describe('RouteParamsFactory', () => {
|
||||
describe(`RouteParamtypes.HEADERS`, () => {
|
||||
it('should return headers object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(
|
||||
untypedFactory.exchangeKeyForValue(
|
||||
RouteParamtypes.HEADERS,
|
||||
...args,
|
||||
),
|
||||
@@ -91,14 +95,14 @@ describe('RouteParamsFactory', () => {
|
||||
describe(`RouteParamtypes.IP`, () => {
|
||||
it('should return ip property', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(RouteParamtypes.IP, ...args),
|
||||
untypedFactory.exchangeKeyForValue(RouteParamtypes.IP, ...args),
|
||||
).to.be.equal(req.ip);
|
||||
});
|
||||
});
|
||||
describe(`RouteParamtypes.SESSION`, () => {
|
||||
it('should return session object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(
|
||||
untypedFactory.exchangeKeyForValue(
|
||||
RouteParamtypes.SESSION,
|
||||
...args,
|
||||
),
|
||||
@@ -108,50 +112,41 @@ describe('RouteParamsFactory', () => {
|
||||
describe(`RouteParamtypes.QUERY`, () => {
|
||||
it('should return query object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(
|
||||
RouteParamtypes.QUERY,
|
||||
...args,
|
||||
),
|
||||
untypedFactory.exchangeKeyForValue(RouteParamtypes.QUERY, ...args),
|
||||
).to.be.eql(req.query);
|
||||
});
|
||||
});
|
||||
describe(`RouteParamtypes.PARAM`, () => {
|
||||
it('should return params object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(
|
||||
RouteParamtypes.PARAM,
|
||||
...args,
|
||||
),
|
||||
untypedFactory.exchangeKeyForValue(RouteParamtypes.PARAM, ...args),
|
||||
).to.be.eql(req.params);
|
||||
});
|
||||
});
|
||||
describe(`RouteParamtypes.HOST`, () => {
|
||||
it('should return hosts object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(RouteParamtypes.HOST, ...args),
|
||||
untypedFactory.exchangeKeyForValue(RouteParamtypes.HOST, ...args),
|
||||
).to.be.eql(req.hosts);
|
||||
});
|
||||
});
|
||||
describe(`RouteParamtypes.FILE`, () => {
|
||||
it('should return file object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(RouteParamtypes.FILE, ...args),
|
||||
untypedFactory.exchangeKeyForValue(RouteParamtypes.FILE, ...args),
|
||||
).to.be.eql(req.file);
|
||||
});
|
||||
});
|
||||
describe(`RouteParamtypes.FILES`, () => {
|
||||
it('should return files object', () => {
|
||||
expect(
|
||||
(factory as any).exchangeKeyForValue(
|
||||
RouteParamtypes.FILES,
|
||||
...args,
|
||||
),
|
||||
untypedFactory.exchangeKeyForValue(RouteParamtypes.FILES, ...args),
|
||||
).to.be.eql(req.files);
|
||||
});
|
||||
});
|
||||
describe('not available', () => {
|
||||
it('should return null', () => {
|
||||
expect((factory as any).exchangeKeyForValue(-1, ...args)).to.be.eql(
|
||||
expect(untypedFactory.exchangeKeyForValue(-1, ...args)).to.be.eql(
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@ describe('RoutesResolver', () => {
|
||||
|
||||
let router: any;
|
||||
let routesResolver: RoutesResolver;
|
||||
let untypedRoutesResolver: any;
|
||||
let container: NestContainer;
|
||||
let modules: Map<string, any>;
|
||||
let applicationRef: any;
|
||||
@@ -82,6 +83,7 @@ describe('RoutesResolver', () => {
|
||||
new Injector(),
|
||||
new GraphInspector(container),
|
||||
);
|
||||
untypedRoutesResolver = routesResolver as any;
|
||||
});
|
||||
|
||||
describe('registerRouters', () => {
|
||||
@@ -95,14 +97,14 @@ describe('RoutesResolver', () => {
|
||||
|
||||
const appInstance = new NoopHttpAdapter(router);
|
||||
const exploreSpy = sinon.spy(
|
||||
(routesResolver as any).routerExplorer,
|
||||
untypedRoutesResolver.routerExplorer,
|
||||
'explore',
|
||||
);
|
||||
const moduleName = '';
|
||||
modules.set(moduleName, {});
|
||||
|
||||
sinon
|
||||
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
|
||||
.stub(untypedRoutesResolver.routerExplorer, 'extractRouterPath')
|
||||
.callsFake(() => ['']);
|
||||
routesResolver.registerRouters(routes, moduleName, '', '', appInstance);
|
||||
|
||||
@@ -137,14 +139,14 @@ describe('RoutesResolver', () => {
|
||||
|
||||
const appInstance = new NoopHttpAdapter(router);
|
||||
const exploreSpy = sinon.spy(
|
||||
(routesResolver as any).routerExplorer,
|
||||
untypedRoutesResolver.routerExplorer,
|
||||
'explore',
|
||||
);
|
||||
const moduleName = '';
|
||||
modules.set(moduleName, {});
|
||||
|
||||
sinon
|
||||
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
|
||||
.stub(untypedRoutesResolver.routerExplorer, 'extractRouterPath')
|
||||
.callsFake(() => ['']);
|
||||
routesResolver.registerRouters(routes, moduleName, '', '', appInstance);
|
||||
|
||||
@@ -181,6 +183,7 @@ describe('RoutesResolver', () => {
|
||||
new Injector(),
|
||||
new GraphInspector(container),
|
||||
);
|
||||
untypedRoutesResolver = routesResolver as any;
|
||||
|
||||
const routes = new Map();
|
||||
const routeWrapper = new InstanceWrapper({
|
||||
@@ -191,14 +194,14 @@ describe('RoutesResolver', () => {
|
||||
|
||||
const appInstance = new NoopHttpAdapter(router);
|
||||
const exploreSpy = sinon.spy(
|
||||
(routesResolver as any).routerExplorer,
|
||||
untypedRoutesResolver.routerExplorer,
|
||||
'explore',
|
||||
);
|
||||
const moduleName = '';
|
||||
modules.set(moduleName, {});
|
||||
|
||||
sinon
|
||||
.stub((routesResolver as any).routerExplorer, 'extractRouterPath')
|
||||
.stub(untypedRoutesResolver.routerExplorer, 'extractRouterPath')
|
||||
.callsFake(() => ['']);
|
||||
routesResolver.registerRouters(routes, moduleName, '', '', appInstance);
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('DependenciesScanner', () => {
|
||||
class InvalidModule {}
|
||||
|
||||
let scanner: DependenciesScanner;
|
||||
let untypedScanner: any;
|
||||
let mockContainer: sinon.SinonMock;
|
||||
let container: NestContainer;
|
||||
let graphInspector: GraphInspector;
|
||||
@@ -71,6 +72,7 @@ describe('DependenciesScanner', () => {
|
||||
graphInspector,
|
||||
new ApplicationConfig(),
|
||||
);
|
||||
untypedScanner = scanner as any;
|
||||
sinon.stub(scanner, 'registerCoreModule').callsFake(async () => {});
|
||||
});
|
||||
|
||||
@@ -86,7 +88,7 @@ describe('DependenciesScanner', () => {
|
||||
.expects('replaceModule')
|
||||
.never();
|
||||
|
||||
await scanner.scan(TestModule as any);
|
||||
await scanner.scan(TestModule);
|
||||
expectationCountAddModule.verify();
|
||||
expectationCountReplaceModule.verify();
|
||||
});
|
||||
@@ -95,20 +97,20 @@ describe('DependenciesScanner', () => {
|
||||
const expectation = mockContainer.expects('addProvider').twice();
|
||||
const stub = sinon.stub(scanner, 'insertExportedProvider');
|
||||
|
||||
await scanner.scan(TestModule as any);
|
||||
await scanner.scan(TestModule);
|
||||
expectation.verify();
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
it('should "insertController" call twice (2 components) container method "addController"', async () => {
|
||||
const expectation = mockContainer.expects('addController').twice();
|
||||
await scanner.scan(TestModule as any);
|
||||
await scanner.scan(TestModule);
|
||||
expectation.verify();
|
||||
});
|
||||
|
||||
it('should "insertExportedProvider" call once (1 component) container method "addExportedProvider"', async () => {
|
||||
const expectation = mockContainer.expects('addExportedProvider').once();
|
||||
await scanner.scan(TestModule as any);
|
||||
await scanner.scan(TestModule);
|
||||
expectation.verify();
|
||||
});
|
||||
|
||||
@@ -176,7 +178,7 @@ describe('DependenciesScanner', () => {
|
||||
.expects('addModule')
|
||||
.once();
|
||||
|
||||
await scanner.scan(OverrideTestModule as any, {
|
||||
await scanner.scan(OverrideTestModule, {
|
||||
overrides: modulesToOverride,
|
||||
});
|
||||
|
||||
@@ -188,13 +190,13 @@ describe('DependenciesScanner', () => {
|
||||
it('should "insertProvider" call once container method "addProvider"', async () => {
|
||||
const expectation = mockContainer.expects('addProvider').once();
|
||||
|
||||
await scanner.scan(OverrideTestModule as any);
|
||||
await scanner.scan(OverrideTestModule);
|
||||
expectation.verify();
|
||||
});
|
||||
|
||||
it('should "insertController" call twice (2 components) container method "addController"', async () => {
|
||||
const expectation = mockContainer.expects('addController').twice();
|
||||
await scanner.scan(OverrideTestModule as any);
|
||||
await scanner.scan(OverrideTestModule);
|
||||
expectation.verify();
|
||||
});
|
||||
|
||||
@@ -230,7 +232,7 @@ describe('DependenciesScanner', () => {
|
||||
})
|
||||
class OverrideForwardRefTestModule {}
|
||||
|
||||
await scanner.scan(OverrideForwardRefTestModule as any, {
|
||||
await scanner.scan(OverrideForwardRefTestModule, {
|
||||
overrides: [
|
||||
{
|
||||
moduleToReplace: Overwritten,
|
||||
@@ -290,7 +292,7 @@ describe('DependenciesScanner', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
addInjectableStub = sinon
|
||||
.stub((scanner as any).container, 'addInjectable')
|
||||
.stub(untypedScanner.container, 'addInjectable')
|
||||
.callsFake(() => instanceWrapper);
|
||||
insertEnhancerMetadataCacheStub = sinon
|
||||
.stub(graphInspector, 'insertEnhancerMetadataCache')
|
||||
@@ -427,7 +429,7 @@ describe('DependenciesScanner', () => {
|
||||
const module = { forwardRef: sinon.stub().returns({}) };
|
||||
|
||||
sinon.stub(container, 'addImport').returns({} as any);
|
||||
await scanner.insertImport(module as any, [] as any, 'test');
|
||||
await scanner.insertImport(module, [] as any, 'test');
|
||||
expect(module.forwardRef.called).to.be.true;
|
||||
});
|
||||
describe('when "related" is nil', () => {
|
||||
@@ -477,7 +479,7 @@ describe('DependenciesScanner', () => {
|
||||
it('should push new object to "applicationProvidersApplyMap" array', () => {
|
||||
mockContainer.expects('addProvider').callsFake(() => false);
|
||||
scanner.insertProvider(provider, token);
|
||||
const applyMap = (scanner as any).applicationProvidersApplyMap;
|
||||
const applyMap = untypedScanner.applicationProvidersApplyMap;
|
||||
|
||||
expect(applyMap).to.have.length(1);
|
||||
expect(applyMap[0].moduleKey).to.be.eql(token);
|
||||
@@ -514,15 +516,11 @@ describe('DependenciesScanner', () => {
|
||||
expectation.verify();
|
||||
});
|
||||
it('should not push new object to "applicationProvidersApplyMap" array', () => {
|
||||
expect((scanner as any).applicationProvidersApplyMap).to.have.length(
|
||||
0,
|
||||
);
|
||||
expect(untypedScanner.applicationProvidersApplyMap).to.have.length(0);
|
||||
|
||||
mockContainer.expects('addProvider').callsFake(() => false);
|
||||
scanner.insertProvider(component, token);
|
||||
expect((scanner as any).applicationProvidersApplyMap).to.have.length(
|
||||
0,
|
||||
);
|
||||
expect(untypedScanner.applicationProvidersApplyMap).to.have.length(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -534,7 +532,7 @@ describe('DependenciesScanner', () => {
|
||||
providerKey: 'providerToken',
|
||||
type: APP_GUARD,
|
||||
};
|
||||
(scanner as any).applicationProvidersApplyMap = [provider];
|
||||
untypedScanner.applicationProvidersApplyMap = [provider];
|
||||
|
||||
const expectedInstance = {};
|
||||
const instanceWrapper = {
|
||||
@@ -569,7 +567,7 @@ describe('DependenciesScanner', () => {
|
||||
type: APP_GUARD,
|
||||
scope: Scope.REQUEST,
|
||||
};
|
||||
(scanner as any).applicationProvidersApplyMap = [provider];
|
||||
untypedScanner.applicationProvidersApplyMap = [provider];
|
||||
|
||||
const expectedInstanceWrapper = new InstanceWrapper();
|
||||
mockContainer.expects('getModules').callsFake(() => ({
|
||||
@@ -606,7 +604,7 @@ describe('DependenciesScanner', () => {
|
||||
};
|
||||
|
||||
it('should add enhancers metadata to every controller and every entry provider', () => {
|
||||
(scanner as any).applicationProvidersApplyMap = [provider];
|
||||
untypedScanner.applicationProvidersApplyMap = [provider];
|
||||
|
||||
const instance = new InstanceWrapper({ name: 'test' });
|
||||
const controllers = new Map();
|
||||
@@ -651,7 +649,7 @@ describe('DependenciesScanner', () => {
|
||||
describe(`when token is ${APP_INTERCEPTOR}`, () => {
|
||||
it('call "addGlobalInterceptor"', () => {
|
||||
const addSpy = sinon.spy(
|
||||
(scanner as any).applicationConfig,
|
||||
untypedScanner.applicationConfig,
|
||||
'addGlobalInterceptor',
|
||||
);
|
||||
scanner.getApplyProvidersMap()[APP_INTERCEPTOR](null);
|
||||
@@ -661,7 +659,7 @@ describe('DependenciesScanner', () => {
|
||||
describe(`when token is ${APP_GUARD}`, () => {
|
||||
it('call "addGlobalGuard"', () => {
|
||||
const addSpy = sinon.spy(
|
||||
(scanner as any).applicationConfig,
|
||||
untypedScanner.applicationConfig,
|
||||
'addGlobalGuard',
|
||||
);
|
||||
scanner.getApplyProvidersMap()[APP_GUARD](null);
|
||||
@@ -671,7 +669,7 @@ describe('DependenciesScanner', () => {
|
||||
describe(`when token is ${APP_PIPE}`, () => {
|
||||
it('call "addGlobalPipe"', () => {
|
||||
const addSpy = sinon.spy(
|
||||
(scanner as any).applicationConfig,
|
||||
untypedScanner.applicationConfig,
|
||||
'addGlobalPipe',
|
||||
);
|
||||
scanner.getApplyProvidersMap()[APP_PIPE](null);
|
||||
@@ -681,7 +679,7 @@ describe('DependenciesScanner', () => {
|
||||
describe(`when token is ${APP_FILTER}`, () => {
|
||||
it('call "addGlobalFilter"', () => {
|
||||
const addSpy = sinon.spy(
|
||||
(scanner as any).applicationConfig,
|
||||
untypedScanner.applicationConfig,
|
||||
'addGlobalFilter',
|
||||
);
|
||||
scanner.getApplyProvidersMap()[APP_FILTER](null);
|
||||
@@ -693,7 +691,7 @@ describe('DependenciesScanner', () => {
|
||||
describe(`when token is ${APP_INTERCEPTOR}`, () => {
|
||||
it('call "addGlobalRequestInterceptor"', () => {
|
||||
const addSpy = sinon.spy(
|
||||
(scanner as any).applicationConfig,
|
||||
untypedScanner.applicationConfig,
|
||||
'addGlobalRequestInterceptor',
|
||||
);
|
||||
scanner.getApplyRequestProvidersMap()[APP_INTERCEPTOR](null);
|
||||
@@ -703,7 +701,7 @@ describe('DependenciesScanner', () => {
|
||||
describe(`when token is ${APP_GUARD}`, () => {
|
||||
it('call "addGlobalRequestGuard"', () => {
|
||||
const addSpy = sinon.spy(
|
||||
(scanner as any).applicationConfig,
|
||||
untypedScanner.applicationConfig,
|
||||
'addGlobalRequestGuard',
|
||||
);
|
||||
scanner.getApplyRequestProvidersMap()[APP_GUARD](null);
|
||||
@@ -713,7 +711,7 @@ describe('DependenciesScanner', () => {
|
||||
describe(`when token is ${APP_PIPE}`, () => {
|
||||
it('call "addGlobalRequestPipe"', () => {
|
||||
const addSpy = sinon.spy(
|
||||
(scanner as any).applicationConfig,
|
||||
untypedScanner.applicationConfig,
|
||||
'addGlobalRequestPipe',
|
||||
);
|
||||
scanner.getApplyRequestProvidersMap()[APP_PIPE](null);
|
||||
@@ -723,7 +721,7 @@ describe('DependenciesScanner', () => {
|
||||
describe(`when token is ${APP_FILTER}`, () => {
|
||||
it('call "addGlobalRequestFilter"', () => {
|
||||
const addSpy = sinon.spy(
|
||||
(scanner as any).applicationConfig,
|
||||
untypedScanner.applicationConfig,
|
||||
'addGlobalRequestFilter',
|
||||
);
|
||||
scanner.getApplyRequestProvidersMap()[APP_FILTER](null);
|
||||
|
||||
@@ -5,48 +5,63 @@ const transformDecorator = Reflector.createDecorator<string[], number>({
|
||||
transform: value => value.length,
|
||||
});
|
||||
|
||||
describe('Reflector', () => {
|
||||
let reflector: Reflector;
|
||||
type TestObject = {
|
||||
only1?: string;
|
||||
only2?: string;
|
||||
both: string;
|
||||
};
|
||||
|
||||
class Test {}
|
||||
describe('Reflector', () => {
|
||||
const key = 'key';
|
||||
let reflector: Reflector;
|
||||
|
||||
@transformDecorator(['a', 'b', 'c'])
|
||||
class TestTransform {}
|
||||
class Test {}
|
||||
class Test1 {}
|
||||
class Test2 {}
|
||||
|
||||
beforeEach(() => {
|
||||
Reflect.deleteMetadata(key, Test1);
|
||||
Reflect.deleteMetadata(key, Test2);
|
||||
reflector = new Reflector();
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should reflect metadata by key', () => {
|
||||
const key = 'key';
|
||||
const value = 'value';
|
||||
Reflect.defineMetadata(key, value, Test);
|
||||
expect(reflector.get(key, Test)).to.eql(value);
|
||||
Reflect.defineMetadata(key, value, Test1);
|
||||
expect(reflector.get(key, Test1)).to.eql(value);
|
||||
});
|
||||
|
||||
it('should reflect metadata by decorator', () => {
|
||||
const decorator = Reflector.createDecorator<string>();
|
||||
const value = 'value';
|
||||
Reflect.defineMetadata(decorator.KEY, value, Test);
|
||||
Reflect.defineMetadata(decorator.KEY, value, Test1);
|
||||
|
||||
let reflectedValue = reflector.get(decorator, Test);
|
||||
// string
|
||||
let reflectedValue = reflector.get(decorator, Test1);
|
||||
expect(reflectedValue).to.eql(value);
|
||||
|
||||
// @ts-expect-error 'value' is not assignable to parameter of type 'string'
|
||||
reflectedValue = true;
|
||||
|
||||
reflectedValue satisfies string;
|
||||
});
|
||||
|
||||
it('should reflect metadata by decorator (custom key)', () => {
|
||||
const decorator = Reflector.createDecorator<string[]>({ key: 'custom' });
|
||||
const value = ['value'];
|
||||
Reflect.defineMetadata('custom', value, Test);
|
||||
Reflect.defineMetadata('custom', value, Test1);
|
||||
|
||||
let reflectedValue = reflector.get(decorator, Test);
|
||||
// string[]
|
||||
let reflectedValue = reflector.get(decorator, Test1);
|
||||
expect(reflectedValue).to.eql(value);
|
||||
|
||||
// @ts-expect-error 'value' is not assignable to parameter of type 'string[]'
|
||||
reflectedValue = true;
|
||||
|
||||
reflectedValue satisfies string[];
|
||||
});
|
||||
|
||||
it('should reflect metadata by decorator (with transform option)', () => {
|
||||
@@ -55,6 +70,8 @@ describe('Reflector', () => {
|
||||
|
||||
// @ts-expect-error 'value' is not assignable to type 'number'
|
||||
reflectedValue = [];
|
||||
|
||||
reflectedValue satisfies number;
|
||||
});
|
||||
|
||||
it('should require transform option when second generic type is provided', () => {
|
||||
@@ -64,53 +81,121 @@ describe('Reflector', () => {
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should reflect metadata of all targets', () => {
|
||||
const key = 'key';
|
||||
const value = 'value';
|
||||
Reflect.defineMetadata(key, value, Test);
|
||||
expect(reflector.getAll(key, [Test])).to.eql([value]);
|
||||
it('should reflect metadata of all targets by key', () => {
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
Reflect.defineMetadata(key, value1, Test1);
|
||||
Reflect.defineMetadata(key, value2, Test2);
|
||||
expect(reflector.getAll(key, [Test1, Test2])).to.eql([value1, value2]);
|
||||
});
|
||||
it('should reflect metadata of all targets by decorator', () => {
|
||||
const decorator = Reflector.createDecorator<string>();
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
Reflect.defineMetadata(decorator.KEY, value1, Test1);
|
||||
Reflect.defineMetadata(decorator.KEY, value2, Test2);
|
||||
|
||||
// string[]
|
||||
const reflectedValue = reflector.getAll(decorator, [Test1, Test2]);
|
||||
expect(reflectedValue).to.eql([value1, value2]);
|
||||
|
||||
reflectedValue satisfies string[];
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllAndMerge', () => {
|
||||
it('should return an empty array when there are no targets', () => {
|
||||
const key = 'key';
|
||||
expect(reflector.getAllAndMerge(key, [])).to.be.empty;
|
||||
});
|
||||
it('should reflect metadata of all targets and concat arrays', () => {
|
||||
const key = 'key';
|
||||
const decorator = Reflector.createDecorator<string[]>();
|
||||
const value = 'value';
|
||||
Reflect.defineMetadata(key, [value], Test);
|
||||
expect(reflector.getAllAndMerge(key, [Test, Test])).to.eql([
|
||||
value,
|
||||
value,
|
||||
Reflect.defineMetadata(decorator.KEY, [value], Test1);
|
||||
|
||||
// string[]
|
||||
const reflectedValue = reflector.getAllAndMerge(decorator, [
|
||||
Test1,
|
||||
Test1,
|
||||
]);
|
||||
expect(reflectedValue).to.eql([value, value]);
|
||||
|
||||
reflectedValue satisfies string[];
|
||||
});
|
||||
it('should reflect metadata of all targets and concat boolean arrays', () => {
|
||||
const decorator = Reflector.createDecorator<boolean>();
|
||||
const value = true;
|
||||
Reflect.defineMetadata(decorator.KEY, [value], Test1);
|
||||
|
||||
// string[]
|
||||
const reflectedValue = reflector.getAllAndMerge(decorator, [
|
||||
Test1,
|
||||
Test1,
|
||||
]);
|
||||
expect(reflectedValue).to.eql([value, value]);
|
||||
|
||||
reflectedValue satisfies boolean[];
|
||||
});
|
||||
it('should reflect metadata of all targets and create an array', () => {
|
||||
const key = 'key';
|
||||
const decorator = Reflector.createDecorator<string>();
|
||||
const value = 'value';
|
||||
Reflect.defineMetadata(key, value, Test);
|
||||
expect(reflector.getAllAndMerge(key, [Test, Test])).to.eql([
|
||||
value,
|
||||
value,
|
||||
Reflect.defineMetadata(decorator.KEY, value, Test1);
|
||||
|
||||
// string[]
|
||||
const reflectedValue = reflector.getAllAndMerge(decorator, [
|
||||
Test1,
|
||||
Test1,
|
||||
]);
|
||||
expect(reflectedValue).to.eql([value, value]);
|
||||
|
||||
reflectedValue satisfies string[];
|
||||
});
|
||||
it('should reflect metadata of all targets and merge an object', () => {
|
||||
const key = 'key';
|
||||
const value = { test: 'test' };
|
||||
Reflect.defineMetadata(key, value, Test);
|
||||
expect(reflector.getAllAndMerge(key, [Test, Test])).to.eql({
|
||||
...value,
|
||||
it('should reflect metadata of all targets and merge objects', () => {
|
||||
const decorator = Reflector.createDecorator<TestObject>();
|
||||
const value1: TestObject = { only1: 'test1', both: 'overriden' };
|
||||
const value2: TestObject = { only2: 'test2', both: 'test' };
|
||||
Reflect.defineMetadata(decorator.KEY, value1, Test1);
|
||||
Reflect.defineMetadata(decorator.KEY, value2, Test2);
|
||||
|
||||
// TestObject
|
||||
const reflectedValue = reflector.getAllAndMerge(decorator, [
|
||||
Test1,
|
||||
Test2,
|
||||
]);
|
||||
expect(reflectedValue).to.eql({
|
||||
...value1,
|
||||
...value2,
|
||||
});
|
||||
|
||||
reflectedValue satisfies TestObject;
|
||||
});
|
||||
it('should reflect metadata of all targets and create an array from a single value', () => {
|
||||
const value = 'value';
|
||||
Reflect.defineMetadata(key, value, Test1);
|
||||
|
||||
const result = reflector.getAllAndMerge(key, [Test1, Test2]);
|
||||
expect(result).to.eql([value]);
|
||||
|
||||
result satisfies string[];
|
||||
});
|
||||
it('should reflect metadata of all targets and return a single array unmodified', () => {
|
||||
const value = ['value'];
|
||||
Reflect.defineMetadata(key, value, Test1);
|
||||
expect(reflector.getAllAndMerge(key, [Test1, Test2])).to.eql(value);
|
||||
});
|
||||
it('should reflect metadata of all targets and return a single object unmodified', () => {
|
||||
const value = { test: 'value' };
|
||||
Reflect.defineMetadata(key, value, Test1);
|
||||
expect(reflector.getAllAndMerge(key, [Test1, Test2])).to.eql(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllAndOverride', () => {
|
||||
it('should reflect metadata of all targets and return a first not undefined value', () => {
|
||||
const key = 'key';
|
||||
const value = 'value';
|
||||
Reflect.defineMetadata(key, value, Test);
|
||||
expect(reflector.getAllAndOverride(key, [Test, Test])).to.eql(value);
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
Reflect.defineMetadata(key, value1, Test1);
|
||||
Reflect.defineMetadata(key, value2, Test2);
|
||||
expect(reflector.getAllAndOverride(key, [Test1, Test2])).to.eql(value1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circfleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
@@ -96,6 +96,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
|
||||
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -6,23 +6,42 @@ import { GRPC_DEFAULT_PROTO_LOADER, GRPC_DEFAULT_URL } from '../constants';
|
||||
import { InvalidGrpcPackageException } from '../errors/invalid-grpc-package.exception';
|
||||
import { InvalidGrpcServiceException } from '../errors/invalid-grpc-service.exception';
|
||||
import { InvalidProtoDefinitionException } from '../errors/invalid-proto-definition.exception';
|
||||
import { ClientGrpc, GrpcOptions } from '../interfaces';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
import { GRPC_CANCELLED } from './constants';
|
||||
import { ChannelOptions } from '../external/grpc-options.interface';
|
||||
import { getGrpcPackageDefinition } from '../helpers';
|
||||
import { ClientGrpc, GrpcOptions } from '../interfaces';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
|
||||
let grpcPackage: any = {};
|
||||
let grpcProtoLoaderPackage: any = {};
|
||||
const GRPC_CANCELLED = 'Cancelled';
|
||||
|
||||
// To enable type safety for gRPC. This cant be uncommented by default
|
||||
// because it would require the user to install the @grpc/grpc-js package even if they dont use gRPC
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type GrpcClient = import('@grpc/grpc-js').Client;
|
||||
// let grpcPackage = {} as typeof import('@grpc/grpc-js');
|
||||
// let grpcProtoLoaderPackage = {} as typeof import('@grpc/proto-loader');
|
||||
|
||||
type GrpcClient = any;
|
||||
let grpcPackage = {} as any;
|
||||
let grpcProtoLoaderPackage = {} as any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientGrpcProxy extends ClientProxy implements ClientGrpc {
|
||||
export class ClientGrpcProxy
|
||||
extends ClientProxy<never, never>
|
||||
implements ClientGrpc
|
||||
{
|
||||
protected readonly logger = new Logger(ClientProxy.name);
|
||||
protected readonly clients = new Map<string, any>();
|
||||
protected readonly url: string;
|
||||
protected grpcClients = [];
|
||||
protected grpcClients: GrpcClient[] = [];
|
||||
|
||||
get status(): never {
|
||||
throw new Error(
|
||||
'The "status" attribute is not supported by the gRPC transport',
|
||||
);
|
||||
}
|
||||
|
||||
constructor(protected readonly options: GrpcOptions['options']) {
|
||||
super();
|
||||
@@ -367,4 +386,15 @@ export class ClientGrpcProxy extends ClientProxy implements ClientGrpc {
|
||||
'Method is not supported in gRPC mode. Use ClientGrpc instead (learn more in the documentation).',
|
||||
);
|
||||
}
|
||||
|
||||
public on<EventKey extends never = never, EventCallback = any>(
|
||||
event: EventKey,
|
||||
callback: EventCallback,
|
||||
) {
|
||||
throw new Error('Method is not supported in gRPC mode.');
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
throw new Error('Method is not supported in gRPC mode.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { KafkaResponseDeserializer } from '../deserializers/kafka-response.deserializer';
|
||||
import { KafkaHeaders } from '../enums';
|
||||
import { InvalidKafkaClientTopicException } from '../errors/invalid-kafka-client-topic.exception';
|
||||
import { KafkaStatus } from '../events';
|
||||
import {
|
||||
BrokersFunction,
|
||||
Consumer,
|
||||
@@ -27,7 +28,9 @@ import {
|
||||
KafkaReplyPartitionAssigner,
|
||||
} from '../helpers';
|
||||
import {
|
||||
ClientKafkaProxy,
|
||||
KafkaOptions,
|
||||
MsPattern,
|
||||
OutgoingEvent,
|
||||
ReadPacket,
|
||||
WritePacket,
|
||||
@@ -43,11 +46,12 @@ let kafkaPackage: any = {};
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientKafka extends ClientProxy {
|
||||
export class ClientKafka
|
||||
extends ClientProxy<never, KafkaStatus>
|
||||
implements ClientKafkaProxy
|
||||
{
|
||||
protected logger = new Logger(ClientKafka.name);
|
||||
protected client: Kafka | null = null;
|
||||
protected consumer: Consumer | null = null;
|
||||
protected producer: Producer | null = null;
|
||||
protected parser: KafkaParser | null = null;
|
||||
protected initialized: Promise<void> | null = null;
|
||||
protected responsePatterns: string[] = [];
|
||||
@@ -56,6 +60,26 @@ export class ClientKafka extends ClientProxy {
|
||||
protected clientId: string;
|
||||
protected groupId: string;
|
||||
protected producerOnlyMode: boolean;
|
||||
protected _consumer: Consumer | null = null;
|
||||
protected _producer: Producer | null = null;
|
||||
|
||||
get consumer(): Consumer {
|
||||
if (!this._consumer) {
|
||||
throw new Error(
|
||||
'No consumer initialized. Please, call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this._consumer;
|
||||
}
|
||||
|
||||
get producer(): Producer {
|
||||
if (!this._consumer) {
|
||||
throw new Error(
|
||||
'No producer initialized. Please, call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this._producer;
|
||||
}
|
||||
|
||||
constructor(protected readonly options: KafkaOptions['options']) {
|
||||
super();
|
||||
@@ -95,28 +119,27 @@ export class ClientKafka extends ClientProxy {
|
||||
this.initializeDeserializer(options);
|
||||
}
|
||||
|
||||
public subscribeToResponseOf(pattern: any): void {
|
||||
const request = this.normalizePattern(pattern);
|
||||
public subscribeToResponseOf(pattern: unknown): void {
|
||||
const request = this.normalizePattern(pattern as MsPattern);
|
||||
this.responsePatterns.push(this.getResponsePatternName(request));
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
this.producer && (await this.producer.disconnect());
|
||||
this.consumer && (await this.consumer.disconnect());
|
||||
this.producer = null;
|
||||
this.consumer = null;
|
||||
this._producer && (await this._producer.disconnect());
|
||||
this._consumer && (await this._consumer.disconnect());
|
||||
this._producer = null;
|
||||
this._consumer = null;
|
||||
this.initialized = null;
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
public async connect(): Promise<Producer> {
|
||||
if (this.initialized) {
|
||||
return this.initialized.then(() => this.producer);
|
||||
return this.initialized.then(() => this._producer);
|
||||
}
|
||||
this.initialized = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
this.client = this.createClient();
|
||||
|
||||
if (!this.producerOnlyMode) {
|
||||
const partitionAssigners = [
|
||||
(
|
||||
@@ -136,42 +159,45 @@ export class ClientKafka extends ClientProxy {
|
||||
},
|
||||
);
|
||||
|
||||
this.consumer = this.client.consumer(consumerOptions);
|
||||
// set member assignments on join and rebalance
|
||||
this.consumer.on(
|
||||
this.consumer.events.GROUP_JOIN,
|
||||
this._consumer = this.client.consumer(consumerOptions);
|
||||
this.registerConsumerEventListeners();
|
||||
|
||||
// Set member assignments on join and rebalance
|
||||
this._consumer.on(
|
||||
this._consumer.events.GROUP_JOIN,
|
||||
this.setConsumerAssignments.bind(this),
|
||||
);
|
||||
await this.consumer.connect();
|
||||
await this._consumer.connect();
|
||||
await this.bindTopics();
|
||||
}
|
||||
|
||||
this.producer = this.client.producer(this.options.producer || {});
|
||||
await this.producer.connect();
|
||||
this._producer = this.client.producer(this.options.producer || {});
|
||||
this.registerProducerEventListeners();
|
||||
await this._producer.connect();
|
||||
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
return this.initialized.then(() => this.producer);
|
||||
return this.initialized.then(() => this._producer);
|
||||
}
|
||||
|
||||
public async bindTopics(): Promise<void> {
|
||||
if (!this.consumer) {
|
||||
if (!this._consumer) {
|
||||
throw Error('No consumer initialized');
|
||||
}
|
||||
|
||||
const consumerSubscribeOptions = this.options.subscribe || {};
|
||||
|
||||
if (this.responsePatterns.length > 0) {
|
||||
await this.consumer.subscribe({
|
||||
await this._consumer.subscribe({
|
||||
...consumerSubscribeOptions,
|
||||
topics: this.responsePatterns,
|
||||
});
|
||||
}
|
||||
|
||||
await this.consumer.run(
|
||||
await this._consumer.run(
|
||||
Object.assign(this.options.run || {}, {
|
||||
eachMessage: this.createResponseCallback(),
|
||||
}),
|
||||
@@ -223,6 +249,52 @@ export class ClientKafka extends ClientProxy {
|
||||
return this.consumerAssignments;
|
||||
}
|
||||
|
||||
public commitOffsets(
|
||||
topicPartitions: TopicPartitionOffsetAndMetadata[],
|
||||
): Promise<void> {
|
||||
if (this._consumer) {
|
||||
return this._consumer.commitOffsets(topicPartitions);
|
||||
} else {
|
||||
throw new Error('No consumer initialized');
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.client) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.client as T;
|
||||
}
|
||||
|
||||
protected registerConsumerEventListeners() {
|
||||
this._consumer.on(this._consumer.events.CONNECT, () =>
|
||||
this._status$.next(KafkaStatus.CONNECTED),
|
||||
);
|
||||
this._consumer.on(this._consumer.events.DISCONNECT, () =>
|
||||
this._status$.next(KafkaStatus.DISCONNECTED),
|
||||
);
|
||||
this._consumer.on(this._consumer.events.REBALANCING, () =>
|
||||
this._status$.next(KafkaStatus.REBALANCING),
|
||||
);
|
||||
this._consumer.on(this._consumer.events.STOP, () =>
|
||||
this._status$.next(KafkaStatus.STOPPED),
|
||||
);
|
||||
this.consumer.on(this._consumer.events.CRASH, () =>
|
||||
this._status$.next(KafkaStatus.CRASHED),
|
||||
);
|
||||
}
|
||||
|
||||
protected registerProducerEventListeners() {
|
||||
this._producer.on(this._producer.events.CONNECT, () =>
|
||||
this._status$.next(KafkaStatus.CONNECTED),
|
||||
);
|
||||
this._producer.on(this._producer.events.DISCONNECT, () =>
|
||||
this._status$.next(KafkaStatus.DISCONNECTED),
|
||||
);
|
||||
}
|
||||
|
||||
protected async dispatchEvent(packet: OutgoingEvent): Promise<any> {
|
||||
const pattern = this.normalizePattern(packet.pattern);
|
||||
const outgoingEvent = await this.serializer.serialize(packet.data, {
|
||||
@@ -236,7 +308,7 @@ export class ClientKafka extends ClientProxy {
|
||||
this.options.send || {},
|
||||
);
|
||||
|
||||
return this.producer.send(message);
|
||||
return this._producer.send(message);
|
||||
}
|
||||
|
||||
protected getReplyTopicPartition(topic: string): string {
|
||||
@@ -245,7 +317,7 @@ export class ClientKafka extends ClientProxy {
|
||||
throw new InvalidKafkaClientTopicException(topic);
|
||||
}
|
||||
|
||||
// get the minimum partition
|
||||
// Get the minimum partition
|
||||
return minimumPartition.toString();
|
||||
}
|
||||
|
||||
@@ -282,7 +354,7 @@ export class ClientKafka extends ClientProxy {
|
||||
this.options.send || {},
|
||||
);
|
||||
|
||||
return this.producer.send(message);
|
||||
return this._producer.send(message);
|
||||
})
|
||||
.catch(err => errorCallback(err));
|
||||
|
||||
@@ -299,7 +371,7 @@ export class ClientKafka extends ClientProxy {
|
||||
protected setConsumerAssignments(data: ConsumerGroupJoinEvent): void {
|
||||
const consumerAssignments: { [key: string]: number } = {};
|
||||
|
||||
// only need to set the minimum
|
||||
// Only need to set the minimum
|
||||
Object.keys(data.payload.memberAssignment).forEach(topic => {
|
||||
const memberPartitions = data.payload.memberAssignment[topic];
|
||||
|
||||
@@ -321,13 +393,10 @@ export class ClientKafka extends ClientProxy {
|
||||
(options && options.deserializer) || new KafkaResponseDeserializer();
|
||||
}
|
||||
|
||||
public commitOffsets(
|
||||
topicPartitions: TopicPartitionOffsetAndMetadata[],
|
||||
): Promise<void> {
|
||||
if (this.consumer) {
|
||||
return this.consumer.commitOffsets(topicPartitions);
|
||||
} else {
|
||||
throw new Error('No consumer initialized');
|
||||
}
|
||||
public on<
|
||||
EventKey extends string | number | symbol = string | number | symbol,
|
||||
EventCallback = any,
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
throw new Error('Method is not supported for Kafka client');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,8 @@ import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { EmptyError, fromEvent, lastValueFrom, merge, Observable } from 'rxjs';
|
||||
import { first, map, share, tap } from 'rxjs/operators';
|
||||
import {
|
||||
CLOSE_EVENT,
|
||||
ECONNREFUSED,
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
MQTT_DEFAULT_URL,
|
||||
} from '../constants';
|
||||
import { MqttClient } from '../external/mqtt-client.interface';
|
||||
import { ECONNREFUSED, MQTT_DEFAULT_URL } from '../constants';
|
||||
import { MqttEvents, MqttEventsMap, MqttStatus } from '../events/mqtt.events';
|
||||
import { MqttOptions, ReadPacket, WritePacket } from '../interfaces';
|
||||
import {
|
||||
MqttRecord,
|
||||
@@ -20,19 +14,32 @@ import { ClientProxy } from './client-proxy';
|
||||
|
||||
let mqttPackage: any = {};
|
||||
|
||||
// To enable type safety for MQTT. This cant be uncommented by default
|
||||
// because it would require the user to install the mqtt package even if they dont use MQTT
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
type MqttClient = import('mqtt').MqttClient;
|
||||
// type MqttClient = any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientMqtt extends ClientProxy {
|
||||
export class ClientMqtt extends ClientProxy<MqttEvents, MqttStatus> {
|
||||
protected readonly logger = new Logger(ClientProxy.name);
|
||||
protected readonly subscriptionsCount = new Map<string, number>();
|
||||
protected readonly url: string;
|
||||
protected mqttClient: MqttClient;
|
||||
protected connection: Promise<any>;
|
||||
protected connectionPromise: Promise<any>;
|
||||
protected isInitialConnection = false;
|
||||
protected isReconnecting = false;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof MqttEvents;
|
||||
callback: MqttEvents[keyof MqttEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(protected readonly options: MqttOptions['options']) {
|
||||
super();
|
||||
this.url = this.getOptionsProp(this.options, 'url') || MQTT_DEFAULT_URL;
|
||||
this.url = this.getOptionsProp(this.options, 'url') ?? MQTT_DEFAULT_URL;
|
||||
|
||||
mqttPackage = loadPackage('mqtt', ClientMqtt.name, () => require('mqtt'));
|
||||
|
||||
@@ -51,38 +58,49 @@ export class ClientMqtt extends ClientProxy {
|
||||
public close() {
|
||||
this.mqttClient && this.mqttClient.end();
|
||||
this.mqttClient = null;
|
||||
this.connection = null;
|
||||
this.connectionPromise = null;
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public connect(): Promise<any> {
|
||||
if (this.mqttClient) {
|
||||
return this.connection;
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.mqttClient = this.createClient();
|
||||
this.handleError(this.mqttClient);
|
||||
this.registerErrorListener(this.mqttClient);
|
||||
this.registerOfflineListener(this.mqttClient);
|
||||
this.registerReconnectListener(this.mqttClient);
|
||||
this.registerConnectListener(this.mqttClient);
|
||||
this.registerDisconnectListener(this.mqttClient);
|
||||
this.registerCloseListener(this.mqttClient);
|
||||
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
this.mqttClient.on(event, callback),
|
||||
);
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
const connect$ = this.connect$(this.mqttClient);
|
||||
this.connection = lastValueFrom(
|
||||
this.mergeCloseEvent(this.mqttClient, connect$).pipe(
|
||||
tap(() =>
|
||||
this.mqttClient.on(MESSAGE_EVENT, this.createResponseCallback()),
|
||||
),
|
||||
share(),
|
||||
),
|
||||
this.connectionPromise = lastValueFrom(
|
||||
this.mergeCloseEvent(this.mqttClient, connect$).pipe(share()),
|
||||
).catch(err => {
|
||||
if (err instanceof EmptyError) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
return this.connection;
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
public mergeCloseEvent<T = any>(
|
||||
instance: MqttClient,
|
||||
source$: Observable<T>,
|
||||
): Observable<T> {
|
||||
const close$ = fromEvent(instance, CLOSE_EVENT).pipe(
|
||||
const close$ = fromEvent(instance, MqttEventsMap.CLOSE).pipe(
|
||||
tap({
|
||||
next: () => {
|
||||
this._status$.next(MqttStatus.CLOSED);
|
||||
},
|
||||
}),
|
||||
map((err: any) => {
|
||||
throw err;
|
||||
}),
|
||||
@@ -94,13 +112,81 @@ export class ClientMqtt extends ClientProxy {
|
||||
return mqttPackage.connect(this.url, this.options as MqttOptions);
|
||||
}
|
||||
|
||||
public handleError(client: MqttClient) {
|
||||
client.addListener(
|
||||
ERROR_EVENT,
|
||||
public registerErrorListener(client: MqttClient) {
|
||||
client.on(
|
||||
MqttEventsMap.ERROR,
|
||||
(err: any) => err.code !== ECONNREFUSED && this.logger.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
public registerOfflineListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.OFFLINE, () => {
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
|
||||
// Prevent unhandled rejections
|
||||
this.connectionPromise.catch(() => {});
|
||||
this.logger.error('MQTT broker went offline.');
|
||||
});
|
||||
}
|
||||
|
||||
public registerReconnectListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.RECONNECT, () => {
|
||||
this.isReconnecting = true;
|
||||
this._status$.next(MqttStatus.RECONNECTING);
|
||||
|
||||
this.logger.log('MQTT connection lost. Trying to reconnect...');
|
||||
});
|
||||
}
|
||||
|
||||
public registerDisconnectListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.DISCONNECT, () => {
|
||||
this._status$.next(MqttStatus.DISCONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
public registerCloseListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.CLOSE, () => {
|
||||
this._status$.next(MqttStatus.CLOSED);
|
||||
});
|
||||
}
|
||||
|
||||
public registerConnectListener(client: MqttClient) {
|
||||
client.on(MqttEventsMap.CONNECT, () => {
|
||||
this.isReconnecting = false;
|
||||
this._status$.next(MqttStatus.CONNECTED);
|
||||
|
||||
this.logger.log('Connected to MQTT broker');
|
||||
this.connectionPromise = Promise.resolve();
|
||||
|
||||
if (!this.isInitialConnection) {
|
||||
this.isInitialConnection = true;
|
||||
client.on('message', this.createResponseCallback());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof MqttEvents = keyof MqttEvents,
|
||||
EventCallback extends MqttEvents[EventKey] = MqttEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.mqttClient) {
|
||||
this.mqttClient.on(event, callback as any);
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.mqttClient) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.mqttClient as T;
|
||||
}
|
||||
|
||||
public createResponseCallback(): (channel: string, buffer: Buffer) => any {
|
||||
return async (channel: string, buffer: Buffer) => {
|
||||
const packet = JSON.parse(buffer.toString());
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { isObject } from '@nestjs/common/utils/shared.utils';
|
||||
import { EventEmitter } from 'stream';
|
||||
import { NATS_DEFAULT_URL } from '../constants';
|
||||
import { NatsResponseJSONDeserializer } from '../deserializers/nats-response-json.deserializer';
|
||||
import { EmptyResponseException } from '../errors/empty-response.exception';
|
||||
import { Client, NatsMsg } from '../external/nats-client.interface';
|
||||
import { NatsEvents, NatsEventsMap, NatsStatus } from '../events/nats.events';
|
||||
import { NatsOptions, PacketId, ReadPacket, WritePacket } from '../interfaces';
|
||||
import { NatsRecord } from '../record-builders';
|
||||
import { NatsRecordSerializer } from '../serializers/nats-record.serializer';
|
||||
@@ -12,13 +13,27 @@ import { ClientProxy } from './client-proxy';
|
||||
|
||||
let natsPackage = {} as any;
|
||||
|
||||
// To enable type safety for Nats. This cant be uncommented by default
|
||||
// because it would require the user to install the nats package even if they dont use Nats
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type Client = import('nats').NatsConnection;
|
||||
// type NatsMsg = import('nats').Msg;
|
||||
|
||||
type Client = any;
|
||||
type NatsMsg = any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientNats extends ClientProxy {
|
||||
export class ClientNats extends ClientProxy<NatsEvents, NatsStatus> {
|
||||
protected readonly logger = new Logger(ClientNats.name);
|
||||
|
||||
protected natsClient: Client;
|
||||
protected clientConnectionPromise: Promise<Client>;
|
||||
protected connectionPromise: Promise<Client>;
|
||||
protected statusEventEmitter = new EventEmitter<{
|
||||
[key in keyof NatsEvents]: Parameters<NatsEvents[key]>;
|
||||
}>();
|
||||
|
||||
constructor(protected readonly options: NatsOptions['options']) {
|
||||
super();
|
||||
@@ -30,22 +45,29 @@ export class ClientNats extends ClientProxy {
|
||||
|
||||
public async close() {
|
||||
await this.natsClient?.close();
|
||||
this.statusEventEmitter.removeAllListeners();
|
||||
|
||||
this.natsClient = null;
|
||||
this.clientConnectionPromise = null;
|
||||
this.connectionPromise = null;
|
||||
}
|
||||
|
||||
public async connect(): Promise<any> {
|
||||
if (this.clientConnectionPromise) {
|
||||
return this.clientConnectionPromise;
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.clientConnectionPromise = this.createClient();
|
||||
this.natsClient = await this.clientConnectionPromise;
|
||||
this.connectionPromise = this.createClient();
|
||||
this.natsClient = await this.connectionPromise.catch(err => {
|
||||
this.connectionPromise = null;
|
||||
throw err;
|
||||
});
|
||||
|
||||
this._status$.next(NatsStatus.CONNECTED);
|
||||
this.handleStatusUpdates(this.natsClient);
|
||||
return this.natsClient;
|
||||
}
|
||||
|
||||
public createClient(): Promise<Client> {
|
||||
const options: any = this.options || ({} as NatsOptions);
|
||||
const options = this.options || ({} as NatsOptions);
|
||||
return natsPackage.connect({
|
||||
servers: NATS_DEFAULT_URL,
|
||||
...options,
|
||||
@@ -61,12 +83,46 @@ export class ClientNats extends ClientProxy {
|
||||
|
||||
switch (status.type) {
|
||||
case 'error':
|
||||
case 'disconnect':
|
||||
this.logger.error(
|
||||
`NatsError: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
// Prevent unhandled promise rejection
|
||||
this.connectionPromise.catch(() => {});
|
||||
|
||||
this.logger.error(
|
||||
`NatsError: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
|
||||
this._status$.next(NatsStatus.DISCONNECTED);
|
||||
this.statusEventEmitter.emit(
|
||||
NatsEventsMap.DISCONNECT,
|
||||
status.data as string,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'reconnecting':
|
||||
this._status$.next(NatsStatus.RECONNECTING);
|
||||
break;
|
||||
|
||||
case 'reconnect':
|
||||
this.connectionPromise = Promise.resolve(client);
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
|
||||
this._status$.next(NatsStatus.CONNECTED);
|
||||
this.statusEventEmitter.emit(
|
||||
NatsEventsMap.RECONNECT,
|
||||
status.data as string,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'pingTimer':
|
||||
if (this.options.debug) {
|
||||
this.logger.debug(
|
||||
@@ -75,6 +131,13 @@ export class ClientNats extends ClientProxy {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update':
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
this.statusEventEmitter.emit(NatsEventsMap.UPDATE, status.data);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
@@ -84,6 +147,22 @@ export class ClientNats extends ClientProxy {
|
||||
}
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof NatsEvents = keyof NatsEvents,
|
||||
EventCallback extends NatsEvents[EventKey] = NatsEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
this.statusEventEmitter.on(event, callback as any);
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.natsClient) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.natsClient as T;
|
||||
}
|
||||
|
||||
public createSubscriptionHandler(
|
||||
packet: ReadPacket & PacketId,
|
||||
callback: (packet: WritePacket) => any,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Transport } from '../enums/transport.enum';
|
||||
import { ClientKafkaProxy } from '../interfaces';
|
||||
import {
|
||||
ClientOptions,
|
||||
CustomClientOptions,
|
||||
TcpClientOptions,
|
||||
} from '../interfaces/client-metadata.interface';
|
||||
import { Closeable } from '../interfaces/closeable.interface';
|
||||
import {
|
||||
GrpcOptions,
|
||||
KafkaOptions,
|
||||
@@ -23,7 +23,7 @@ import { ClientRMQ } from './client-rmq';
|
||||
import { ClientTCP } from './client-tcp';
|
||||
|
||||
export interface IClientProxyFactory {
|
||||
create(clientOptions: ClientOptions): ClientProxy & Closeable;
|
||||
create(clientOptions: ClientOptions): ClientProxy;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,33 +33,38 @@ export class ClientProxyFactory {
|
||||
public static create(
|
||||
clientOptions: { transport: Transport.GRPC } & ClientOptions,
|
||||
): ClientGrpcProxy;
|
||||
public static create(clientOptions: ClientOptions): ClientProxy & Closeable;
|
||||
public static create(
|
||||
clientOptions: CustomClientOptions,
|
||||
): ClientProxy & Closeable;
|
||||
clientOptions: { transport: Transport.KAFKA } & ClientOptions,
|
||||
): ClientKafkaProxy;
|
||||
public static create(clientOptions: ClientOptions): ClientProxy;
|
||||
public static create(clientOptions: CustomClientOptions): ClientProxy;
|
||||
public static create(
|
||||
clientOptions: ClientOptions | CustomClientOptions,
|
||||
): ClientProxy & Closeable {
|
||||
): ClientProxy | ClientGrpcProxy | ClientKafkaProxy {
|
||||
if (this.isCustomClientOptions(clientOptions)) {
|
||||
const { customClass, options } = clientOptions;
|
||||
return new customClass(options);
|
||||
}
|
||||
const { transport, options } = clientOptions || {};
|
||||
const { transport, options = {} } = clientOptions ?? { options: {} };
|
||||
switch (transport) {
|
||||
case Transport.REDIS:
|
||||
return new ClientRedis(options as RedisOptions['options']);
|
||||
return new ClientRedis(
|
||||
options as RedisOptions['options'],
|
||||
) as ClientProxy;
|
||||
case Transport.NATS:
|
||||
return new ClientNats(options as NatsOptions['options']);
|
||||
return new ClientNats(options as NatsOptions['options']) as ClientProxy;
|
||||
case Transport.MQTT:
|
||||
return new ClientMqtt(options as MqttOptions['options']);
|
||||
return new ClientMqtt(options as MqttOptions['options']) as ClientProxy;
|
||||
case Transport.GRPC:
|
||||
return new ClientGrpcProxy(options as GrpcOptions['options']);
|
||||
case Transport.RMQ:
|
||||
return new ClientRMQ(options as RmqOptions['options']);
|
||||
return new ClientRMQ(options as RmqOptions['options']) as ClientProxy;
|
||||
case Transport.KAFKA:
|
||||
return new ClientKafka(options as KafkaOptions['options']);
|
||||
default:
|
||||
return new ClientTCP(options as TcpClientOptions['options']);
|
||||
return new ClientTCP(
|
||||
options as TcpClientOptions['options'],
|
||||
) as ClientProxy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
|
||||
import { isNil } from '@nestjs/common/utils/shared.utils';
|
||||
import {
|
||||
throwError as _throw,
|
||||
connectable,
|
||||
defer,
|
||||
fromEvent,
|
||||
merge,
|
||||
Observable,
|
||||
Observer,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
throwError as _throw,
|
||||
} from 'rxjs';
|
||||
import { map, mergeMap, take } from 'rxjs/operators';
|
||||
import { CONNECT_EVENT, ERROR_EVENT } from '../constants';
|
||||
import { distinctUntilChanged, map, mergeMap, take } from 'rxjs/operators';
|
||||
import { IncomingResponseDeserializer } from '../deserializers/incoming-response.deserializer';
|
||||
import { InvalidMessageException } from '../errors/invalid-message.exception';
|
||||
import {
|
||||
@@ -32,14 +32,57 @@ import { ProducerSerializer } from '../interfaces/serializer.interface';
|
||||
import { IdentitySerializer } from '../serializers/identity.serializer';
|
||||
import { transformPatternToRoute } from '../utils';
|
||||
|
||||
export abstract class ClientProxy {
|
||||
public abstract connect(): Promise<any>;
|
||||
public abstract close(): any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export abstract class ClientProxy<
|
||||
EventsMap extends Record<never, Function> = Record<never, Function>,
|
||||
Status extends string = string,
|
||||
> {
|
||||
protected routingMap = new Map<string, Function>();
|
||||
protected serializer: ProducerSerializer;
|
||||
protected deserializer: ProducerDeserializer;
|
||||
protected _status$ = new ReplaySubject<Status>(1);
|
||||
|
||||
/**
|
||||
* Returns an observable that emits status changes.
|
||||
*/
|
||||
public get status(): Observable<Status> {
|
||||
return this._status$.asObservable().pipe(distinctUntilChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes the connection to the underlying server/broker.
|
||||
*/
|
||||
public abstract connect(): Promise<any>;
|
||||
/**
|
||||
* Closes the underlying connection to the server/broker.
|
||||
*/
|
||||
public abstract close(): any;
|
||||
/**
|
||||
* Registers an event listener for the given event.
|
||||
* @param event Event name
|
||||
* @param callback Callback to be executed when the event is emitted
|
||||
*/
|
||||
public on<
|
||||
EventKey extends keyof EventsMap = keyof EventsMap,
|
||||
EventCallback extends EventsMap[EventKey] = EventsMap[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
/**
|
||||
* Returns an instance of the underlying server/broker instance,
|
||||
* or a group of servers if there are more than one.
|
||||
*/
|
||||
public abstract unwrap<T>(): T;
|
||||
|
||||
/**
|
||||
* Send a message to the server/broker.
|
||||
* Used for message-driven communication style between microservices.
|
||||
* @param pattern Pattern to identify the message
|
||||
* @param data Data to be sent
|
||||
* @returns Observable with the result
|
||||
*/
|
||||
public send<TResult = any, TInput = any>(
|
||||
pattern: any,
|
||||
data: TInput,
|
||||
@@ -58,6 +101,13 @@ export abstract class ClientProxy {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event to the server/broker.
|
||||
* Used for event-driven communication style between microservices.
|
||||
* @param pattern Pattern to identify the event
|
||||
* @param data Data to be sent
|
||||
* @returns Observable that completes when the event is successfully emitted
|
||||
*/
|
||||
public emit<TResult = any, TInput = any>(
|
||||
pattern: any,
|
||||
data: TInput,
|
||||
@@ -114,8 +164,8 @@ export abstract class ClientProxy {
|
||||
|
||||
protected connect$(
|
||||
instance: any,
|
||||
errorEvent = ERROR_EVENT,
|
||||
connectEvent = CONNECT_EVENT,
|
||||
errorEvent = 'error',
|
||||
connectEvent = 'connect',
|
||||
): Observable<any> {
|
||||
const error$ = fromEvent(instance, errorEvent).pipe(
|
||||
map((err: any) => {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Logger } from '@nestjs/common/services/logger.service';
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { REDIS_DEFAULT_HOST, REDIS_DEFAULT_PORT } from '../constants';
|
||||
import {
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
REDIS_DEFAULT_HOST,
|
||||
REDIS_DEFAULT_PORT,
|
||||
} from '../constants';
|
||||
RedisEvents,
|
||||
RedisEventsMap,
|
||||
RedisStatus,
|
||||
} from '../events/redis.events';
|
||||
import { ReadPacket, RedisOptions, WritePacket } from '../interfaces';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
|
||||
// To enable type safety for Redis. This cant be uncommented by default
|
||||
// because it would require the user to install the ioredis package even if they dont use Redis
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type Redis = import('ioredis').Redis;
|
||||
type Redis = any;
|
||||
|
||||
let redisPackage = {} as any;
|
||||
@@ -16,13 +21,18 @@ let redisPackage = {} as any;
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientRedis extends ClientProxy {
|
||||
export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
|
||||
protected readonly logger = new Logger(ClientProxy.name);
|
||||
protected readonly subscriptionsCount = new Map<string, number>();
|
||||
protected pubClient: Redis;
|
||||
protected subClient: Redis;
|
||||
protected connection: Promise<any>;
|
||||
protected isExplicitlyTerminated = false;
|
||||
protected connectionPromise: Promise<any>;
|
||||
protected isManuallyClosed = false;
|
||||
protected wasInitialConnectionSuccessful = false;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof RedisEvents;
|
||||
callback: RedisEvents[keyof RedisEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(protected readonly options: RedisOptions['options']) {
|
||||
super();
|
||||
@@ -47,26 +57,35 @@ export class ClientRedis extends ClientProxy {
|
||||
this.pubClient && this.pubClient.quit();
|
||||
this.subClient && this.subClient.quit();
|
||||
this.pubClient = this.subClient = null;
|
||||
this.isExplicitlyTerminated = true;
|
||||
this.isManuallyClosed = true;
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public async connect(): Promise<any> {
|
||||
if (this.pubClient && this.subClient) {
|
||||
return this.connection;
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.pubClient = this.createClient();
|
||||
this.subClient = this.createClient();
|
||||
this.handleError(this.pubClient);
|
||||
this.handleError(this.subClient);
|
||||
|
||||
this.connection = Promise.all([
|
||||
[this.pubClient, this.subClient].forEach((client, index) => {
|
||||
const type = index === 0 ? 'pub' : 'sub';
|
||||
this.registerErrorListener(client);
|
||||
this.registerReconnectListener(client);
|
||||
this.registerReadyListener(client);
|
||||
this.registerEndListener(client);
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
client.on(event, (...args: [any]) => callback(type, ...args)),
|
||||
);
|
||||
});
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
this.connectionPromise = Promise.all([
|
||||
this.subClient.connect(),
|
||||
this.pubClient.connect(),
|
||||
]);
|
||||
await this.connection;
|
||||
|
||||
this.subClient.on(MESSAGE_EVENT, this.createResponseCallback());
|
||||
return this.connection;
|
||||
await this.connectionPromise;
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
public createClient(): Redis {
|
||||
@@ -78,8 +97,76 @@ export class ClientRedis extends ClientProxy {
|
||||
});
|
||||
}
|
||||
|
||||
public handleError(client: Redis) {
|
||||
client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err));
|
||||
public registerErrorListener(client: Redis) {
|
||||
client.addListener(RedisEventsMap.ERROR, (err: any) =>
|
||||
this.logger.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
public registerReconnectListener(client: {
|
||||
on: (event: string, fn: () => void) => void;
|
||||
}) {
|
||||
client.on(RedisEventsMap.RECONNECTING, () => {
|
||||
if (this.isManuallyClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
|
||||
// Prevent unhandled rejections
|
||||
this.connectionPromise.catch(() => {});
|
||||
|
||||
this._status$.next(RedisStatus.RECONNECTING);
|
||||
|
||||
if (this.wasInitialConnectionSuccessful) {
|
||||
this.logger.log('Reconnecting to Redis...');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerReadyListener(client: {
|
||||
on: (event: string, fn: () => void) => void;
|
||||
}) {
|
||||
client.on(RedisEventsMap.READY, () => {
|
||||
this.connectionPromise = Promise.resolve();
|
||||
this._status$.next(RedisStatus.CONNECTED);
|
||||
|
||||
this.logger.log('Connected to Redis. Subscribing to channels...');
|
||||
|
||||
if (!this.wasInitialConnectionSuccessful) {
|
||||
this.wasInitialConnectionSuccessful = true;
|
||||
this.subClient.on('message', this.createResponseCallback());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerEndListener(client: {
|
||||
on: (event: string, fn: () => void) => void;
|
||||
}) {
|
||||
client.on('end', () => {
|
||||
if (this.isManuallyClosed) {
|
||||
return;
|
||||
}
|
||||
this._status$.next(RedisStatus.DISCONNECTED);
|
||||
|
||||
if (this.getOptionsProp(this.options, 'retryAttempts') === undefined) {
|
||||
// When retryAttempts is not specified, the connection will not be re-established
|
||||
this.logger.error('Disconnected from Redis.');
|
||||
|
||||
// Clean up client instances and just recreate them when connect is called
|
||||
this.pubClient = this.subClient = null;
|
||||
} else {
|
||||
this.logger.error('Disconnected from Redis.');
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
|
||||
// Prevent unhandled rejections
|
||||
this.connectionPromise.catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getClientOptions(): Partial<RedisOptions['options']> {
|
||||
@@ -91,18 +178,42 @@ export class ClientRedis extends ClientProxy {
|
||||
};
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof RedisEvents = keyof RedisEvents,
|
||||
EventCallback extends RedisEvents[EventKey] = RedisEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.subClient && this.pubClient) {
|
||||
this.subClient.on(event, (...args: [any]) => callback('sub', ...args));
|
||||
this.pubClient.on(event, (...args: [any]) => callback('pub', ...args));
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.pubClient || !this.subClient) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return [this.pubClient, this.subClient] as T;
|
||||
}
|
||||
|
||||
public createRetryStrategy(times: number): undefined | number {
|
||||
if (this.isExplicitlyTerminated) {
|
||||
if (this.isManuallyClosed) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
!this.getOptionsProp(this.options, 'retryAttempts') ||
|
||||
times > this.getOptionsProp(this.options, 'retryAttempts')
|
||||
) {
|
||||
if (!this.getOptionsProp(this.options, 'retryAttempts')) {
|
||||
this.logger.error(
|
||||
'Redis connection closed and retry attempts not specified',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (times > this.getOptionsProp(this.options, 'retryAttempts')) {
|
||||
this.logger.error('Retry time exhausted');
|
||||
return;
|
||||
}
|
||||
return this.getOptionsProp(this.options, 'retryDelay') || 0;
|
||||
return this.getOptionsProp(this.options, 'retryDelay') ?? 5000;
|
||||
}
|
||||
|
||||
public createResponseCallback(): (
|
||||
|
||||
@@ -13,11 +13,7 @@ import {
|
||||
} from 'rxjs';
|
||||
import { first, map, retryWhen, scan, skip, switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
CONNECT_EVENT,
|
||||
CONNECT_FAILED_EVENT,
|
||||
DISCONNECT_EVENT,
|
||||
DISCONNECTED_RMQ_MESSAGE,
|
||||
ERROR_EVENT,
|
||||
RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT,
|
||||
RQM_DEFAULT_NO_ASSERT,
|
||||
RQM_DEFAULT_NOACK,
|
||||
@@ -27,47 +23,53 @@ import {
|
||||
RQM_DEFAULT_QUEUE_OPTIONS,
|
||||
RQM_DEFAULT_URL,
|
||||
} from '../constants';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { RmqEvents, RmqEventsMap, RmqStatus } from '../events/rmq.events';
|
||||
import { ReadPacket, RmqOptions, WritePacket } from '../interfaces';
|
||||
import { RmqRecord } from '../record-builders';
|
||||
import { RmqRecordSerializer } from '../serializers/rmq-record.serializer';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
|
||||
// import type {
|
||||
// AmqpConnectionManager,
|
||||
// ChannelWrapper,
|
||||
// } from 'amqp-connection-manager';
|
||||
// import type { Channel, ConsumeMessage } from 'amqplib';
|
||||
// To enable type safety for RMQ. This cant be uncommented by default
|
||||
// because it would require the user to install the amqplib package even if they dont use RabbitMQ
|
||||
// Otherwise, TypeScript would fail to compile the code.
|
||||
//
|
||||
// type AmqpConnectionManager =
|
||||
// import('amqp-connection-manager').AmqpConnectionManager;
|
||||
// type ChannelWrapper = import('amqp-connection-manager').ChannelWrapper;
|
||||
// type Channel = import('amqplib').Channel;
|
||||
// type ConsumeMessage = import('amqplib').ConsumeMessage;
|
||||
|
||||
type Channel = any;
|
||||
type ChannelWrapper = any;
|
||||
type ConsumeMessage = any;
|
||||
type AmqpConnectionManager = any;
|
||||
|
||||
let rmqPackage: any = {};
|
||||
let rmqPackage = {} as any; // typeof import('amqp-connection-manager');
|
||||
|
||||
const REPLY_QUEUE = 'amq.rabbitmq.reply-to';
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientRMQ extends ClientProxy {
|
||||
export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
|
||||
protected readonly logger = new Logger(ClientProxy.name);
|
||||
protected connection$: ReplaySubject<any>;
|
||||
protected connection: Promise<any>;
|
||||
protected connectionPromise: Promise<void>;
|
||||
protected client: AmqpConnectionManager = null;
|
||||
protected channel: ChannelWrapper = null;
|
||||
protected urls: string[] | RmqUrl[];
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof RmqEvents;
|
||||
callback: RmqEvents[keyof RmqEvents];
|
||||
}> = [];
|
||||
protected isInitialConnect = true;
|
||||
protected responseEmitter: EventEmitter;
|
||||
protected queue: string;
|
||||
protected queueOptions: Record<string, any>;
|
||||
protected responseEmitter: EventEmitter;
|
||||
protected replyQueue: string;
|
||||
protected persistent: boolean;
|
||||
protected noAssert: boolean;
|
||||
|
||||
constructor(protected readonly options: RmqOptions['options']) {
|
||||
super();
|
||||
this.urls = this.getOptionsProp(this.options, 'urls') || [RQM_DEFAULT_URL];
|
||||
this.queue =
|
||||
this.getOptionsProp(this.options, 'queue') || RQM_DEFAULT_QUEUE;
|
||||
this.queueOptions =
|
||||
@@ -75,8 +77,6 @@ export class ClientRMQ extends ClientProxy {
|
||||
RQM_DEFAULT_QUEUE_OPTIONS;
|
||||
this.replyQueue =
|
||||
this.getOptionsProp(this.options, 'replyQueue') || REPLY_QUEUE;
|
||||
this.persistent =
|
||||
this.getOptionsProp(this.options, 'persistent') || RQM_DEFAULT_PERSISTENT;
|
||||
this.noAssert =
|
||||
this.getOptionsProp(this.options, 'noAssert') ??
|
||||
this.queueOptions.noAssert ??
|
||||
@@ -96,15 +96,22 @@ export class ClientRMQ extends ClientProxy {
|
||||
this.client && this.client.close();
|
||||
this.channel = null;
|
||||
this.client = null;
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public connect(): Promise<any> {
|
||||
if (this.client) {
|
||||
return this.convertConnectionToPromise();
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.client = this.createClient();
|
||||
this.handleError(this.client);
|
||||
this.handleDisconnectError(this.client);
|
||||
|
||||
this.registerErrorListener(this.client);
|
||||
this.registerDisconnectListener(this.client);
|
||||
this.registerConnectListener(this.client);
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
this.client.on(event, callback),
|
||||
);
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
this.responseEmitter = new EventEmitter();
|
||||
this.responseEmitter.setMaxListeners(0);
|
||||
@@ -115,13 +122,16 @@ export class ClientRMQ extends ClientProxy {
|
||||
connect$,
|
||||
).pipe(switchMap(() => this.createChannel()));
|
||||
|
||||
const withReconnect$ = fromEvent(this.client, CONNECT_EVENT).pipe(skip(1));
|
||||
const withReconnect$ = fromEvent(this.client, RmqEventsMap.CONNECT).pipe(
|
||||
skip(1),
|
||||
);
|
||||
const source$ = merge(withDisconnect$, withReconnect$);
|
||||
|
||||
this.connection$ = new ReplaySubject(1);
|
||||
source$.subscribe(this.connection$);
|
||||
this.connectionPromise = this.convertConnectionToPromise();
|
||||
|
||||
return this.convertConnectionToPromise();
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
public createChannel(): Promise<void> {
|
||||
@@ -135,9 +145,8 @@ export class ClientRMQ extends ClientProxy {
|
||||
|
||||
public createClient(): AmqpConnectionManager {
|
||||
const socketOptions = this.getOptionsProp(this.options, 'socketOptions');
|
||||
return rmqPackage.connect(this.urls, {
|
||||
connectionOptions: socketOptions?.connectionOptions,
|
||||
});
|
||||
const urls = this.getOptionsProp(this.options, 'urls') || [RQM_DEFAULT_URL];
|
||||
return rmqPackage.connect(urls, socketOptions);
|
||||
}
|
||||
|
||||
public mergeDisconnectEvent<T = any>(
|
||||
@@ -150,10 +159,11 @@ export class ClientRMQ extends ClientProxy {
|
||||
throw err;
|
||||
}),
|
||||
);
|
||||
const disconnect$ = eventToError(DISCONNECT_EVENT);
|
||||
const disconnect$ = eventToError(RmqEventsMap.DISCONNECT);
|
||||
|
||||
const urls = this.getOptionsProp(this.options, 'urls', []);
|
||||
const connectFailed$ = eventToError(CONNECT_FAILED_EVENT).pipe(
|
||||
const connectFailedEventKey = 'connectFailed';
|
||||
const connectFailed$ = eventToError(connectFailedEventKey).pipe(
|
||||
retryWhen(e =>
|
||||
e.pipe(
|
||||
scan((errorCount, error: any) => {
|
||||
@@ -192,6 +202,15 @@ export class ClientRMQ extends ClientProxy {
|
||||
if (!this.noAssert) {
|
||||
await channel.assertQueue(this.queue, this.queueOptions);
|
||||
}
|
||||
|
||||
if (this.options.exchange && this.options.routingKey) {
|
||||
await channel.bindQueue(
|
||||
this.queue,
|
||||
this.options.exchange,
|
||||
this.options.routingKey,
|
||||
);
|
||||
}
|
||||
|
||||
await channel.prefetch(prefetchCount, isGlobalPrefetchCount);
|
||||
await this.consumeChannel(channel);
|
||||
resolve();
|
||||
@@ -209,31 +228,81 @@ export class ClientRMQ extends ClientProxy {
|
||||
);
|
||||
}
|
||||
|
||||
public handleError(client: AmqpConnectionManager): void {
|
||||
client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err));
|
||||
public registerErrorListener(client: AmqpConnectionManager): void {
|
||||
client.addListener(RmqEventsMap.ERROR, (err: any) =>
|
||||
this.logger.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
public handleDisconnectError(client: AmqpConnectionManager): void {
|
||||
client.addListener(DISCONNECT_EVENT, (err: any) => {
|
||||
public registerDisconnectListener(client: AmqpConnectionManager): void {
|
||||
client.addListener(RmqEventsMap.DISCONNECT, (err: any) => {
|
||||
this._status$.next(RmqStatus.DISCONNECTED);
|
||||
|
||||
if (!this.isInitialConnect) {
|
||||
this.connectionPromise = Promise.reject(
|
||||
'Error: Connection lost. Trying to reconnect...',
|
||||
);
|
||||
|
||||
// Prevent unhandled promise rejection
|
||||
this.connectionPromise.catch(() => {});
|
||||
}
|
||||
|
||||
this.logger.error(DISCONNECTED_RMQ_MESSAGE);
|
||||
this.logger.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
private registerConnectListener(client: AmqpConnectionManager): void {
|
||||
client.addListener(RmqEventsMap.CONNECT, () => {
|
||||
this._status$.next(RmqStatus.CONNECTED);
|
||||
this.logger.log('Successfully connected to RMQ broker');
|
||||
|
||||
if (this.isInitialConnect) {
|
||||
this.isInitialConnect = false;
|
||||
|
||||
if (!this.channel) {
|
||||
this.connectionPromise = this.createChannel();
|
||||
}
|
||||
} else {
|
||||
this.connectionPromise = Promise.resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof RmqEvents = keyof RmqEvents,
|
||||
EventCallback extends RmqEvents[EventKey] = RmqEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.client) {
|
||||
this.client.addListener(event, callback);
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.client) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.client as T;
|
||||
}
|
||||
|
||||
public async handleMessage(
|
||||
packet: unknown,
|
||||
callback: (packet: WritePacket) => any,
|
||||
);
|
||||
): Promise<void>;
|
||||
public async handleMessage(
|
||||
packet: unknown,
|
||||
options: Record<string, unknown>,
|
||||
callback: (packet: WritePacket) => any,
|
||||
);
|
||||
): Promise<void>;
|
||||
public async handleMessage(
|
||||
packet: unknown,
|
||||
options: Record<string, unknown> | ((packet: WritePacket) => any),
|
||||
callback?: (packet: WritePacket) => any,
|
||||
) {
|
||||
): Promise<void> {
|
||||
if (isFunction(options)) {
|
||||
callback = options as (packet: WritePacket) => any;
|
||||
options = undefined;
|
||||
@@ -289,7 +358,11 @@ export class ClientRMQ extends ClientProxy {
|
||||
Buffer.from(JSON.stringify(serializedPacket)),
|
||||
{
|
||||
replyTo: this.replyQueue,
|
||||
persistent: this.persistent,
|
||||
persistent: this.getOptionsProp(
|
||||
this.options,
|
||||
'persistent',
|
||||
RQM_DEFAULT_PERSISTENT,
|
||||
),
|
||||
...options,
|
||||
headers: this.mergeHeaders(options?.headers),
|
||||
correlationId,
|
||||
@@ -314,7 +387,11 @@ export class ClientRMQ extends ClientProxy {
|
||||
this.queue,
|
||||
Buffer.from(JSON.stringify(serializedPacket)),
|
||||
{
|
||||
persistent: this.persistent,
|
||||
persistent: this.getOptionsProp(
|
||||
this.options,
|
||||
'persistent',
|
||||
RQM_DEFAULT_PERSISTENT,
|
||||
),
|
||||
...options,
|
||||
headers: this.mergeHeaders(options?.headers),
|
||||
},
|
||||
|
||||
@@ -2,17 +2,10 @@ import { Logger, Type } from '@nestjs/common';
|
||||
import * as net from 'net';
|
||||
import { EmptyError, lastValueFrom } from 'rxjs';
|
||||
import { share, tap } from 'rxjs/operators';
|
||||
import { ConnectionOptions } from 'tls';
|
||||
import {
|
||||
CLOSE_EVENT,
|
||||
ECONNREFUSED,
|
||||
ERROR_EVENT,
|
||||
MESSAGE_EVENT,
|
||||
TCP_DEFAULT_HOST,
|
||||
TCP_DEFAULT_PORT,
|
||||
} from '../constants';
|
||||
import { ConnectionOptions, connect as tlsConnect, TLSSocket } from 'tls';
|
||||
import { ECONNREFUSED, TCP_DEFAULT_HOST, TCP_DEFAULT_PORT } from '../constants';
|
||||
import { TcpEvents, TcpEventsMap, TcpStatus } from '../events/tcp.events';
|
||||
import { JsonSocket, TcpSocket } from '../helpers';
|
||||
import { connect as tlsConnect, TLSSocket } from 'tls';
|
||||
import { PacketId, ReadPacket, WritePacket } from '../interfaces';
|
||||
import { TcpClientOptions } from '../interfaces/client-metadata.interface';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
@@ -20,15 +13,18 @@ import { ClientProxy } from './client-proxy';
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ClientTCP extends ClientProxy {
|
||||
protected connection: Promise<any>;
|
||||
private readonly logger = new Logger(ClientTCP.name);
|
||||
private readonly port: number;
|
||||
private readonly host: string;
|
||||
private readonly socketClass: Type<TcpSocket>;
|
||||
private isConnected = false;
|
||||
private socket: TcpSocket;
|
||||
public tlsOptions?: ConnectionOptions;
|
||||
export class ClientTCP extends ClientProxy<TcpEvents, TcpStatus> {
|
||||
protected readonly logger = new Logger(ClientTCP.name);
|
||||
protected readonly port: number;
|
||||
protected readonly host: string;
|
||||
protected readonly socketClass: Type<TcpSocket>;
|
||||
protected readonly tlsOptions?: ConnectionOptions;
|
||||
protected socket: TcpSocket;
|
||||
protected connectionPromise: Promise<any>;
|
||||
protected pendingEventListeners: Array<{
|
||||
event: keyof TcpEvents;
|
||||
callback: TcpEvents[keyof TcpEvents];
|
||||
}> = [];
|
||||
|
||||
constructor(options: TcpClientOptions['options']) {
|
||||
super();
|
||||
@@ -43,16 +39,22 @@ export class ClientTCP extends ClientProxy {
|
||||
}
|
||||
|
||||
public connect(): Promise<any> {
|
||||
if (this.connection) {
|
||||
return this.connection;
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
this.socket = this.createSocket();
|
||||
this.bindEvents(this.socket);
|
||||
this.registerConnectListener(this.socket);
|
||||
this.registerCloseListener(this.socket);
|
||||
this.registerErrorListener(this.socket);
|
||||
|
||||
this.pendingEventListeners.forEach(({ event, callback }) =>
|
||||
this.socket.on(event, callback as any),
|
||||
);
|
||||
this.pendingEventListeners = [];
|
||||
|
||||
const source$ = this.connect$(this.socket.netSocket).pipe(
|
||||
tap(() => {
|
||||
this.isConnected = true;
|
||||
this.socket.on(MESSAGE_EVENT, (buffer: WritePacket & PacketId) =>
|
||||
this.socket.on('message', (buffer: WritePacket & PacketId) =>
|
||||
this.handleResponse(buffer),
|
||||
);
|
||||
}),
|
||||
@@ -63,14 +65,14 @@ export class ClientTCP extends ClientProxy {
|
||||
if (!this.tlsOptions) {
|
||||
this.socket.connect(this.port, this.host);
|
||||
}
|
||||
this.connection = lastValueFrom(source$).catch(err => {
|
||||
this.connectionPromise = lastValueFrom(source$).catch(err => {
|
||||
if (err instanceof EmptyError) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
return this.connection;
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
public async handleResponse(buffer: unknown): Promise<void> {
|
||||
@@ -114,14 +116,30 @@ export class ClientTCP extends ClientProxy {
|
||||
public close() {
|
||||
this.socket && this.socket.end();
|
||||
this.handleClose();
|
||||
this.pendingEventListeners = [];
|
||||
}
|
||||
|
||||
public bindEvents(socket: TcpSocket) {
|
||||
socket.on(
|
||||
ERROR_EVENT,
|
||||
(err: any) => err.code !== ECONNREFUSED && this.handleError(err),
|
||||
);
|
||||
socket.on(CLOSE_EVENT, () => this.handleClose());
|
||||
public registerConnectListener(socket: TcpSocket) {
|
||||
socket.on(TcpEventsMap.CONNECT, () => {
|
||||
this._status$.next(TcpStatus.CONNECTED);
|
||||
});
|
||||
}
|
||||
|
||||
public registerErrorListener(socket: TcpSocket) {
|
||||
socket.on(TcpEventsMap.ERROR, err => {
|
||||
if (err.code !== ECONNREFUSED) {
|
||||
this.handleError(err);
|
||||
} else {
|
||||
this._status$.next(TcpStatus.DISCONNECTED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public registerCloseListener(socket: TcpSocket) {
|
||||
socket.on(TcpEventsMap.CLOSE, () => {
|
||||
this._status$.next(TcpStatus.DISCONNECTED);
|
||||
this.handleClose();
|
||||
});
|
||||
}
|
||||
|
||||
public handleError(err: any) {
|
||||
@@ -129,9 +147,8 @@ export class ClientTCP extends ClientProxy {
|
||||
}
|
||||
|
||||
public handleClose() {
|
||||
this.isConnected = false;
|
||||
this.socket = null;
|
||||
this.connection = undefined;
|
||||
this.connectionPromise = undefined;
|
||||
|
||||
if (this.routingMap.size > 0) {
|
||||
const err = new Error('Connection closed');
|
||||
@@ -142,6 +159,26 @@ export class ClientTCP extends ClientProxy {
|
||||
}
|
||||
}
|
||||
|
||||
public on<
|
||||
EventKey extends keyof TcpEvents = keyof TcpEvents,
|
||||
EventCallback extends TcpEvents[EventKey] = TcpEvents[EventKey],
|
||||
>(event: EventKey, callback: EventCallback) {
|
||||
if (this.socket) {
|
||||
this.socket.on(event, callback as any);
|
||||
} else {
|
||||
this.pendingEventListeners.push({ event, callback });
|
||||
}
|
||||
}
|
||||
|
||||
public unwrap<T>(): T {
|
||||
if (!this.socket) {
|
||||
throw new Error(
|
||||
'Not initialized. Please call the "connect" method first.',
|
||||
);
|
||||
}
|
||||
return this.socket.netSocket as T;
|
||||
}
|
||||
|
||||
protected publish(
|
||||
partialPacket: ReadPacket,
|
||||
callback: (packet: WritePacket) => any,
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const GRPC_CANCELLED = 'Cancelled';
|
||||
export const RABBITMQ_REPLY_QUEUE = 'amq.rabbitmq.reply-to';
|
||||
@@ -2,25 +2,29 @@ import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants';
|
||||
|
||||
export const TCP_DEFAULT_PORT = 3000;
|
||||
export const TCP_DEFAULT_HOST = 'localhost';
|
||||
|
||||
export const REDIS_DEFAULT_PORT = 6379;
|
||||
export const REDIS_DEFAULT_HOST = 'localhost';
|
||||
|
||||
export const NATS_DEFAULT_URL = 'nats://localhost:4222';
|
||||
export const MQTT_DEFAULT_URL = 'mqtt://localhost:1883';
|
||||
export const GRPC_DEFAULT_URL = 'localhost:5000';
|
||||
export const RQM_DEFAULT_URL = 'amqp://localhost';
|
||||
export const KAFKA_DEFAULT_BROKER = 'localhost:9092';
|
||||
export const KAFKA_DEFAULT_CLIENT = 'nestjs-consumer';
|
||||
export const KAFKA_DEFAULT_GROUP = 'nestjs-group';
|
||||
export const MQTT_SEPARATOR = '/';
|
||||
export const MQTT_WILDCARD_SINGLE = '+';
|
||||
export const MQTT_WILDCARD_ALL = '#';
|
||||
export const RQM_DEFAULT_QUEUE = 'default';
|
||||
export const RQM_DEFAULT_PREFETCH_COUNT = 0;
|
||||
export const RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT = false;
|
||||
export const RQM_DEFAULT_QUEUE_OPTIONS = {};
|
||||
export const RQM_DEFAULT_NOACK = true;
|
||||
export const RQM_DEFAULT_PERSISTENT = false;
|
||||
export const RQM_DEFAULT_NO_ASSERT = false;
|
||||
|
||||
export const CONNECT_EVENT = 'connect';
|
||||
export const DISCONNECT_EVENT = 'disconnect';
|
||||
export const CONNECT_FAILED_EVENT = 'connectFailed';
|
||||
export const MESSAGE_EVENT = 'message';
|
||||
export const DATA_EVENT = 'data';
|
||||
export const ERROR_EVENT = 'error';
|
||||
export const CLOSE_EVENT = 'close';
|
||||
export const SUBSCRIBE = 'subscribe';
|
||||
export const CANCEL_EVENT = 'cancelled';
|
||||
export const ECONNREFUSED = 'ECONNREFUSED';
|
||||
export const CONN_ERR = 'CONN_ERR';
|
||||
export const EADDRINUSE = 'EADDRINUSE';
|
||||
|
||||
export const PATTERN_METADATA = 'microservices:pattern';
|
||||
export const PATTERN_EXTRAS_METADATA = 'microservices:pattern_extras';
|
||||
@@ -29,17 +33,9 @@ export const CLIENT_CONFIGURATION_METADATA = 'microservices:client';
|
||||
export const PATTERN_HANDLER_METADATA = 'microservices:handler_type';
|
||||
export const CLIENT_METADATA = 'microservices:is_client_instance';
|
||||
export const PARAM_ARGS_METADATA = ROUTE_ARGS_METADATA;
|
||||
|
||||
export const REQUEST_PATTERN_METADATA = 'microservices:request_pattern';
|
||||
export const REPLY_PATTERN_METADATA = 'microservices:reply_pattern';
|
||||
|
||||
export const RQM_DEFAULT_QUEUE = 'default';
|
||||
export const RQM_DEFAULT_PREFETCH_COUNT = 0;
|
||||
export const RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT = false;
|
||||
export const RQM_DEFAULT_QUEUE_OPTIONS = {};
|
||||
export const RQM_DEFAULT_NOACK = true;
|
||||
export const RQM_DEFAULT_PERSISTENT = false;
|
||||
export const RQM_DEFAULT_NO_ASSERT = false;
|
||||
export const RQM_NO_EVENT_HANDLER = (
|
||||
text: TemplateStringsArray,
|
||||
pattern: string,
|
||||
@@ -55,19 +51,8 @@ export const GRPC_DEFAULT_PROTO_LOADER = '@grpc/proto-loader';
|
||||
export const NO_EVENT_HANDLER = (text: TemplateStringsArray, pattern: string) =>
|
||||
`There is no matching event handler defined in the remote service. Event pattern: ${pattern}`;
|
||||
export const NO_MESSAGE_HANDLER = `There is no matching message handler defined in the remote service.`;
|
||||
|
||||
export const DISCONNECTED_RMQ_MESSAGE = `Disconnected from RMQ. Trying to reconnect.`;
|
||||
|
||||
export const KAFKA_DEFAULT_CLIENT = 'nestjs-consumer';
|
||||
export const KAFKA_DEFAULT_GROUP = 'nestjs-group';
|
||||
|
||||
export const MQTT_SEPARATOR = '/';
|
||||
export const MQTT_WILDCARD_SINGLE = '+';
|
||||
export const MQTT_WILDCARD_ALL = '#';
|
||||
|
||||
export const ECONNREFUSED = 'ECONNREFUSED';
|
||||
export const CONN_ERR = 'CONN_ERR';
|
||||
export const EADDRINUSE = 'EADDRINUSE';
|
||||
|
||||
export const CONNECTION_FAILED_MESSAGE =
|
||||
'Connection to transport failed. Trying to reconnect...';
|
||||
|
||||
export const NATS_DEFAULT_GRACE_PERIOD = 10000;
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { ClientProxy } from './client/client-proxy';
|
||||
import { Closeable } from './interfaces/closeable.interface';
|
||||
|
||||
export type CloseableClient = Closeable & ClientProxy;
|
||||
|
||||
export class ClientsContainer {
|
||||
private clients: CloseableClient[] = [];
|
||||
private clients: ClientProxy[] = [];
|
||||
|
||||
public getAllClients(): CloseableClient[] {
|
||||
public getAllClients(): ClientProxy[] {
|
||||
return this.clients;
|
||||
}
|
||||
|
||||
public addClient(client: CloseableClient) {
|
||||
public addClient(client: ClientProxy) {
|
||||
this.clients.push(client);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { NatsCodec } from '../external/nats-client.interface';
|
||||
import { NatsCodec } from '../external/nats-codec.interface';
|
||||
import { IncomingEvent, IncomingRequest } from '../interfaces';
|
||||
import { IncomingRequestDeserializer } from './incoming-request.deserializer';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { NatsCodec } from '../external/nats-client.interface';
|
||||
import { NatsCodec } from '../external/nats-codec.interface';
|
||||
import { IncomingResponse } from '../interfaces';
|
||||
import { IncomingResponseDeserializer } from './incoming-response.deserializer';
|
||||
import { NatsRequestJSONDeserializer } from './nats-request-json.deserializer';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class MaxPacketLengthExceededException extends Error {
|
||||
constructor(length: number) {
|
||||
super(`The packet length (${length}) exceeds the maximum allowed length`);
|
||||
}
|
||||
}
|
||||
6
packages/microservices/events/index.ts
Normal file
6
packages/microservices/events/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { KafkaStatus } from './kafka.events';
|
||||
export { MqttEvents, MqttStatus } from './mqtt.events';
|
||||
export { NatsEvents, NatsStatus } from './nats.events';
|
||||
export { RedisEvents, RedisStatus } from './redis.events';
|
||||
export { RmqEvents, RmqStatus } from './rmq.events';
|
||||
export { TcpEvents, TcpStatus } from './tcp.events';
|
||||
7
packages/microservices/events/kafka.events.ts
Normal file
7
packages/microservices/events/kafka.events.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const enum KafkaStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTED = 'connected',
|
||||
CRASHED = 'crashed',
|
||||
STOPPED = 'stopped',
|
||||
REBALANCING = 'rebalancing',
|
||||
}
|
||||
39
packages/microservices/events/mqtt.events.ts
Normal file
39
packages/microservices/events/mqtt.events.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
type VoidCallback = () => void;
|
||||
type OnPacketCallback = (packet: any) => void;
|
||||
type OnErrorCallback = (error: Error) => void;
|
||||
|
||||
export const enum MqttStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
RECONNECTING = 'reconnecting',
|
||||
CONNECTED = 'connected',
|
||||
CLOSED = 'closed',
|
||||
}
|
||||
|
||||
export const enum MqttEventsMap {
|
||||
CONNECT = 'connect',
|
||||
RECONNECT = 'reconnect',
|
||||
DISCONNECT = 'disconnect',
|
||||
CLOSE = 'close',
|
||||
OFFLINE = 'offline',
|
||||
END = 'end',
|
||||
ERROR = 'error',
|
||||
PACKETRECEIVE = 'packetreceive',
|
||||
PACKETSEND = 'packetsend',
|
||||
}
|
||||
|
||||
/**
|
||||
* MQTT events map for the MQTT client.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type MqttEvents = {
|
||||
connect: OnPacketCallback;
|
||||
reconnect: VoidCallback;
|
||||
disconnect: OnPacketCallback;
|
||||
close: VoidCallback;
|
||||
offline: VoidCallback;
|
||||
end: VoidCallback;
|
||||
error: OnErrorCallback;
|
||||
packetreceive: OnPacketCallback;
|
||||
packetsend: OnPacketCallback;
|
||||
};
|
||||
29
packages/microservices/events/nats.events.ts
Normal file
29
packages/microservices/events/nats.events.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
type DefaultCallback = (data?: string | number) => any;
|
||||
|
||||
export type ServersChangedEvent = {
|
||||
added: string[];
|
||||
deleted: string[];
|
||||
};
|
||||
|
||||
export const enum NatsStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
RECONNECTING = 'reconnecting',
|
||||
CONNECTED = 'connected',
|
||||
}
|
||||
|
||||
export const enum NatsEventsMap {
|
||||
DISCONNECT = 'disconnect',
|
||||
RECONNECT = 'reconnect',
|
||||
UPDATE = 'update',
|
||||
}
|
||||
|
||||
/**
|
||||
* Nats events map for the Nats client.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type NatsEvents = {
|
||||
disconnect: DefaultCallback;
|
||||
reconnect: DefaultCallback;
|
||||
update: (data?: string | number | ServersChangedEvent) => any;
|
||||
};
|
||||
34
packages/microservices/events/redis.events.ts
Normal file
34
packages/microservices/events/redis.events.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
type VoidCallback = (client: 'pub' | 'sub') => void;
|
||||
type OnErrorCallback = (client: 'pub' | 'sub', error: Error) => void;
|
||||
type OnWarningCallback = (client: 'pub' | 'sub', warning: any) => void;
|
||||
|
||||
export const enum RedisStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
RECONNECTING = 'reconnecting',
|
||||
CONNECTED = 'connected',
|
||||
}
|
||||
|
||||
export const enum RedisEventsMap {
|
||||
CONNECT = 'connect',
|
||||
READY = 'ready',
|
||||
ERROR = 'error',
|
||||
CLOSE = 'close',
|
||||
RECONNECTING = 'reconnecting',
|
||||
END = 'end',
|
||||
WARNING = 'warning',
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis events map for the Redis client.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type RedisEvents = {
|
||||
connect: VoidCallback;
|
||||
ready: VoidCallback;
|
||||
error: OnErrorCallback;
|
||||
close: VoidCallback;
|
||||
reconnecting: VoidCallback;
|
||||
end: VoidCallback;
|
||||
warning: OnWarningCallback;
|
||||
};
|
||||
24
packages/microservices/events/rmq.events.ts
Normal file
24
packages/microservices/events/rmq.events.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
type VoidCallback = () => void;
|
||||
type OnErrorCallback = (error: Error) => void;
|
||||
|
||||
export const enum RmqStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTED = 'connected',
|
||||
}
|
||||
|
||||
export const enum RmqEventsMap {
|
||||
ERROR = 'error',
|
||||
DISCONNECT = 'disconnect',
|
||||
CONNECT = 'connect',
|
||||
}
|
||||
|
||||
/**
|
||||
* RabbitMQ events map for the ampqlip client.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type RmqEvents = {
|
||||
error: OnErrorCallback;
|
||||
disconnect: VoidCallback;
|
||||
connect: VoidCallback;
|
||||
};
|
||||
39
packages/microservices/events/tcp.events.ts
Normal file
39
packages/microservices/events/tcp.events.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
type VoidCallback = () => void;
|
||||
type OnErrorCallback = (error: Error) => void;
|
||||
type OnLookupCallback = (
|
||||
err: Error,
|
||||
address: string,
|
||||
family: string,
|
||||
host: string,
|
||||
) => void;
|
||||
|
||||
export const enum TcpStatus {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTED = 'connected',
|
||||
}
|
||||
|
||||
export const enum TcpEventsMap {
|
||||
ERROR = 'error',
|
||||
CONNECT = 'connect',
|
||||
END = 'end',
|
||||
CLOSE = 'close',
|
||||
TIMEOUT = 'timeout',
|
||||
DRAIN = 'drain',
|
||||
LOOKUP = 'lookup',
|
||||
LISTENING = 'listening',
|
||||
}
|
||||
|
||||
/**
|
||||
* TCP events map for the net TCP socket.
|
||||
* Key is the event name and value is the corresponding callback function.
|
||||
* @publicApi
|
||||
*/
|
||||
export type TcpEvents = {
|
||||
error: OnErrorCallback;
|
||||
connect: VoidCallback;
|
||||
end: VoidCallback;
|
||||
close: VoidCallback;
|
||||
timeout: VoidCallback;
|
||||
drain: VoidCallback;
|
||||
lookup: OnLookupCallback;
|
||||
};
|
||||
@@ -26,11 +26,8 @@ export class BaseRpcExceptionFilter<T = any, R = any>
|
||||
public handleUnknownError(exception: T, status: string) {
|
||||
const errorMessage = MESSAGES.UNKNOWN_EXCEPTION_MESSAGE;
|
||||
|
||||
const loggerArgs = this.isError(exception)
|
||||
? [exception.message, exception.stack]
|
||||
: [exception];
|
||||
const logger = BaseRpcExceptionFilter.logger;
|
||||
logger.error.apply(logger, loggerArgs as any);
|
||||
logger.error.apply(logger, [exception]);
|
||||
|
||||
return _throw(() => ({ status, message: errorMessage }));
|
||||
}
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
/**
|
||||
* @see https://github.com/mqttjs/MQTT.js/
|
||||
*
|
||||
* @publicApi
|
||||
*
|
||||
*/
|
||||
export declare class MqttClient extends EventEmitter {
|
||||
public connected: boolean;
|
||||
public disconnecting: boolean;
|
||||
public disconnected: boolean;
|
||||
public reconnecting: boolean;
|
||||
public incomingStore: any;
|
||||
public outgoingStore: any;
|
||||
public options: any;
|
||||
public queueQoSZero: boolean;
|
||||
|
||||
constructor(streamBuilder: (client: MqttClient) => any, options: any);
|
||||
|
||||
public on(event: 'message', cb: any): this;
|
||||
public on(event: 'packetsend' | 'packetreceive', cb: any): this;
|
||||
public on(event: 'error', cb: any): this;
|
||||
public on(event: string, cb: Function): this;
|
||||
|
||||
public once(event: 'message', cb: any): this;
|
||||
public once(event: 'packetsend' | 'packetreceive', cb: any): this;
|
||||
public once(event: 'error', cb: any): this;
|
||||
public once(event: string, cb: Function): this;
|
||||
|
||||
/**
|
||||
* publish - publish <message> to <topic>
|
||||
*
|
||||
* @param {String} topic - topic to publish to
|
||||
* @param {(String|Buffer)} message - message to publish
|
||||
*
|
||||
* @param {Object} [opts] - publish options, includes:
|
||||
* @param {Number} [opts.qos] - qos level to publish on
|
||||
* @param {Boolean} [opts.retain] - whether or not to retain the message
|
||||
*
|
||||
* @param {Function} [callback] - function(err){}
|
||||
* called when publish succeeds or fails
|
||||
* @returns {Client} this - for chaining
|
||||
* @api public
|
||||
*
|
||||
* @example client.publish('topic', 'message')
|
||||
* @example
|
||||
* client.publish('topic', 'message', {qos: 1, retain: true})
|
||||
* @example client.publish('topic', 'message', console.log)
|
||||
*/
|
||||
public publish(
|
||||
topic: string,
|
||||
message: string | Buffer,
|
||||
opts: any,
|
||||
callback?: any,
|
||||
): this;
|
||||
public publish(topic: string, message: string | Buffer, callback?: any): this;
|
||||
|
||||
/**
|
||||
* subscribe - subscribe to <topic>
|
||||
*
|
||||
* @param {String, Array, Object} topic - topic(s) to subscribe to, supports objects in the form {'topic': qos}
|
||||
* @param {Object} [opts] - optional subscription options, includes:
|
||||
* @param {Number} [opts.qos] - subscribe qos level
|
||||
* @param {Function} [callback] - function(err, granted){} where:
|
||||
* {Error} err - subscription error (none at the moment!)
|
||||
* {Array} granted - array of {topic: 't', qos: 0}
|
||||
* @returns {MqttClient} this - for chaining
|
||||
* @api public
|
||||
* @example client.subscribe('topic')
|
||||
* @example client.subscribe('topic', {qos: 1})
|
||||
* @example client.subscribe({'topic': 0, 'topic2': 1}, console.log)
|
||||
* @example client.subscribe('topic', console.log)
|
||||
*/
|
||||
public subscribe(topic: string | string[], opts: any, callback?: any): this;
|
||||
public subscribe(topic: string | string[] | any, callback?: any): this;
|
||||
|
||||
/**
|
||||
* unsubscribe - unsubscribe from topic(s)
|
||||
*
|
||||
* @param {string|Array} topic - topics to unsubscribe from
|
||||
* @param {Function} [callback] - callback fired on unsuback
|
||||
* @returns {MqttClient} this - for chaining
|
||||
* @api public
|
||||
* @example client.unsubscribe('topic')
|
||||
* @example client.unsubscribe('topic', console.log)
|
||||
*/
|
||||
public unsubscribe(topic: string | string[], callback?: any): this;
|
||||
|
||||
/**
|
||||
* end - close connection
|
||||
*
|
||||
* @returns {MqttClient} this - for chaining
|
||||
* @param {Boolean} force - do not wait for all in-flight messages to be acked
|
||||
* @param {Function} cb - called when the client has been closed
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
public end(force?: boolean, cb?: any): this;
|
||||
|
||||
/**
|
||||
* removeOutgoingMessage - remove a message in outgoing store
|
||||
* the outgoing callback will be called withe Error('Message removed') if the message is removed
|
||||
*
|
||||
* @param {Number} mid - messageId to remove message
|
||||
* @returns {MqttClient} this - for chaining
|
||||
* @api public
|
||||
*
|
||||
* @example client.removeOutgoingMessage(client.getLastMessageId());
|
||||
*/
|
||||
public removeOutgoingMessage(mid: number): this;
|
||||
|
||||
/**
|
||||
* reconnect - connect again using the same options as connect()
|
||||
*
|
||||
* @param {Object} [opts] - optional reconnect options, includes:
|
||||
* {any} incomingStore - a store for the incoming packets
|
||||
* {any} outgoingStore - a store for the outgoing packets
|
||||
* if opts is not given, current stores are used
|
||||
*
|
||||
* @returns {MqttClient} this - for chaining
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
public reconnect(opts?: any): this;
|
||||
|
||||
/**
|
||||
* Handle messages with backpressure support, one at a time.
|
||||
* Override at will.
|
||||
*
|
||||
* @param packet packet the packet
|
||||
* @param callback callback call when finished
|
||||
* @api public
|
||||
*/
|
||||
public handleMessage(packet: any, callback: any): void;
|
||||
|
||||
/**
|
||||
* getLastMessageId
|
||||
*/
|
||||
public getLastMessageId(): number;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* @see https://github.com/nats-io/nats.js
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface NatsCodec<T> {
|
||||
encode(d: T): Uint8Array;
|
||||
decode(a: Uint8Array): T;
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
timeout: number;
|
||||
headers?: any;
|
||||
noMux?: boolean;
|
||||
reply?: string;
|
||||
}
|
||||
interface PublishOptions {
|
||||
reply?: string;
|
||||
headers?: any;
|
||||
}
|
||||
interface SubOpts<T> {
|
||||
queue?: string;
|
||||
max?: number;
|
||||
timeout?: number;
|
||||
callback?: (err: object | null, msg: T) => void;
|
||||
}
|
||||
|
||||
declare type SubscriptionOptions = SubOpts<NatsMsg>;
|
||||
|
||||
export interface NatsMsg {
|
||||
subject: string;
|
||||
sid: number;
|
||||
reply?: string;
|
||||
data: Uint8Array;
|
||||
headers?: any;
|
||||
respond(data?: Uint8Array, opts?: PublishOptions): boolean;
|
||||
}
|
||||
|
||||
interface Sub<T> extends AsyncIterable<T> {
|
||||
unsubscribe(max?: number): void;
|
||||
drain(): Promise<void>;
|
||||
isDraining(): boolean;
|
||||
isClosed(): boolean;
|
||||
callback(err: object | null, msg: NatsMsg): void;
|
||||
getSubject(): string;
|
||||
getReceived(): number;
|
||||
getProcessed(): number;
|
||||
getPending(): number;
|
||||
getID(): number;
|
||||
getMax(): number | undefined;
|
||||
}
|
||||
|
||||
declare type Subscription = Sub<NatsMsg>;
|
||||
|
||||
declare enum Events {
|
||||
Disconnect = 'disconnect',
|
||||
Reconnect = 'reconnect',
|
||||
Update = 'update',
|
||||
LDM = 'ldm',
|
||||
Error = 'error',
|
||||
}
|
||||
interface Status {
|
||||
type: Events | DebugEvents;
|
||||
data: string | number;
|
||||
}
|
||||
|
||||
declare enum DebugEvents {
|
||||
Reconnecting = 'reconnecting',
|
||||
PingTimer = 'pingTimer',
|
||||
StaleConnection = 'staleConnection',
|
||||
}
|
||||
|
||||
export declare class Client {
|
||||
info?: Record<string, any>;
|
||||
closed(): Promise<void | Error>;
|
||||
close(): Promise<void>;
|
||||
publish(subject: string, data?: Uint8Array, options?: PublishOptions): void;
|
||||
subscribe(subject: string, opts?: SubscriptionOptions): Subscription;
|
||||
request(
|
||||
subject: string,
|
||||
data?: Uint8Array,
|
||||
opts?: RequestOptions,
|
||||
): Promise<NatsMsg>;
|
||||
flush(): Promise<void>;
|
||||
drain(): Promise<void>;
|
||||
isClosed(): boolean;
|
||||
isDraining(): boolean;
|
||||
getServer(): string;
|
||||
status(): AsyncIterable<Status>;
|
||||
stats(): Record<string, any>;
|
||||
jetstreamManager(opts?: Record<string, any>): Promise<any>;
|
||||
jetstream(opts?: Record<string, any>): any;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user