Compare commits

..

110 Commits

Author SHA1 Message Date
Kamil Myśliwiec
ab660c7edd test: add unit test 2024-11-25 13:48:54 +01:00
Kamil Myśliwiec
21279a9c49 feat(microservices): support nats queue per handler 2024-11-25 13:42:24 +01:00
Kamil Myśliwiec
03a61e9728 chore: revert apply decorators signature (regression) 2024-11-21 10:41:37 +01:00
Kamil Myśliwiec
212d07fcbf refactor(common): improve apply decorators types 2024-11-20 12:45:29 +01:00
Kamil Mysliwiec
bdc9b4409a Merge pull request #14142 from nestjs/feat/microservice-client-server-additions
feat(microservices): add status, unwrap, on, and other features
2024-11-20 11:04:50 +01:00
Kamil Myśliwiec
c66d1fb5f7 fix: add type constraints and assertions 2024-11-20 10:41:37 +01:00
Kamil Mysliwiec
9356509274 Update packages/microservices/server/server-nats.ts
Co-authored-by: Rick Dutour Geerling <rick@trilon.io>
2024-11-20 10:22:02 +01:00
Kamil Mysliwiec
129fe79f6b Update packages/microservices/server/server-rmq.ts
Co-authored-by: Rick Dutour Geerling <rick@trilon.io>
2024-11-18 15:26:15 +01:00
Kamil Mysliwiec
4f55f88bff Update packages/microservices/server/server-nats.ts
Co-authored-by: Rick Dutour Geerling <rick@trilon.io>
2024-11-18 15:26:07 +01:00
Kamil Mysliwiec
397390083f Merge pull request #13278 from johaven/feat-webdav-methods
feat(core,common,platform-fastify): add webdav http methods support
2024-11-18 13:16:35 +01:00
Kamil Myśliwiec
bc31b5cff5 test: update tcp config opts type 2024-11-18 13:14:59 +01:00
Kamil Mysliwiec
fb6025eb76 Merge pull request #13990 from Tony133/feat/upgrade-fastify-v5
feat(platform-fastify): added support for Fastify v5
2024-11-18 12:46:08 +01:00
Kamil Myśliwiec
3267135f2d chore: resolve merge conflicts 2024-11-18 12:45:34 +01:00
Kamil Mysliwiec
234d8d5600 Merge pull request #13924 from alinowrouzii/feat-microservices/nats-gracefull-shutdown
feat(microservices): add gracefull shutdown option for nats server
2024-11-18 12:33:48 +01:00
Kamil Mysliwiec
774ccbe1b2 Merge pull request #12622 from jmcdo29/feat/async-microservce-config
feat: allow for microservice options to come from the di container
2024-11-18 12:18:57 +01:00
Kamil Mysliwiec
ad97a73ab2 Merge branch '11.0.0' into feat-microservices/nats-gracefull-shutdown 2024-11-18 12:18:16 +01:00
Kamil Mysliwiec
00bb6309c1 Merge pull request #13407 from MegaSpaceHamlet/multer-error-fieldname
Multer error fieldname
2024-11-18 12:09:40 +01:00
Kamil Myśliwiec
c239f9a037 refactor: replace inline as any calls with untyped vars 2024-11-18 10:33:51 +01:00
Kamil Mysliwiec
a669981c81 Merge pull request #14121 from nestjs/feat/logger-additions
feat(common): json logger and a few other improvements
2024-11-18 10:09:57 +01:00
Kamil Myśliwiec
47941ba4d9 test: update outdated unit test 2024-11-18 09:56:13 +01:00
Kamil Mysliwiec
e89bb8e4a2 Merge pull request #14127 from CodyTseng/feat-ws-message-preprocessor
feat(ws): introduce message parser for ws adapter
2024-11-18 09:52:52 +01:00
Kamil Mysliwiec
ca86bf6374 Merge pull request #14129 from EeeasyCode/fix/issue-#13931
fix: rabbitmq bindings and auto-generated queues
2024-11-18 09:52:00 +01:00
Kamil Myśliwiec
09a193337a Merge branch 'ReneZeidler-fix-get-all-and-merge' into 11.0.0 2024-11-18 09:51:35 +01:00
Kamil Myśliwiec
33ef121d6b chore: a few improvements 2024-11-18 09:49:48 +01:00
Kamil Mysliwiec
d2ac397115 Merge pull request #13388 from AlexRMU/master
Сorrection of `Reflector` types
2024-11-18 09:14:09 +01:00
Kamil Myśliwiec
9129e4f858 refactor: remove redundant conditions 2024-11-18 09:10:34 +01:00
Kamil Myśliwiec
f37dafc132 chore: pass error instance to logger error method 2024-11-18 09:08:47 +01:00
Kamil Mysliwiec
3ad835c05d Merge pull request #14110 from wenlong-chen/fix-global-module-distance
fix(core): calculate module distance after bindGlobalScope
2024-11-18 08:18:34 +01:00
Kamil Mysliwiec
e965b4a141 Merge pull request #14111 from wenlong-chen/fix-module-destroy-order
fix(core): Order of module destroy should be the reverse of module init
2024-11-18 08:18:16 +01:00
Kamil Mysliwiec
feb2135373 Merge pull request #14123 from micalevisk/feat/narrow-nest-factories-methods
feat(core)!: type narrowing valid entry module for nestjs factories
2024-11-15 13:29:54 +01:00
Kamil Mysliwiec
9ea618e0a8 Update packages/core/nest-factory.ts 2024-11-15 13:29:35 +01:00
Kamil Myśliwiec
259b5dd828 fix: remove duplicated client redis message handler 2024-11-15 13:27:04 +01:00
Kamil Myśliwiec
66c3ab23ee sample: update grpc client options type 2024-11-15 12:48:22 +01:00
Kamil Myśliwiec
f2b6cc0e93 fix: fix minor type issues, broken imports 2024-11-15 11:50:09 +01:00
Kamil Myśliwiec
f983f03665 fix: update broken import paths 2024-11-15 10:19:26 +01:00
Kamil Mysliwiec
9a48dc123e Merge pull request #14131 from nestjs/feat/number-exception-create-body
feat(common): allow passing number to http error createBody
2024-11-15 10:18:14 +01:00
Kamil Mysliwiec
07adf37156 Merge pull request #14122 from nestjs/feat/parse-date-pipe
feat(common): add ParseDatePipe, add tsdoc to other pipes
2024-11-15 10:17:52 +01:00
Kamil Mysliwiec
ea92f5fb3a Merge pull request #14134 from nestjs/feat/add-max-tcp-message-length
feat(microservices): add max tcp packet buffer length
2024-11-15 10:17:28 +01:00
Kamil Myśliwiec
3dfc7fc68e feat(microservices): add status, unwrap, on, and other features 2024-11-15 10:03:05 +01:00
codytseng
33e32d9d5d feat(ws): allow setting message parser in constructor 2024-11-13 09:58:32 +08:00
codytseng
2ab99739cd refactor(ws): rename to message parser and move json parse logic inside 2024-11-13 09:57:29 +08:00
Kamil Myśliwiec
9ae6a4c19f feat(microservices): add max tcp packet buffer length 2024-11-12 13:37:14 +01:00
Kamil Myśliwiec
35846843e1 feat(common): allow passing number to http error create body #12787 2024-11-12 11:12:41 +01:00
Kamil Mysliwiec
945d134b66 Merge pull request #14126 from EeeasyCode/issue-#14093
fix(common): type narrowing context parameter on createParamDecorator's callback
2024-11-12 09:36:52 +01:00
Khan / 이창민
e231f9887e fix: rabbitmq bindings and auto-generated queues 2024-11-11 15:31:19 +09:00
Khan / 이창민
2874478b95 fix: import to use direct file path 2024-11-11 00:05:24 +09:00
codytseng
de13eec632 feat: introduce message preprocessor for ws adapter 2024-11-10 16:14:07 +08:00
Khan / 이창민
e9d57dd795 fix: type narrowing ctx 2024-11-10 16:31:29 +09:00
Micael Levi L. Cavalcante
3cdaaf0bed feat(core): type narrowing valid entry module for nestjs factories 2024-11-09 06:50:47 -04:00
Kamil Myśliwiec
676461ff4e test: add unit tests, minor updates 2024-11-08 15:49:24 +01:00
Kamil Myśliwiec
cd7079bcc0 feat(common): add parse date pipe, add tsdoc to other pipes 2024-11-08 15:28:13 +01:00
Kamil Myśliwiec
66c4810b0f style: fix linter issues 2024-11-08 14:29:42 +01:00
Kamil Myśliwiec
23e602b712 style: fix linter issues 2024-11-08 14:09:58 +01:00
Kamil Myśliwiec
e767bd364e feat: produce a parseable json when colors off and compact on 2024-11-08 13:49:42 +01:00
Kamil Mysliwiec
bcda8fd791 Merge pull request #14115 from Tony133/chore/update-fastify-view-options-interface
chore(fastify): update fastify view options interface
2024-11-08 12:26:59 +01:00
Kamil Mysliwiec
fb32ef6d46 Merge pull request #14116 from Tony133/chore/update-fastify-static-options-interface
refactor(fastify): update fastify static options interface
2024-11-08 12:26:29 +01:00
Kamil Mysliwiec
51a03bbcf2 Merge pull request #14113 from nestjs/feat/context-select-options
feat(core): allow overriding abortOnError for the select method
2024-11-08 11:44:11 +01:00
Kamil Mysliwiec
f191f0f1fd Merge pull request #14112 from nestjs/fix/instance-method-call
fix(microservices): use instance refs for target handler callbacks
2024-11-08 11:43:06 +01:00
Kamil Mysliwiec
84bf5706e2 Merge pull request #14097 from clkamp/wip-distance-order
fix(core): revisit dependencies w/ possibly higher distance
2024-11-08 11:42:49 +01:00
Kamil Myśliwiec
03d8bcc21c feat: add pid to log object 2024-11-08 11:32:08 +01:00
Kamil Myśliwiec
e75eb1d14a feat(common): json logger and a few other improvements 2024-11-08 11:21:37 +01:00
Tony133
790bafa46e refactor(): update fastify static options interface 2024-11-07 21:15:39 +01:00
Tony133
f13e32ad33 chore: update fastify view options interface 2024-11-07 21:12:38 +01:00
Antonio Tripodi
1ed97489e3 Merge branch 'master' into feat/upgrade-fastify-v5 2024-11-07 18:39:10 +01:00
Tony133
7de7320550 chore: upgrade dependencies for nest v11.x 2024-11-07 18:37:41 +01:00
Kamil Mysliwiec
3081f544f4 Merge pull request #14114 from micalevisk/fix-issue-13733
fix(common)!: drop broken support for promises on `exports` of modules
2024-11-07 14:10:35 +01:00
Micael Levi L. Cavalcante
5bc7f91d90 fix(common): drop broken support for promises on exports of modules 2024-11-07 08:49:14 -04:00
Kamil Mysliwiec
6ea78d52fb Merge pull request #13628 from tomflorentin/master
chore(class-transformer): plainToClass is deprectated and replaced with plainToInstance
2024-11-07 12:38:37 +01:00
Kamil Myśliwiec
38bfa9a13a feat(core): allow overriding abort on error for select method 2024-11-07 12:18:12 +01:00
Kamil Myśliwiec
f273041594 test: update unit tests 2024-11-07 12:07:59 +01:00
Kamil Myśliwiec
da8ebded25 fix(microservices): use instance ref to call handler #13473 2024-11-07 12:01:18 +01:00
Wenlong Chen
34c2ccabe8 fix(core): Order of module destroy should be the reverse of module init 2024-11-06 21:08:35 +08:00
Kamil Mysliwiec
0f38439aa6 Merge pull request #12761 from PieterScheffers/tcp-use-random-port
Be able to use a random port for the TCP server
2024-11-06 13:22:30 +01:00
Kamil Mysliwiec
5cdea22c39 Merge pull request #13468 from nestjs/revert-13467-revert-13283-fix/unnecessary-call
fix(microservices): delete unnecessary call of grpcClient.start
2024-11-06 12:54:35 +01:00
Kamil Mysliwiec
fd563c0ae9 Merge pull request #12735 from micalevisk/feat/narrow-inject
feat(common)!: type narrowing allowed injection tokens for `@Inject()`
2024-11-06 12:53:08 +01:00
Kamil Mysliwiec
9b690d95a2 Merge branch '11.0.0' into master 2024-11-06 12:51:14 +01:00
Kamil Mysliwiec
6228af1444 Merge pull request #13336 from nestjs/feat/module-opaque-keys
feat(core): introduce different module opaque key factories
2024-11-06 12:49:55 +01:00
Kamil Mysliwiec
8d0a3cd457 Merge pull request #12893 from Hareloo/12864-default-number-query-param-bug
fix(common): when transforming undefined numeric values
2024-11-06 12:49:48 +01:00
Kamil Mysliwiec
c63717db9d Merge pull request #12764 from kmw14641/kmw14641-bugfix/class-serializer-interceptor
fix(common): apply options to plaintoclass in classserializerinterceptor
2024-11-06 12:48:37 +01:00
Kamil Mysliwiec
9cb84e78f8 Update packages/microservices/server/server-tcp.ts
Co-authored-by: Gil Tichon <gil.tichon@lemonade.com>
2024-11-06 12:47:19 +01:00
Wenlong Chen
5ccc88fe8a fix(core): Calculate module distance after bindGlobalScope 2024-11-06 19:02:17 +08:00
Johaven
af4e1d0a33 Merge branch 'nestjs:master' into feat-webdav-methods 2024-11-02 00:22:06 +01:00
Christian Lütke Stetzkamp
2c8982a2c7 fix(core): revisit dependencies w/ possibly higher distance 2024-10-29 15:33:44 +01:00
Johaven
e1c1945a45 fix(platform-fastify): uppercase methods 2024-10-08 13:49:56 +02:00
Johaven
0d5ad26268 Merge branch 'nestjs:master' into feat-webdav-methods 2024-10-08 13:42:54 +02:00
Johaven
bc36e3c32a Merge branch 'nestjs:master' into feat-webdav-methods 2024-09-19 10:12:45 +02:00
Tony133
5f45310ab6 feat(platform-fastify): added compatibility for fastify version 5.x 2024-09-17 22:58:29 +02:00
Johaven
96bf1d9bb7 Merge branch 'nestjs:master' into feat-webdav-methods 2024-09-16 14:39:48 +02:00
Ali Nowrouzi
d20a1e580f feat(microservices): add gracefull shutdown option for nats server 2024-08-28 22:30:10 +03:30
Johaven
09cfc8279f Merge branch 'nestjs:master' into feat-webdav-methods 2024-08-15 13:23:01 +02:00
Johaven
b90e19f15f Merge branch 'nestjs:master' into feat-webdav-methods 2024-08-13 15:05:28 +02:00
johaven
b62b9255cf feat(core,common,platform-fastify): add webdav http methods support 2024-08-12 11:18:40 +02:00
Tom Florentin
3fd4220052 plain to class deprectated 2024-05-29 11:09:42 +02:00
Kamil Mysliwiec
7d5adfcc1f Revert "Revert "fix(microservice) Delete unnecessary call of grpcClient.start"" 2024-04-19 09:14:09 +02:00
MegaSpaceHamlet
ef12d355ff refactor(platform-express): modify error arg type
Add the possible `field` property to the `error` arg in
`transformExecption`.
2024-04-08 16:58:33 -04:00
Alex
52b2c72124 Improving types 2024-04-03 12:26:16 +05:00
Alex
1ccc7f9d91 Сorrection of Reflector types 2024-04-02 16:01:17 +05:00
MegaSpaceHamlet
50e1d705c3 feat(platform-express): multer errors show 'field'
Multer errors may include a 'field' property for certain errors. Pass it
along into the error message.
2024-03-30 22:43:51 -04:00
Kamil Myśliwiec
b3f85e9c04 test: update module compiler test 2024-03-19 10:52:31 +01:00
Kamil Myśliwiec
c4a905b996 chore: update comment to suggest switching to a different algorithm 2024-03-19 09:37:58 +01:00
Kamil Myśliwiec
094c35cf79 refactor: update types to better reflect dynamic metadata shape 2024-03-19 09:35:43 +01:00
Kamil Mysliwiec
30110d49e0 Update packages/core/injector/compiler.ts
Co-authored-by: Vinicius Lourenço <12551007+H4ad@users.noreply.github.com>
2024-03-19 09:33:04 +01:00
Kamil Myśliwiec
fb72a8ec9f feat(core): introduce different module opaque key factories 2024-03-18 10:12:56 +01:00
Hareloo
0d7a768b89 fix(common): when transforming undefined numeric values
Transforming numeric values in validationpipe is incorrect when value is undefined

closes #12864
2023-12-06 20:52:48 +02:00
kmw14641
ed8699769d fix(common): apply options to plaintoclass in classserializerinterceptor
fixed issue that with type option which is not already converted,
any other option cannot be applied to plainToClass

Closes #12763
2023-11-17 00:59:04 +09:00
Pieter Scheffers
0cbc15405d refactor: Use TCP_DEFAULT_PORT as default instead of using a boolean check 2023-11-16 11:26:44 +01:00
Micael Levi L. Cavalcante
c47be164f3 ci: switch to shallow cloning of 'wrk' repository 2023-11-14 15:46:35 -04:00
Micael Levi L. Cavalcante
61791e5115 feat(common): narrow allowed injection tokens for @Inject() 2023-11-14 15:46:32 -04:00
Jay McDoniel
2f2252264b feat: allow for microservice options to come from the di container
Microservices are now able to be created by getting their options
from within the DI container itself. This has been a long requested
feature of developers and I finally had some time to work through how
we could possibly let this happen.
2023-10-20 17:08:35 -07:00
René Zeidler
d04886d816 fix(core): fix type and return value of Reflector#getAllAndMerge
When only a single metadata value is present in the list of targets for
`Reflector#getAllAndMerge`, that value gets wrapped in an array if it is
not already an array or an object.
Previously, single values were returned as is, so the method could
actually return any value. Now the method always returns an array or an
object as indicated by its return type.
This also fixes the return type of both `getAll` and `getAllAndMerge`
when using the strongly typed `ReflectableDecorator<T>`.

Fixes #12231
2023-08-21 10:44:28 +02:00
156 changed files with 4834 additions and 1622 deletions

View File

@@ -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

1
.gitignore vendored
View File

@@ -50,4 +50,3 @@ build/config\.gypi
.npmrc
pnpm-lock.yaml
/.history

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View 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();
});
});

View File

@@ -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';

View File

@@ -27,14 +27,10 @@ describe('Durable providers', () => {
tenantId: number,
end: (err?: any) => void,
endpoint = '/durable',
opts: {
forceError: boolean;
} = { forceError: false },
) =>
request(server)
.get(endpoint)
.set({ ['x-tenant-id']: tenantId })
.set({ ['x-force-error']: opts.forceError ? 'true' : 'false' })
.end((err, res) => {
if (err) return end(err);
end(res);
@@ -88,23 +84,6 @@ describe('Durable providers', () => {
);
expect(result.body).deep.equal({ tenantId: '3' });
});
it(`should not cache durable providers that throw errors`, async () => {
let result: request.Response;
result = await new Promise<request.Response>(resolve =>
performHttpCall(10, resolve, '/durable/echo', { forceError: true }),
);
expect(result.statusCode).equal(412);
// The second request should be successful
result = await new Promise<request.Response>(resolve =>
performHttpCall(10, resolve, '/durable/echo'),
);
expect(result.body).deep.equal({ tenantId: '10' });
});
});
after(async () => {

View File

@@ -6,8 +6,6 @@ const tenants = new Map<string, ContextId>();
export class DurableContextIdStrategy implements ContextIdStrategy {
attach(contextId: ContextId, request: Request) {
const tenantId = request.headers['x-tenant-id'] as string;
const forceError = request.headers['x-force-error'] === 'true';
let tenantSubTreeId: ContextId;
if (tenants.has(tenantId)) {
@@ -16,18 +14,10 @@ export class DurableContextIdStrategy implements ContextIdStrategy {
tenantSubTreeId = { id: +tenantId } as ContextId;
tenants.set(tenantId, tenantSubTreeId);
}
const payload: {
tenantId: string;
forceError?: boolean;
} = { tenantId };
if (forceError) {
payload.forceError = true;
}
return {
resolve: (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId,
payload,
payload: { tenantId },
};
}
}

View File

@@ -1,23 +1,11 @@
import {
Inject,
Injectable,
PreconditionFailedException,
Scope,
} from '@nestjs/common';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
@Injectable({ scope: Scope.REQUEST, durable: true })
export class DurableService {
public instanceCounter = 0;
constructor(
@Inject(REQUEST)
public readonly requestPayload: { tenantId: string; forceError: boolean },
) {
if (requestPayload.forceError) {
throw new PreconditionFailedException('Forced error');
}
}
constructor(@Inject(REQUEST) public readonly requestPayload: unknown) {}
greeting() {
++this.instanceCounter;

View File

@@ -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();
});

View File

@@ -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;

View File

@@ -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)[]

View File

@@ -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);

View File

@@ -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,
}

View File

@@ -8,4 +8,11 @@ export enum RequestMethod {
OPTIONS,
HEAD,
SEARCH,
PROPFIND,
PROPPATCH,
MKCOL,
COPY,
MOVE,
LOCK,
UNLOCK,
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
export type HttpExceptionBodyMessage = string | string[];
export type HttpExceptionBodyMessage = string | string[] | number;
export interface HttpExceptionBody {
message: HttpExceptionBodyMessage;

View File

@@ -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;

View File

@@ -35,7 +35,6 @@ export interface ModuleMetadata {
*/
exports?: Array<
| DynamicModule
| Promise<DynamicModule>
| string
| symbol
| Provider

View File

@@ -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';
}

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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';

View 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;
}

View File

@@ -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;
}

View 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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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[]) {

View File

@@ -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', () => {

View File

@@ -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('/');
});
});

View 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)',
);
}
});
});
});
});

View File

@@ -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 () => {

View File

@@ -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;
}
}

View File

@@ -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[]) {

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -170,11 +170,6 @@ export class Injector {
inquirer,
);
} catch (err) {
wrapper.removeInstanceByContextId(
this.getContextId(contextId, wrapper),
inquirerId,
);
settlementSignal.error(err);
throw err;
}

View File

@@ -168,21 +168,6 @@ export class InstanceWrapper<T = any> {
collection.set(contextId, value);
}
public removeInstanceByContextId(contextId: ContextId, inquirerId?: string) {
if (this.scope === Scope.TRANSIENT && inquirerId) {
return this.removeInstanceByInquirerId(contextId, inquirerId);
}
this.values.delete(contextId);
}
public removeInstanceByInquirerId(contextId: ContextId, inquirerId: string) {
const collection = this.transientMap.get(inquirerId);
if (!collection) {
return;
}
collection.delete(contextId);
}
public addCtorMetadata(index: number, wrapper: InstanceWrapper) {
if (!this[INSTANCE_METADATA_SYMBOL].dependencies) {
this[INSTANCE_METADATA_SYMBOL].dependencies = [];

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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);
}
});
};

View File

@@ -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.
*

View File

@@ -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;
});
});
});

View File

@@ -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);
});
});

View File

@@ -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,
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -1,7 +1,6 @@
import { Scope } from '@nestjs/common';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { createContextId } from '../../helpers';
import { STATIC_CONTEXT } from '../../injector/constants';
import { InstanceWrapper } from '../../injector/instance-wrapper';
@@ -738,53 +737,6 @@ describe('InstanceWrapper', () => {
});
});
describe('removeInstanceByContextId', () => {
describe('without inquirer', () => {
it('should remove instance for given context', () => {
const wrapper = new InstanceWrapper({
scope: Scope.TRANSIENT,
});
const contextId = createContextId();
wrapper.setInstanceByContextId(contextId, { instance: {} });
const existingContext = wrapper.getInstanceByContextId(contextId);
expect(existingContext.instance).to.be.not.undefined;
wrapper.removeInstanceByContextId(contextId);
const removedContext = wrapper.getInstanceByContextId(contextId);
expect(removedContext.instance).to.be.undefined;
});
});
describe('when transient and inquirer has been passed', () => {
it('should remove instance for given context', () => {
const wrapper = new InstanceWrapper({
scope: Scope.TRANSIENT,
});
wrapper.setInstanceByContextId(
STATIC_CONTEXT,
{ instance: {} },
'inquirerId',
);
const existingContext = wrapper.getInstanceByContextId(
STATIC_CONTEXT,
'inquirerId',
);
expect(existingContext.instance).to.be.not.undefined;
wrapper.removeInstanceByContextId(STATIC_CONTEXT, 'inquirerId');
const removedContext = wrapper.getInstanceByContextId(
STATIC_CONTEXT,
'inquirerId',
);
expect(removedContext.instance).to.be.undefined;
});
});
});
describe('isInRequestScope', () => {
describe('when tree and context are not static and is not transient', () => {
it('should return true', () => {

View File

@@ -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;
});
});
});

View File

@@ -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);
});
});
});
});

View File

@@ -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('');

View File

@@ -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,
);
});

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
});
});
});

View File

@@ -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.');
}
}

View File

@@ -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');
}
}

View File

@@ -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());

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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) => {

View File

@@ -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(): (

View File

@@ -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),
},

View File

@@ -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,

View File

@@ -1,2 +0,0 @@
export const GRPC_CANCELLED = 'Cancelled';
export const RABBITMQ_REPLY_QUEUE = 'amq.rabbitmq.reply-to';

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -0,0 +1,8 @@
/**
* @publicApi
*/
export class MaxPacketLengthExceededException extends Error {
constructor(length: number) {
super(`The packet length (${length}) exceeds the maximum allowed length`);
}
}

View 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';

View File

@@ -0,0 +1,7 @@
export const enum KafkaStatus {
DISCONNECTED = 'disconnected',
CONNECTED = 'connected',
CRASHED = 'crashed',
STOPPED = 'stopped',
REBALANCING = 'rebalancing',
}

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View File

@@ -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 }));
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
/**
* @see https://github.com/nats-io/nats.js
*
* @publicApi
*/
export interface NatsCodec<T> {
encode(d: T): Uint8Array;
decode(a: Uint8Array): T;
}

View File

@@ -1,8 +1,11 @@
import { Buffer } from 'buffer';
import { StringDecoder } from 'string_decoder';
import { CorruptedPacketLengthException } from '../errors/corrupted-packet-length.exception';
import { MaxPacketLengthExceededException } from '../errors/max-packet-length-exceeded.exception';
import { TcpSocket } from './tcp-socket';
const MAX_BUFFER_SIZE = (512 * 1024 * 1024) / 4; // 512 MBs in characters with 4 bytes per character (32-bit)
export class JsonSocket extends TcpSocket {
private contentLength: number | null = null;
private buffer = '';
@@ -20,7 +23,12 @@ export class JsonSocket extends TcpSocket {
: dataRaw;
this.buffer += data;
if (this.contentLength == null) {
if (this.buffer.length > MAX_BUFFER_SIZE) {
this.buffer = '';
throw new MaxPacketLengthExceededException(this.buffer.length);
}
if (this.contentLength === null) {
const i = this.buffer.indexOf(this.delimiter);
/**
* Check if the buffer has the delimiter (#),

View File

@@ -1,14 +1,8 @@
import { Buffer } from 'buffer';
import { Socket } from 'net';
import {
CLOSE_EVENT,
CONNECT_EVENT,
DATA_EVENT,
ERROR_EVENT,
MESSAGE_EVENT,
} from '../constants';
import { NetSocketClosedException } from '../errors/net-socket-closed.exception';
import { InvalidJSONFormatException } from '../errors/invalid-json-format.exception';
import { NetSocketClosedException } from '../errors/net-socket-closed.exception';
import { TcpEventsMap } from '../events/tcp.events';
export abstract class TcpSocket {
private isClosed = false;
@@ -18,10 +12,10 @@ export abstract class TcpSocket {
}
constructor(public readonly socket: Socket) {
this.socket.on(DATA_EVENT, this.onData.bind(this));
this.socket.on(CONNECT_EVENT, () => (this.isClosed = false));
this.socket.on(CLOSE_EVENT, () => (this.isClosed = true));
this.socket.on(ERROR_EVENT, () => (this.isClosed = true));
this.socket.on('data', this.onData.bind(this));
this.socket.on(TcpEventsMap.CONNECT, () => (this.isClosed = false));
this.socket.on(TcpEventsMap.CLOSE, () => (this.isClosed = true));
this.socket.on(TcpEventsMap.ERROR, () => (this.isClosed = true));
}
public connect(port: number, host: string) {
@@ -52,18 +46,21 @@ export abstract class TcpSocket {
this.handleSend(message, callback);
}
protected abstract handleSend(message: any, callback?: (err?: any) => void);
protected abstract handleSend(
message: any,
callback?: (err?: any) => void,
): any;
private onData(data: Buffer) {
try {
this.handleData(data);
} catch (e) {
this.socket.emit(ERROR_EVENT, e.message);
this.socket.emit(TcpEventsMap.ERROR, e.message);
this.socket.end();
}
}
protected abstract handleData(data: Buffer | string);
protected abstract handleData(data: Buffer | string): any;
protected emitMessage(data: string) {
let message: Record<string, unknown>;
@@ -73,6 +70,6 @@ export abstract class TcpSocket {
throw new InvalidJSONFormatException(e, data);
}
message = message || {};
this.socket.emit(MESSAGE_EVENT, message);
this.socket.emit('message', message);
}
}

View File

@@ -10,6 +10,7 @@ export * from './client';
export * from './ctx-host';
export * from './decorators';
export * from './enums';
export * from './events';
export * from './exceptions';
export * from './helpers';
export * from './interfaces';

View File

@@ -2,6 +2,16 @@
* @publicApi
*/
export interface ClientGrpc {
/**
* Returns an instance of the given gRPC service.
* @param name Service name
* @returns gRPC service
*/
getService<T extends {}>(name: string): T;
/**
* Returns an instance of the given gRPC client.
* @param name Service name
* @returns gRPC client
*/
getClientByServiceName<T = any>(name: string): T;
}

Some files were not shown because too many files have changed in this diff Show More