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
317 changed files with 63158 additions and 60266 deletions

View File

@@ -121,7 +121,7 @@ jobs:
lint:
working_directory: ~/nest
docker:
- image: cimg/node:<< pipeline.parameters.current-node-version >>
- image: cimg/node:<< pipeline.parameters.maintenance-node-version >>
steps:
- checkout
- *restore-cache

View File

@@ -17,13 +17,13 @@ module.exports = {
sourceType: 'module',
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-wrapper-object-types': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-array-constructor': 'off',
},
},
{
@@ -34,13 +34,13 @@ module.exports = {
sourceType: 'module',
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-wrapper-object-types': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
},
}
]

1
.gitignore vendored
View File

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

View File

@@ -25,7 +25,7 @@ services:
- "9001:9001"
restart: always
mysql:
image: mysql:8.4.3
image: mysql:8.3.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
@@ -50,7 +50,7 @@ services:
zookeeper:
container_name: test-zookeeper
hostname: zookeeper
image: confluentinc/cp-zookeeper:7.7.1
image: confluentinc/cp-zookeeper:7.5.3
ports:
- "2181:2181"
environment:
@@ -59,7 +59,7 @@ services:
kafka:
container_name: test-kafka
hostname: kafka
image: confluentinc/cp-kafka:7.7.1
image: confluentinc/cp-kafka:7.5.3
depends_on:
- zookeeper
ports:

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

@@ -1,8 +1,8 @@
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { ExternalSvcService } from './external-svc.service';
import { CreateExternalSvcDto } from './dto/create-external-svc.dto';
import { UpdateExternalSvcDto } from './dto/update-external-svc.dto';
import { ExternalSvcService } from './external-svc.service';
@Controller()
export class ExternalSvcController {

View File

@@ -1,32 +0,0 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { TransientLazyModule } from '../src/transient.module';
import { LazyController } from '../src/lazy.controller';
import * as chai from 'chai';
import { expect } from 'chai';
import * as request from 'supertest';
describe('Lazy Transient providers', () => {
let app: INestApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [LazyController],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should not recreate dependencies for default scope', async () => {
const resultOne = await request(app.getHttpServer()).get('/lazy/transient');
const resultTwo = await request(app.getHttpServer()).get('/lazy/transient');
expect(resultOne.text).to.be.equal('Hi! Counter is 1');
expect(resultOne.statusCode).to.be.equal(200);
expect(resultTwo.text).to.be.equal('Hi! Counter is 2');
expect(resultTwo.statusCode).to.be.equal(200);
});
});

View File

@@ -3,13 +3,7 @@ import { GlobalService } from './global.module';
@Injectable()
export class EagerService {
private counter = 0;
constructor(public globalService: GlobalService) {}
sayHello() {
this.counter++;
return 'Hi! Counter is ' + this.counter;
}
}
@Module({

View File

@@ -1,18 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { LazyModuleLoader } from '@nestjs/core';
@Controller('lazy')
export class LazyController {
constructor(private lazyLoadModule: LazyModuleLoader) {}
@Get('transient')
async exec() {
const { TransientLazyModule } = await import('./transient.module');
const moduleRef = await this.lazyLoadModule.load(() => TransientLazyModule);
const { TransientService } = await import('./transient.service');
const _service = await moduleRef.resolve(TransientService);
return _service.eager();
}
}

View File

@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common';
import { GlobalService } from './global.module';
import { EagerService } from './eager.module';
import { TransientService } from './transient.service';
@Module({
imports: [],
providers: [TransientService, GlobalService, EagerService],
exports: [TransientService],
})
export class TransientLazyModule {}

View File

@@ -1,11 +0,0 @@
import { Injectable, Scope } from '@nestjs/common';
import { EagerService } from './eager.module';
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {
constructor(private eagerService: EagerService) {}
eager() {
return this.eagerService.sayHello();
}
}

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

@@ -24,9 +24,8 @@ export class DisconnectedClientController {
return throwError(() =>
code === 'ECONNREFUSED' ||
code === 'CONN_ERR' ||
code === 'ENOTFOUND' ||
code === 'CONNECTION_REFUSED' ||
error.message.includes('Connection is closed.')
error.message === 'Connection is closed.'
? new RequestTimeoutException('ECONNREFUSED')
: new InternalServerErrorException(),
);

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']: String(tenantId) })
.set({ ['x-force-error']: opts.forceError ? 'true' : 'false' })
.set({ ['x-tenant-id']: tenantId })
.end((err, res) => {
if (err) return end(err);
end(res);
@@ -88,42 +84,6 @@ describe('Durable providers', () => {
);
expect(result.body).deep.equal({ tenantId: '3' });
});
it(`should return the same tenantId both from durable request scoped service and non-durable request scoped service`, async () => {
let result: request.Response;
result = await new Promise<request.Response>(resolve =>
performHttpCall(1, resolve, '/durable/request-context'),
);
expect(result.body).deep.equal({
durableService: '1',
nonDurableService: '1',
});
result = await new Promise<request.Response>(resolve =>
performHttpCall(2, resolve, '/durable/request-context'),
);
expect(result.body).deep.equal({
durableService: '2',
nonDurableService: '2',
});
});
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

@@ -1,18 +1,11 @@
import { ContextId, ContextIdStrategy, HostComponentInfo } from '@nestjs/core';
import { Request } from 'express';
export type TenantContext = {
tenantId: string;
forceError?: boolean;
};
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)) {
@@ -21,15 +14,10 @@ export class DurableContextIdStrategy implements ContextIdStrategy {
tenantSubTreeId = { id: +tenantId } as ContextId;
tenants.set(tenantId, tenantSubTreeId);
}
const payload: TenantContext = { tenantId };
if (forceError) {
payload.forceError = true;
}
return {
resolve: (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId,
payload,
payload: { tenantId },
};
}
}

View File

@@ -1,13 +1,9 @@
import { Controller, Get } from '@nestjs/common';
import { DurableService } from './durable.service';
import { NonDurableService } from './non-durable.service';
@Controller('durable')
export class DurableController {
constructor(
private readonly durableService: DurableService,
private readonly nonDurableService: NonDurableService,
) {}
constructor(private readonly durableService: DurableService) {}
@Get()
greeting(): string {
@@ -16,16 +12,6 @@ export class DurableController {
@Get('echo')
echo() {
return {
tenantId: this.durableService.getTenantId(),
};
}
@Get('request-context')
getRequestContext() {
return {
durableService: this.durableService.getTenantId(),
nonDurableService: this.nonDurableService.getTenantId(),
};
return this.durableService.requestPayload;
}
}

View File

@@ -3,13 +3,11 @@ import { APP_GUARD } from '@nestjs/core';
import { DurableController } from './durable.controller';
import { DurableGuard } from './durable.guard';
import { DurableService } from './durable.service';
import { NonDurableService } from './non-durable.service';
@Module({
controllers: [DurableController],
providers: [
DurableService,
NonDurableService,
{
provide: APP_GUARD,
useClass: DurableGuard,

View File

@@ -1,30 +1,14 @@
import {
Inject,
Injectable,
PreconditionFailedException,
Scope,
} from '@nestjs/common';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { TenantContext } from './durable-context-id.strategy';
@Injectable({ scope: Scope.REQUEST, durable: true })
export class DurableService {
public instanceCounter = 0;
constructor(
@Inject(REQUEST) private readonly requestPayload: TenantContext,
) {
if (requestPayload.forceError) {
throw new PreconditionFailedException('Forced error');
}
}
constructor(@Inject(REQUEST) public readonly requestPayload: unknown) {}
greeting() {
++this.instanceCounter;
return `Hello world! Counter: ${this.instanceCounter}`;
}
getTenantId() {
return this.requestPayload.tenantId;
}
}

View File

@@ -1,14 +0,0 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { TenantContext } from './durable-context-id.strategy';
@Injectable()
export class NonDurableService {
constructor(
@Inject(REQUEST) private readonly requestPayload: TenantContext,
) {}
getTenantId() {
return this.requestPayload.tenantId;
}
}

View File

@@ -1,9 +1,7 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as EventSource from 'eventsource';
import { io } from 'socket.io-client';
import { AppController as LongConnectionController } from '../../nest-application/sse/src/app.controller';
import { ApplicationGateway } from '../src/app.gateway';
import { NamespaceGateway } from '../src/namespace.gateway';
import { ServerGateway } from '../src/server.gateway';
@@ -17,7 +15,7 @@ async function createNestApp(...gateways): Promise<INestApplication> {
}
describe('WebSocketGateway', () => {
let ws: ReturnType<typeof io>, app: INestApplication;
let ws, app;
it(`should handle message (2nd port)`, async () => {
app = await createNestApp(ApplicationGateway);
@@ -100,78 +98,5 @@ describe('WebSocketGateway', () => {
);
});
describe('shared server for WS and Long-Running connections', () => {
afterEach(() => {});
it('should block application shutdown', function (done) {
let eventSource;
(async () => {
this.timeout(30000);
setTimeout(() => {
const instance = testingModule.get(ServerGateway);
expect(instance.onApplicationShutdown.called).to.be.false;
eventSource.close();
done();
}, 25000);
const testingModule = await Test.createTestingModule({
providers: [ServerGateway],
controllers: [LongConnectionController],
}).compile();
app = testingModule.createNestApplication();
await app.listen(3000);
ws = io(`http://localhost:3000`);
eventSource = new EventSource(`http://localhost:3000/sse`);
await new Promise<void>((resolve, reject) => {
ws.on('connect', resolve);
ws.on('error', reject);
});
await new Promise((resolve, reject) => {
eventSource.onmessage = resolve;
eventSource.onerror = reject;
});
app.close();
})();
});
it('should shutdown application immediately when forceCloseConnections is true', async () => {
const testingModule = await Test.createTestingModule({
providers: [ServerGateway],
controllers: [LongConnectionController],
}).compile();
app = testingModule.createNestApplication({
forceCloseConnections: true,
});
await app.listen(3000);
ws = io(`http://localhost:3000`);
const eventSource = new EventSource(`http://localhost:3000/sse`);
await new Promise<void>((resolve, reject) => {
ws.on('connect', resolve);
ws.on('error', reject);
});
await new Promise((resolve, reject) => {
eventSource.onmessage = resolve;
eventSource.onerror = reject;
});
await app.close();
const instance = testingModule.get(ServerGateway);
expect(instance.onApplicationShutdown.called).to.be.true;
eventSource.close();
});
});
afterEach(() => app.close());
});

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

@@ -1,10 +1,9 @@
import { OnApplicationShutdown, UseInterceptors } from '@nestjs/common';
import { UseInterceptors } from '@nestjs/common';
import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';
import * as Sinon from 'sinon';
import { RequestInterceptor } from './request.interceptor';
@WebSocketGateway()
export class ServerGateway implements OnApplicationShutdown {
export class ServerGateway {
@SubscribeMessage('push')
onPush(client, data) {
return {
@@ -21,6 +20,4 @@ export class ServerGateway implements OnApplicationShutdown {
data: { ...data, path: client.pattern },
};
}
onApplicationShutdown = Sinon.spy();
}

View File

@@ -3,5 +3,5 @@
"packages": [
"packages/*"
],
"version": "10.4.11"
"version": "10.4.7"
}

52794
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -59,25 +59,26 @@
},
"dependencies": {
"@nuxtjs/opencollective": "0.3.2",
"ansis": "3.3.2",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"cli-color": "2.0.4",
"cors": "2.8.5",
"express": "4.21.1",
"fast-json-stringify": "6.0.0",
"fast-safe-stringify": "2.1.1",
"graphql-subscriptions": "2.0.0",
"iterare": "1.2.1",
"object-hash": "3.0.0",
"path-to-regexp": "3.2.0",
"reflect-metadata": "0.2.2",
"rxjs": "7.8.1",
"socket.io": "4.8.1",
"tslib": "2.8.1",
"socket.io": "4.8.0",
"tslib": "2.7.0",
"uid": "2.0.2",
"uuid": "11.0.3"
"uuid": "10.0.0"
},
"devDependencies": {
"@apollo/server": "4.11.2",
"@apollo/server": "4.11.0",
"@codechecks/client": "0.1.12",
"@commitlint/cli": "19.5.0",
"@commitlint/config-angular": "19.5.0",
@@ -87,11 +88,11 @@
"@fastify/multipart": "8.3.0",
"@fastify/static": "7.0.4",
"@fastify/view": "9.1.0",
"@grpc/grpc-js": "1.12.2",
"@grpc/grpc-js": "1.11.1",
"@grpc/proto-loader": "0.7.13",
"@nestjs/apollo": "12.2.1",
"@nestjs/graphql": "12.2.1",
"@nestjs/mongoose": "10.1.0",
"@nestjs/apollo": "12.2.0",
"@nestjs/graphql": "12.2.0",
"@nestjs/mongoose": "10.0.10",
"@nestjs/typeorm": "10.0.2",
"@types/amqplib": "0.10.5",
"@types/bytes": "3.1.4",
@@ -101,16 +102,16 @@
"@types/express": "4.17.21",
"@types/gulp": "4.0.17",
"@types/http-errors": "2.0.4",
"@types/mocha": "10.0.10",
"@types/node": "22.9.0",
"@types/mocha": "10.0.8",
"@types/node": "22.5.5",
"@types/sinon": "17.0.3",
"@types/supertest": "6.0.2",
"@types/ws": "8.5.13",
"@typescript-eslint/eslint-plugin": "8.15.0",
"@typescript-eslint/parser": "8.15.0",
"@types/supertest": "2.0.16",
"@types/ws": "8.5.12",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"amqp-connection-manager": "4.1.14",
"amqplib": "0.10.5",
"artillery": "2.0.21",
"amqplib": "0.10.4",
"artillery": "2.0.20",
"body-parser": "1.20.3",
"bytes": "3.1.2",
"cache-manager": "5.7.6",
@@ -118,58 +119,57 @@
"chai": "4.5.0",
"chai-as-promised": "7.1.2",
"clang-format": "1.8.0",
"concurrently": "9.1.0",
"concurrently": "9.0.1",
"conventional-changelog": "6.0.0",
"core-js": "3.39.0",
"core-js": "3.38.1",
"coveralls": "3.1.1",
"delete-empty": "3.0.0",
"engine.io-client": "6.6.2",
"engine.io-client": "6.6.1",
"eslint": "8.57.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.29.1",
"eventsource": "2.0.2",
"fancy-log": "2.0.0",
"fastify": "4.28.1",
"graphql": "16.9.0",
"graphql-tools": "9.0.3",
"graphql-subscriptions": "2.0.0",
"gulp": "5.0.0",
"graphql-tools": "9.0.1",
"gulp": "4.0.2",
"gulp-clang-format": "1.0.27",
"gulp-clean": "0.4.0",
"gulp-sourcemaps": "3.0.0",
"gulp-typescript": "5.0.1",
"gulp-watch": "5.0.1",
"http-errors": "2.0.0",
"husky": "9.1.7",
"husky": "9.1.5",
"imports-loader": "5.0.0",
"ioredis": "5.4.1",
"json-loader": "0.5.7",
"kafkajs": "2.2.4",
"lerna": "2.11.0",
"lerna-changelog": "2.2.0",
"light-my-request": "6.3.0",
"light-my-request": "6.1.0",
"lint-staged": "15.2.10",
"markdown-table": "2.0.0",
"mocha": "10.8.2",
"mongoose": "8.8.3",
"mqtt": "5.10.2",
"mocha": "10.7.3",
"mongoose": "8.6.2",
"mqtt": "5.6.0",
"multer": "1.4.4",
"mysql2": "3.11.4",
"mysql2": "3.11.3",
"nats": "2.28.2",
"nodemon": "3.1.7",
"nyc": "17.1.0",
"nodemon": "3.1.5",
"nyc": "14.1.1",
"prettier": "3.3.3",
"redis": "4.7.0",
"rxjs-compat": "6.6.7",
"sinon": "19.0.2",
"sinon-chai": "4.0.0",
"socket.io-client": "4.8.1",
"sinon-chai": "3.7.0",
"socket.io-client": "4.8.0",
"subscriptions-transport-ws": "0.11.0",
"supertest": "7.0.0",
"ts-morph": "24.0.0",
"ts-morph": "23.0.0",
"ts-node": "10.9.2",
"typeorm": "0.3.20",
"typescript": "5.6.3",
"typescript": "5.6.2",
"wrk": "1.2.1",
"ws": "8.18.0"
},

View File

@@ -94,7 +94,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td><a href="https://www.mercedes-benz.com/" target="_blank"><img src="https://nestjs.com/img/logos/mercedes-logo.png" width="100" valign="middle" /></a></td>
<td><a href="https://www.dinii.jp/" target="_blank"><img src="https://nestjs.com/img/logos/dinii-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-dark-logo.svg#2" width="150" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
</tr>

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,4 +1,3 @@
import { isUndefined } from '../../utils/shared.utils';
import {
InjectionToken,
Provider,
@@ -7,17 +6,15 @@ import {
} from '../../interfaces';
/**
* @param value
* @returns `true` if value is `OptionalFactoryDependency`
* check if x is OptionalFactoryDependency, based on prototype presence
* (to avoid classes with a static 'token' field)
* @param x
* @returns x is OptionalFactoryDependency
*/
function isOptionalFactoryDependency(
value: InjectionToken | OptionalFactoryDependency,
): value is OptionalFactoryDependency {
return (
!isUndefined((value as OptionalFactoryDependency).token) &&
!isUndefined((value as OptionalFactoryDependency).optional) &&
!(value as any).prototype
);
x: InjectionToken | OptionalFactoryDependency,
): x is OptionalFactoryDependency {
return !!((x as any)?.token && !(x as any)?.prototype);
}
const mapInjectToTokens = (t: InjectionToken | OptionalFactoryDependency) =>

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/common",
"version": "10.4.11",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
"author": "Kamil Mysliwiec",
"homepage": "https://nestjs.com",

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

@@ -116,13 +116,12 @@ export class ValidationPipe implements PipeTransform<any> {
: value;
}
const originalValue = value;
value = this.toEmptyIfNil(value, metatype);
value = this.toEmptyIfNil(value);
const isNil = value !== originalValue;
const isPrimitive = this.isPrimitive(value);
this.stripProtoKeys(value);
let entity = classTransformer.plainToClass(
let entity = classTransformer.plainToInstance(
metatype,
value,
this.transformOptions,
@@ -204,29 +203,19 @@ 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;
}
protected toEmptyIfNil<T = any, R = any>(
value: T,
metatype: Type<unknown> | object,
): R | {} {
if (!isNil(value)) {
return value;
}
if (
typeof metatype === 'function' ||
(metatype && 'prototype' in metatype && metatype.prototype?.constructor)
) {
return {};
}
// Builder like SWC require empty string to be returned instead of an empty object
// when the value is nil and the metatype is not a class instance, but a plain object (enum, for example).
// Otherwise, the error will be thrown.
// @see https://github.com/nestjs/nest/issues/12680
return '';
protected toEmptyIfNil<T = any, R = any>(value: T): R | {} {
return isNil(value) ? {} : value;
}
protected stripProtoKeys(value: any) {

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

@@ -5,30 +5,19 @@ import { getInjectionProviders } from '../../../module-utils/utils/get-injection
describe('getInjectionProviders', () => {
it('should take only required providers', () => {
class C {
static token = 'anything';
static token = 'a';
}
class G {
static token = 'anything';
static optional = true;
}
class H {
static token = 'anything';
static optional = false;
}
const providers: Provider[] = [
const p: Provider[] = [
{
//0
provide: 'a',
useValue: 'a',
},
{
//1
provide: 'b',
useValue: 'b',
},
C, //2
C,
{
//3
provide: 'd',
useFactory: (c, b) => [c, b],
inject: [
@@ -38,36 +27,23 @@ describe('getInjectionProviders', () => {
optional: true,
},
'x',
G,
H,
],
},
{
//4
provide: 'e',
useFactory: (d, b) => [d, b],
inject: ['d', 'b'],
},
{
//5
provide: 'f',
useValue: 'f',
},
G, //6
H, //7
];
const expected = [
providers[1],
providers[2],
providers[3],
providers[4],
providers[6],
providers[7],
];
const result = getInjectionProviders(providers, ['e']);
// should not include 'a' and 'f'
const expected = p.slice(1, -1);
const result = getInjectionProviders(p, ['e']);
expect(result).to.have.length(expected.length);
expect(result).to.have.members(expected);
expect(result).not.to.have.members([p[0], p[5]]);
});
});

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

@@ -94,7 +94,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td><a href="https://www.mercedes-benz.com/" target="_blank"><img src="https://nestjs.com/img/logos/mercedes-logo.png" width="100" valign="middle" /></a></td>
<td><a href="https://www.dinii.jp/" target="_blank"><img src="https://nestjs.com/img/logos/dinii-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-dark-logo.svg#2" width="150" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
</tr>

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

@@ -1,4 +1,3 @@
import { Observable, Subject } from 'rxjs';
import { AbstractHttpAdapter } from '../adapters/http-adapter';
/**
@@ -17,8 +16,6 @@ export class HttpAdapterHost<
T extends AbstractHttpAdapter = AbstractHttpAdapter,
> {
private _httpAdapter?: T;
private _listen$ = new Subject<void>();
private isListening = false;
/**
* Accessor for the underlying `HttpAdapter`
@@ -38,31 +35,4 @@ export class HttpAdapterHost<
get httpAdapter(): T {
return this._httpAdapter;
}
/**
* Observable that allows to subscribe to the `listen` event.
* This event is emitted when the HTTP application is listening for incoming requests.
*/
get listen$(): Observable<void> {
return this._listen$.asObservable();
}
/**
* Sets the listening state of the application.
*/
set listening(listening: boolean) {
this.isListening = listening;
if (listening) {
this._listen$.next();
this._listen$.complete();
}
}
/**
* Returns a boolean indicating whether the application is listening for incoming requests.
*/
get listening(): boolean {
return this.isListening;
}
}

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;
}
@@ -343,20 +338,11 @@ export class Injector {
wrapper: InstanceWrapper<T>,
): [InjectorDependency[], number[]] {
const optionalDependenciesIds = [];
/**
* Same as the internal utility function `isOptionalFactoryDependency` from `@nestjs/common`.
* We are duplicating it here because that one is not supposed to be exported.
*/
function isOptionalFactoryDependency(
value: InjectionToken | OptionalFactoryDependency,
): value is OptionalFactoryDependency {
return (
!isUndefined((value as OptionalFactoryDependency).token) &&
!isUndefined((value as OptionalFactoryDependency).optional) &&
!(value as any).prototype
);
}
const isOptionalFactoryDep = (
item: InjectionToken | OptionalFactoryDependency,
): item is OptionalFactoryDependency =>
!isUndefined((item as OptionalFactoryDependency).token) &&
!isUndefined((item as OptionalFactoryDependency).optional);
const mapFactoryProviderInjectArray = (
item: InjectionToken | OptionalFactoryDependency,
@@ -365,7 +351,7 @@ export class Injector {
if (typeof item !== 'object') {
return item;
}
if (isOptionalFactoryDependency(item)) {
if (isOptionalFactoryDep(item)) {
if (item.optional) {
optionalDependenciesIds.push(index);
}

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

@@ -12,7 +12,6 @@ import {
InjectionToken,
NestModule,
Provider,
Scope,
Type,
ValueProvider,
} from '@nestjs/common/interfaces';
@@ -254,11 +253,6 @@ export class Module {
return this.addCustomProvider(provider, this._providers, enhancerSubtype);
}
const isAlreadyDeclared = this._providers.has(provider);
if (this.isTransientProvider(provider) && isAlreadyDeclared) {
return provider;
}
this._providers.set(
provider,
new InstanceWrapper({
@@ -659,11 +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();
}
private isTransientProvider(provider: Type<any>): boolean {
return getClassScope(provider) === Scope.TRANSIENT;
}
}

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

@@ -346,9 +346,7 @@ export class MiddlewareModule<
configurable: false,
});
const requestProviderValue = isTreeDurable
? contextId.payload
: Object.assign(request, contextId.payload);
const requestProviderValue = isTreeDurable ? contextId.payload : request;
this.container.registerRequestProvider(requestProviderValue, contextId);
}
return contextId;

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,11 +51,10 @@ 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>;
private initializationPromise?: Promise<void>;
protected get instanceLinksHost() {
if (!this._instanceLinksHost) {
@@ -71,6 +71,7 @@ export class NestApplicationContext<
) {
super();
this.injector = new Injector();
this.moduleCompiler = container.getModuleCompiler();
if (this.appOptions.preview) {
this.printInPreviewModeWarning();
@@ -88,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;
@@ -96,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,
);
@@ -235,16 +252,8 @@ export class NestApplicationContext<
if (this.isInitialized) {
return this;
}
this.initializationPromise = new Promise(async (resolve, reject) => {
try {
await this.callInitHook();
await this.callBootstrapHook();
resolve();
} catch (err) {
reject(err);
}
});
await this.initializationPromise;
await this.callInitHook();
await this.callBootstrapHook();
this.isInitialized = true;
return this;
@@ -255,7 +264,6 @@ export class NestApplicationContext<
* @returns {Promise<void>}
*/
public async close(signal?: string): Promise<void> {
await this.initializationPromise;
await this.callDestroyHook();
await this.callBeforeShutdownHook(signal);
await this.dispose();
@@ -343,7 +351,6 @@ export class NestApplicationContext<
return;
}
receivedSignal = true;
await this.initializationPromise;
await this.callDestroyHook();
await this.callBeforeShutdownHook(signal);
await this.dispose();
@@ -395,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

@@ -294,12 +294,8 @@ export class NestApplication
public async listen(port: number | string, hostname: string): Promise<any>;
public async listen(port: number | string, ...args: any[]): Promise<any> {
this.assertNotInPreviewMode('listen');
!this.isInitialized && (await this.init());
if (!this.isInitialized) {
await this.init();
}
const httpAdapterHost = this.container.getHttpAdapterHostRef();
return new Promise((resolve, reject) => {
const errorHandler = (e: any) => {
this.logger.error(e?.toString?.());
@@ -327,8 +323,6 @@ export class NestApplication
if (address) {
this.httpServer.removeListener('error', errorHandler);
this.isListening = true;
httpAdapterHost.listening = true;
resolve(this.httpServer);
}
if (isCallbackInOriginalArgs) {

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

@@ -1,6 +1,6 @@
{
"name": "@nestjs/core",
"version": "10.4.11",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -36,7 +36,7 @@
"uid": "2.0.2"
},
"devDependencies": {
"@nestjs/common": "10.4.11"
"@nestjs/common": "10.4.7"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0",

View File

@@ -418,9 +418,7 @@ export class RouterExplorer {
configurable: false,
});
const requestProviderValue = isTreeDurable
? contextId.payload
: Object.assign(request, contextId.payload);
const requestProviderValue = isTreeDurable ? contextId.payload : request;
this.container.registerRequestProvider(requestProviderValue, contextId);
}
return contextId;

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

@@ -2,27 +2,11 @@ import { expect } from 'chai';
import { HttpAdapterHost } from '../../helpers/http-adapter-host';
describe('HttpAdapterHost', () => {
let applicationRefHost: HttpAdapterHost;
beforeEach(() => {
applicationRefHost = new HttpAdapterHost();
});
const applicationRefHost = new HttpAdapterHost();
it('should wrap application reference', () => {
const ref = {};
applicationRefHost.httpAdapter = ref as any;
expect(applicationRefHost.httpAdapter).to.be.eql(ref);
});
it('should emit listen event when listening is set to true', done => {
applicationRefHost.listen$.subscribe(() => {
expect(applicationRefHost.listening).to.be.true;
done();
});
applicationRefHost.listening = true;
});
it('listening should return false if the application isnt listening yet', () => {
expect(applicationRefHost.listening).to.be.false;
});
});

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

@@ -1,4 +1,4 @@
import { InjectionToken, Provider, Scope } from '@nestjs/common';
import { InjectionToken, Scope } from '@nestjs/common';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { ContextIdFactory } from '../helpers/context-id-factory';
@@ -7,7 +7,6 @@ import { Injector } from '../injector/injector';
import { InstanceLoader } from '../injector/instance-loader';
import { GraphInspector } from '../inspector/graph-inspector';
import { NestApplicationContext } from '../nest-application-context';
import { setTimeout } from 'timers/promises';
describe('NestApplicationContext', () => {
class A {}
@@ -15,7 +14,6 @@ describe('NestApplicationContext', () => {
async function testHelper(
injectionKey: InjectionToken,
scope: Scope,
additionalProviders: Array<Provider> = [],
): Promise<NestApplicationContext> {
const nestContainer = new NestContainer();
const injector = new Injector();
@@ -35,10 +33,6 @@ describe('NestApplicationContext', () => {
moduleRef.token,
);
for (const provider of additionalProviders) {
nestContainer.addProvider(provider, moduleRef.token);
}
nestContainer.addInjectable(
{
provide: injectionKey,
@@ -102,58 +96,6 @@ describe('NestApplicationContext', () => {
expect(processUp).to.be.false;
expect(promisesResolved).to.be.true;
});
it('should defer shutdown until all init hooks are resolved', async () => {
const clock = sinon.useFakeTimers({
toFake: ['setTimeout'],
});
const signal = 'SIGTERM';
const onModuleInitStub = sinon.stub();
const onApplicationShutdownStub = sinon.stub();
class B {
async onModuleInit() {
await setTimeout(5000);
onModuleInitStub();
}
async onApplicationShutdown() {
await setTimeout(1000);
onApplicationShutdownStub();
}
}
const applicationContext = await testHelper(A, Scope.DEFAULT, [
{ provide: B, useClass: B, scope: Scope.DEFAULT },
]);
applicationContext.enableShutdownHooks([signal]);
const ignoreProcessSignal = () => {
// noop to prevent process from exiting
};
process.on(signal, ignoreProcessSignal);
const deferredShutdown = async () => {
setTimeout(1);
process.kill(process.pid, signal);
};
Promise.all([applicationContext.init(), deferredShutdown()]);
await clock.nextAsync();
expect(onModuleInitStub.called).to.be.false;
expect(onApplicationShutdownStub.called).to.be.false;
await clock.nextAsync();
expect(onModuleInitStub.called).to.be.true;
expect(onApplicationShutdownStub.called).to.be.false;
await clock.nextAsync();
expect(onModuleInitStub.called).to.be.true;
expect(onApplicationShutdownStub.called).to.be.true;
clock.restore();
});
});
describe('get', () => {

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

@@ -94,7 +94,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td><a href="https://www.mercedes-benz.com/" target="_blank"><img src="https://nestjs.com/img/logos/mercedes-logo.png" width="100" valign="middle" /></a></td>
<td><a href="https://www.dinii.jp/" target="_blank"><img src="https://nestjs.com/img/logos/dinii-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://bloodycase.com/?promocode=NEST" target="_blank"><img src="https://nestjs.com/img/logos/bloodycase-logo.png" width="65" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-dark-logo.svg#2" width="150" valign="middle" /></a></td>
<td><a href="https://handsontable.com/docs/react-data-grid/?utm_source=NestJS_GH&utm_medium=sponsorship&utm_campaign=library_sponsorship_2024" target="_blank"><img src="https://nestjs.com/img/logos/handsontable-logo.svg" width="150" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.itflashcards.com/" target="_blank"><img src="https://nestjs.com/img/logos/it_flashcards-logo.png" width="170" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://arcjet.com/?ref=nestjs" target="_blank"><img src="https://nestjs.com/img/logos/arcjet-logo.svg" width="170" valign="middle" /></a></td>
</tr>

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