Merge pull request #11517 from nestjs/10.0.0

chore: laying the grounds for 10.0.0
This commit is contained in:
Kamil Mysliwiec
2023-06-12 12:25:49 +02:00
committed by GitHub
189 changed files with 23670 additions and 19521 deletions

View File

@@ -3,19 +3,19 @@ version: 2
aliases:
- &restore-cache
restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
key: dependency-cache-{{ checksum "package.json" }}
- &install-deps
run:
name: Install dependencies
command: npm ci
name: Install dependencies
command: npm ci --legacy-peer-deps
- &build-packages
run:
name: Build
command: npm run build
name: Build
command: npm run build
- &run-unit-tests
run:
name: Test
command: npm run test
name: Test
command: npm run test
unit-tests-template: &unit-tests-template
working_directory: ~/nest
@@ -30,7 +30,7 @@ jobs:
build:
working_directory: ~/nest
docker:
- image: circleci/node:12
- image: cimg/node:16.19
steps:
- checkout
- run:
@@ -40,7 +40,7 @@ jobs:
key: dependency-cache-{{ checksum "package.json" }}
- run:
name: Install dependencies
command: npm ci
command: npm ci --legacy-peer-deps
- save_cache:
key: dependency-cache-{{ checksum "package.json" }}
paths:
@@ -48,21 +48,11 @@ jobs:
- run:
name: Build
command: npm run build
test_node_12:
<<: *unit-tests-template
docker:
- image: circleci/node:12
test_node_14:
<<: *unit-tests-template
docker:
- image: circleci/node:14
test_node_16:
working_directory: ~/nest
docker:
- image: circleci/node:16
- image: cimg/node:16.19
steps:
- checkout
- *restore-cache
@@ -77,6 +67,16 @@ jobs:
- store_artifacts:
path: coverage
test_node_18:
<<: *unit-tests-template
docker:
- image: cimg/node:18.14
test_node_19:
<<: *unit-tests-template
docker:
- image: cimg/node:19.8
lint:
working_directory: ~/nest
docker:
@@ -97,17 +97,17 @@ jobs:
machine: true
steps:
- checkout
- run:
- run:
name: Prepare nvm
command: |
echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV
echo ' [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
- run:
- run:
name: Upgrade Node.js
command: |
nvm install v12
nvm install v16
node -v
nvm alias default v12
nvm alias default v16
- run:
name: Install Docker Compose
command: |
@@ -126,11 +126,11 @@ jobs:
- run:
name: Integration tests
command: npm run test:integration
codechecks_benchmarks:
working_directory: ~/nest
docker:
- image: circleci/node:16
- image: cimg/node:16.19
steps:
- checkout
- *restore-cache
@@ -146,7 +146,7 @@ jobs:
samples:
working_directory: ~/nest
docker:
- image: circleci/node:16
- image: cimg/node:16.19
environment:
- DISABLE_OPENCOLLECTIVE: true
steps:
@@ -165,10 +165,7 @@ workflows:
- test_node_16:
requires:
- build
- test_node_14:
requires:
- build
- test_node_12:
- test_node_18:
requires:
- build
- lint:

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -1,39 +0,0 @@
import { CACHE_MANAGER, INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AsyncRegisterExtraModule } from '../src/async-register-extra-providers/async-register-extra.module';
import { Cache } from 'cache-manager';
import { assert } from 'chai';
describe('Async Register Extra Providers', () => {
let server;
let app: INestApplication;
let cacheManager: Cache;
before(async () => {
const module = await Test.createTestingModule({
imports: [AsyncRegisterExtraModule],
}).compile();
cacheManager = module.get<Cache>(CACHE_MANAGER);
app = module.createNestApplication();
server = app.getHttpServer();
await app.init();
});
it('should be defined', async () => {
assert.isDefined(cacheManager);
});
it(`should return empty`, async () => {
return request(server).get('/').expect(200, 'Not found');
});
it(`should return data`, async () => {
return request(server).get('/').expect(200, 'value');
});
after(async () => {
await app.close();
});
});

View File

@@ -1,39 +0,0 @@
import { CACHE_MANAGER, INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { AsyncRegisterModule } from '../src/async-register/async-register.module';
import { Cache } from 'cache-manager';
import { assert } from 'chai';
describe('Async Register', () => {
let server;
let app: INestApplication;
let cacheManager: Cache;
before(async () => {
const module = await Test.createTestingModule({
imports: [AsyncRegisterModule],
}).compile();
cacheManager = module.get<Cache>(CACHE_MANAGER);
app = module.createNestApplication();
server = app.getHttpServer();
await app.init();
});
it('should be defined', async () => {
assert.isDefined(cacheManager);
});
it(`should return empty`, async () => {
return request(server).get('/').expect(200, 'Not found');
});
it(`should return data`, async () => {
return request(server).get('/').expect(200, 'value');
});
after(async () => {
await app.close();
});
});

View File

@@ -1,35 +0,0 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { CustomTtlModule } from '../src/custom-ttl/custom-ttl.module';
describe('Caching Custom TTL', () => {
let server;
let app: INestApplication;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [CustomTtlModule],
}).compile();
app = module.createNestApplication();
server = app.getHttpServer();
await app.init();
});
it('should return a differnt value after the TTL is elapsed', async () => {
await request(server).get('/').expect(200, '0');
await new Promise(resolve => setTimeout(resolve, 500));
await request(server).get('/').expect(200, '1');
});
it('should return the cached value within the TTL', async () => {
await request(server).get('/').expect(200, '0');
await new Promise(resolve => setTimeout(resolve, 200));
await request(server).get('/').expect(200, '0');
});
afterEach(async () => {
await app.close();
});
});

View File

@@ -1,31 +0,0 @@
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import * as request from 'supertest';
import { MultiStoreModule } from '../src/multi-store/multi-store.module';
describe('Caching Multi Store', () => {
let server;
let app: INestApplication;
before(async () => {
const module = await Test.createTestingModule({
imports: [MultiStoreModule],
}).compile();
app = module.createNestApplication();
server = app.getHttpServer();
await app.init();
});
it(`should return empty`, async () => {
return request(server).get('/').expect(200, '');
});
it(`should return data`, async () => {
return request(server).get('/').expect(200, 'multi-store-value');
});
after(async () => {
await app.close();
});
});

View File

@@ -1,16 +0,0 @@
import { Controller, Get, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Controller()
export class AsyncRegisterExtraController {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
@Get()
async getFromStore(): Promise<string> {
const value: string | undefined = await this.cacheManager.get('key');
if (!value) {
await this.cacheManager.set('key', 'value');
}
return value ?? 'Not found';
}
}

View File

@@ -1,16 +0,0 @@
import { CacheModule, Module } from '@nestjs/common';
import { AsyncRegisterExtraController } from './async-register-extra.controller';
import { CacheConfig } from './config/cache.config';
import { ConfigModule } from './config/config.module';
@Module({
imports: [
CacheModule.registerAsync({
extraProviders: [ConfigModule],
isGlobal: true,
useClass: CacheConfig,
}),
],
controllers: [AsyncRegisterExtraController],
})
export class AsyncRegisterExtraModule {}

View File

@@ -1,17 +0,0 @@
import {
CacheModuleOptions,
CacheOptionsFactory,
Injectable,
} from '@nestjs/common';
import { ConfigService } from './config.service';
@Injectable()
export class CacheConfig implements CacheOptionsFactory {
constructor(private readonly configService: ConfigService) {}
createCacheOptions(): CacheModuleOptions {
const ttl = this.configService.getTtl();
return { ttl };
}
}

View File

@@ -1,8 +0,0 @@
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}

View File

@@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class ConfigService {
public getTtl(): number {
return 10;
}
}

View File

@@ -1,40 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",
"paths": {
"@nestjs/common": ["../../packages/common"],
"@nestjs/common/*": ["../../packages/common/*"],
"@nestjs/core": ["../../packages/core"],
"@nestjs/core/*": ["../../packages/core/*"],
"@nestjs/microservices": ["../../packages/microservices"],
"@nestjs/microservices/*": ["../../packages/microservices/*"],
"@nestjs/websockets": ["../../packages/websockets"],
"@nestjs/websockets/*": ["../../packages/websockets/*"],
"@nestjs/testing": ["../../packages/testing"],
"@nestjs/testing/*": ["../../packages/testing/*"],
"@nestjs/platform-express": ["../../packages/platform-express"],
"@nestjs/platform-express/*": ["../../packages/platform-express/*"],
"@nestjs/platform-socket.io": ["../../packages/platform-socket.io"],
"@nestjs/platform-socket.io/*": ["../../packages/platform-socket.io/*"],
"@nestjs/platform-ws": ["../../packages/platform-ws"],
"@nestjs/platform-ws/*": ["../../packages/platform-ws/*"]
}
},
"include": [
"src/**/*",
"e2e/**/*"
],
"exclude": [
"node_modules",
]
}

View File

@@ -1,16 +0,0 @@
import { Controller, Get, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Controller()
export class AsyncRegisterController {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
@Get()
async getFromStore(): Promise<string> {
const value: string | undefined = await this.cacheManager.get('key');
if (!value) {
await this.cacheManager.set('key', 'value');
}
return value ?? 'Not found';
}
}

View File

@@ -1,14 +0,0 @@
import { CacheModule, Module } from '@nestjs/common';
import { AsyncRegisterController } from './async-register.controller';
import { CacheConfig } from './config/cache.config';
@Module({
imports: [
CacheModule.registerAsync({
isGlobal: true,
useClass: CacheConfig,
}),
],
controllers: [AsyncRegisterController],
})
export class AsyncRegisterModule {}

View File

@@ -1,14 +0,0 @@
import {
CacheModuleOptions,
CacheOptionsFactory,
Injectable,
} from '@nestjs/common';
@Injectable()
export class CacheConfig implements CacheOptionsFactory {
createCacheOptions(): CacheModuleOptions {
const ttl = 100;
return { ttl };
}
}

View File

@@ -1,40 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",
"paths": {
"@nestjs/common": ["../../packages/common"],
"@nestjs/common/*": ["../../packages/common/*"],
"@nestjs/core": ["../../packages/core"],
"@nestjs/core/*": ["../../packages/core/*"],
"@nestjs/microservices": ["../../packages/microservices"],
"@nestjs/microservices/*": ["../../packages/microservices/*"],
"@nestjs/websockets": ["../../packages/websockets"],
"@nestjs/websockets/*": ["../../packages/websockets/*"],
"@nestjs/testing": ["../../packages/testing"],
"@nestjs/testing/*": ["../../packages/testing/*"],
"@nestjs/platform-express": ["../../packages/platform-express"],
"@nestjs/platform-express/*": ["../../packages/platform-express/*"],
"@nestjs/platform-socket.io": ["../../packages/platform-socket.io"],
"@nestjs/platform-socket.io/*": ["../../packages/platform-socket.io/*"],
"@nestjs/platform-ws": ["../../packages/platform-ws"],
"@nestjs/platform-ws/*": ["../../packages/platform-ws/*"]
}
},
"include": [
"src/**/*",
"e2e/**/*"
],
"exclude": [
"node_modules",
]
}

View File

@@ -1,20 +0,0 @@
import {
CacheInterceptor,
CacheTTL,
Controller,
Get,
UseInterceptors,
} from '@nestjs/common';
@Controller()
export class CustomTtlController {
counter = 0;
constructor() {}
@Get()
@CacheTTL(500)
@UseInterceptors(CacheInterceptor)
getNumber() {
return this.counter++;
}
}

View File

@@ -1,8 +0,0 @@
import { CacheModule, Module } from '@nestjs/common';
import { CustomTtlController } from './custom-ttl.controller';
@Module({
imports: [CacheModule.register()],
controllers: [CustomTtlController],
})
export class CustomTtlModule {}

View File

@@ -1,40 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",
"paths": {
"@nestjs/common": ["../../packages/common"],
"@nestjs/common/*": ["../../packages/common/*"],
"@nestjs/core": ["../../packages/core"],
"@nestjs/core/*": ["../../packages/core/*"],
"@nestjs/microservices": ["../../packages/microservices"],
"@nestjs/microservices/*": ["../../packages/microservices/*"],
"@nestjs/websockets": ["../../packages/websockets"],
"@nestjs/websockets/*": ["../../packages/websockets/*"],
"@nestjs/testing": ["../../packages/testing"],
"@nestjs/testing/*": ["../../packages/testing/*"],
"@nestjs/platform-express": ["../../packages/platform-express"],
"@nestjs/platform-express/*": ["../../packages/platform-express/*"],
"@nestjs/platform-socket.io": ["../../packages/platform-socket.io"],
"@nestjs/platform-socket.io/*": ["../../packages/platform-socket.io/*"],
"@nestjs/platform-ws": ["../../packages/platform-ws"],
"@nestjs/platform-ws/*": ["../../packages/platform-ws/*"]
}
},
"include": [
"src/**/*",
"e2e/**/*"
],
"exclude": [
"node_modules",
]
}

View File

@@ -1,16 +0,0 @@
import { Controller, Get, Inject, CACHE_MANAGER } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Controller()
export class MultiStoreController {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
@Get()
async getFromMultiStore(): Promise<unknown> {
const value = await this.cacheManager.get('multi-store-key');
if (!value) {
await this.cacheManager.set('multi-store-key', 'multi-store-value');
}
return value;
}
}

View File

@@ -1,24 +0,0 @@
import { CacheModule, Module } from '@nestjs/common';
import { redisStore } from 'cache-manager-redis-store';
import { MultiStoreController } from './multi-store.controller';
@Module({
imports: [
CacheModule.register([
{
store: 'memory',
max: 100,
ttl: 10,
},
{
store: redisStore,
host: 'localhost',
port: 6379,
db: 0,
ttl: 600,
},
]),
],
controllers: [MultiStoreController],
})
export class MultiStoreModule {}

View File

@@ -1,40 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",
"paths": {
"@nestjs/common": ["../../packages/common"],
"@nestjs/common/*": ["../../packages/common/*"],
"@nestjs/core": ["../../packages/core"],
"@nestjs/core/*": ["../../packages/core/*"],
"@nestjs/microservices": ["../../packages/microservices"],
"@nestjs/microservices/*": ["../../packages/microservices/*"],
"@nestjs/websockets": ["../../packages/websockets"],
"@nestjs/websockets/*": ["../../packages/websockets/*"],
"@nestjs/testing": ["../../packages/testing"],
"@nestjs/testing/*": ["../../packages/testing/*"],
"@nestjs/platform-express": ["../../packages/platform-express"],
"@nestjs/platform-express/*": ["../../packages/platform-express/*"],
"@nestjs/platform-socket.io": ["../../packages/platform-socket.io"],
"@nestjs/platform-socket.io/*": ["../../packages/platform-socket.io/*"],
"@nestjs/platform-ws": ["../../packages/platform-ws"],
"@nestjs/platform-ws/*": ["../../packages/platform-ws/*"]
}
},
"include": [
"src/**/*",
"e2e/**/*"
],
"exclude": [
"node_modules",
]
}

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -1,15 +1,16 @@
import { HttpServer, HttpStatus, INestApplication } from '@nestjs/common';
import { HttpStatus, INestApplication } from '@nestjs/common';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { RawServerDefault } from 'fastify';
import * as request from 'supertest';
import { ErrorsController } from '../src/errors/errors.controller';
describe('Error messages', () => {
let server: HttpServer;
let server: RawServerDefault;
describe('Express', () => {
let app: INestApplication;

View File

@@ -3,7 +3,10 @@ import {
Get,
MiddlewareConsumer,
Module,
NestMiddleware,
NestModule,
Query,
Req,
RequestMethod,
} from '@nestjs/common';
import {
@@ -14,178 +17,385 @@ import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { AppModule } from '../src/app.module';
const INCLUDED_VALUE = 'test_included';
const QUERY_VALUE = 'test_query';
const REQ_URL_VALUE = 'test_req_url';
const RETURN_VALUE = 'test';
const SCOPED_VALUE = 'test_scoped';
const WILDCARD_VALUE = 'test_wildcard';
@Controller()
class TestController {
@Get('express_style_wildcard/wildcard_nested')
express_style_wildcard() {
return RETURN_VALUE;
}
@Get('test')
test() {
return RETURN_VALUE;
}
@Get('query')
query() {
return RETURN_VALUE;
}
@Get('tests/wildcard_nested')
wildcard_nested() {
return RETURN_VALUE;
}
@Get('tests/included')
included() {
return RETURN_VALUE;
}
}
@Controller(QUERY_VALUE)
class TestQueryController {
@Get()
[QUERY_VALUE](@Query('test') test: string) {
return test;
}
}
@Module({
imports: [AppModule],
controllers: [TestController, TestQueryController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.end(INCLUDED_VALUE))
.forRoutes({ path: 'tests/included', method: RequestMethod.POST })
.apply((req, res, next) => res.end(`${REQ_URL_VALUE}${req.url}`))
.forRoutes('req/url/')
.apply((req, res, next) => res.end(WILDCARD_VALUE))
.forRoutes('express_style_wildcard/*', 'tests/(.*)')
.apply((req, res, next) => res.end(QUERY_VALUE))
.forRoutes('query')
.apply((req, res, next) => next())
.forRoutes(TestQueryController)
.apply((req, res, next) => res.end(SCOPED_VALUE))
.forRoutes(TestController)
.apply((req, res, next) => res.end(RETURN_VALUE))
.exclude({ path: QUERY_VALUE, method: -1 as any })
.forRoutes('(.*)');
}
}
describe('Middleware (FastifyAdapter)', () => {
let app: NestFastifyApplication;
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(new FastifyAdapter());
describe('should return expected values depending on the route', () => {
const INCLUDED_VALUE = 'test_included';
const QUERY_VALUE = 'test_query';
const REQ_URL_VALUE = 'test_req_url';
const RETURN_VALUE = 'test';
const SCOPED_VALUE = 'test_scoped';
const WILDCARD_VALUE = 'test_wildcard';
await app.init();
@Controller()
class TestController {
@Get('express_style_wildcard/wildcard_nested')
express_style_wildcard() {
return RETURN_VALUE;
}
@Get('test')
test() {
return RETURN_VALUE;
}
@Get('query')
query() {
return RETURN_VALUE;
}
@Get('tests/wildcard_nested')
wildcard_nested() {
return RETURN_VALUE;
}
@Get('tests/included')
included() {
return RETURN_VALUE;
}
}
@Controller(QUERY_VALUE)
class TestQueryController {
@Get()
[QUERY_VALUE](@Query('test') test: string) {
return test;
}
}
@Module({
imports: [AppModule],
controllers: [TestController, TestQueryController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.end(INCLUDED_VALUE))
.forRoutes({ path: 'tests/included', method: RequestMethod.POST })
.apply((req, res, next) => res.end(REQ_URL_VALUE))
.forRoutes('req/url/(.*)')
.apply((req, res, next) => res.end(WILDCARD_VALUE))
.forRoutes('express_style_wildcard/*', 'tests/(.*)')
.apply((req, res, next) => res.end(QUERY_VALUE))
.forRoutes('query')
.apply((req, res, next) => next())
.forRoutes(TestQueryController)
.apply((req, res, next) => res.end(SCOPED_VALUE))
.forRoutes(TestController)
.apply((req, res, next) => res.end(RETURN_VALUE))
.exclude({ path: QUERY_VALUE, method: -1 as any })
.forRoutes('(.*)');
}
}
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(new FastifyAdapter());
await app.init();
});
it(`forRoutes((.*))`, () => {
return app
.inject({
method: 'GET',
url: '/hello',
})
.then(({ payload }) => expect(payload).to.be.eql(RETURN_VALUE));
});
it(`forRoutes(TestController)`, () => {
return app
.inject({
method: 'GET',
url: '/test',
})
.then(({ payload }) => expect(payload).to.be.eql(SCOPED_VALUE));
});
it(`query?test=${QUERY_VALUE} forRoutes(query)`, () => {
return app
.inject({
method: 'GET',
url: '/query',
query: {
test: QUERY_VALUE,
},
})
.then(({ payload }) => expect(payload).to.be.eql(QUERY_VALUE));
});
it(`${QUERY_VALUE}?test=${QUERY_VALUE} forRoutes(${QUERY_VALUE})`, () => {
return app
.inject({
method: 'GET',
url: QUERY_VALUE,
query: {
test: QUERY_VALUE,
},
})
.then(({ payload }) => expect(payload).to.be.eql(QUERY_VALUE));
});
it(`forRoutes(tests/(.*))`, () => {
return app
.inject({
method: 'GET',
url: '/tests/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
it(`forRoutes(express_style_wildcard/*)`, () => {
return app
.inject({
method: 'GET',
url: '/express_style_wildcard/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
it(`forRoutes(req/url/)`, () => {
const reqUrl = '/test';
return app
.inject({
method: 'GET',
url: `/req/url${reqUrl}`,
})
.then(({ payload }) => expect(payload).to.be.eql(REQ_URL_VALUE));
});
it(`GET forRoutes(POST tests/included)`, () => {
return app
.inject({
method: 'GET',
url: '/tests/included',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
it(`POST forRoutes(POST tests/included)`, () => {
return app
.inject({
method: 'POST',
url: '/tests/included',
})
.then(({ payload }) => expect(payload).to.be.eql(INCLUDED_VALUE));
});
afterEach(async () => {
await app.close();
});
});
it(`forRoutes((.*))`, () => {
return app
.inject({
method: 'GET',
url: '/hello',
})
.then(({ payload }) => expect(payload).to.be.eql(RETURN_VALUE));
});
describe.only('should execute middleware only once for given routes', () => {
class Middleware implements NestMiddleware {
use(request: any, reply: any, next: () => void) {
if (request.middlewareExecutionCount === undefined) {
request.middlewareExecutionCount = 1;
} else {
request.middlewareExecutionCount++;
}
next();
}
}
it(`forRoutes(TestController)`, () => {
return app
.inject({
method: 'GET',
url: '/test',
})
.then(({ payload }) => expect(payload).to.be.eql(SCOPED_VALUE));
});
@Controller()
class AbcController {
@Get('/a')
async a(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
it(`query?test=${QUERY_VALUE} forRoutes(query)`, () => {
return app
.inject({
method: 'GET',
url: '/query',
query: {
test: QUERY_VALUE,
},
})
.then(({ payload }) => expect(payload).to.be.eql(QUERY_VALUE));
});
@Get('/a/b')
async ab(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
it(`${QUERY_VALUE}?test=${QUERY_VALUE} forRoutes(${QUERY_VALUE})`, () => {
return app
.inject({
method: 'GET',
url: QUERY_VALUE,
query: {
test: QUERY_VALUE,
},
})
.then(({ payload }) => expect(payload).to.be.eql(QUERY_VALUE));
});
@Get('/a/b/c')
async abc(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
it(`forRoutes(tests/(.*))`, () => {
return app
.inject({
method: 'GET',
url: '/tests/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
@Get('/similar')
async withSimilar(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
it(`forRoutes(express_style_wildcard/*)`, () => {
return app
.inject({
method: 'GET',
url: '/express_style_wildcard/wildcard_nested',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
@Get('/similar/test')
async withSimilarTest(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
it(`forRoutes(req/url/)`, () => {
const reqUrl = '/test';
return app
.inject({
method: 'GET',
url: `/req/url${reqUrl}`,
})
.then(({ payload }) =>
expect(payload).to.be.eql(`${REQ_URL_VALUE}${reqUrl}`),
);
});
@Get('/similar/:id')
async withSimilarId(@Req() request: any) {
return this.validateExecutionCount({
request,
expected: 1,
});
}
it(`GET forRoutes(POST tests/included)`, () => {
return app
.inject({
method: 'GET',
url: '/tests/included',
})
.then(({ payload }) => expect(payload).to.be.eql(WILDCARD_VALUE));
});
private validateExecutionCount({
request,
expected,
}: {
request: any;
expected: number;
}) {
let actual: number | undefined;
actual = request.raw.middlewareExecutionCount;
actual ??= 0;
it(`POST forRoutes(POST tests/included)`, () => {
return app
.inject({
method: 'POST',
url: '/tests/included',
})
.then(({ payload }) => expect(payload).to.be.eql(INCLUDED_VALUE));
});
return {
success: actual === expected,
actual,
expected,
};
}
}
afterEach(async () => {
await app.close();
@Module({
controllers: [AbcController],
})
class TestModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(Middleware).forRoutes(AbcController);
}
}
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(new FastifyAdapter());
await app.init();
});
it(`GET forRoutes(/a/b/c)`, () => {
return app
.inject({
method: 'GET',
url: '/a/b/c',
})
.then(({ payload }) => {
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
);
});
});
it(`GET forRoutes(/a/b)`, () => {
return app
.inject({
method: 'GET',
url: '/a/b',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
it(`GET forRoutes(/a)`, () => {
return app
.inject({
method: 'GET',
url: '/a',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
it(`GET forRoutes(/similar)`, () => {
return app
.inject({
method: 'GET',
url: '/similar',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
it(`GET forRoutes(/similar/test)`, () => {
return app
.inject({
method: 'GET',
url: '/similar/test',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
it(`GET forRoutes(/similar/arbitrary)`, () => {
return app
.inject({
method: 'GET',
url: '/similar/arbitrary',
})
.then(({ payload }) =>
expect(payload).to.be.eql(
JSON.stringify({
success: true,
actual: 1,
expected: 1,
}),
),
);
});
afterEach(async () => {
await app.close();
});
});
});

View File

@@ -0,0 +1,103 @@
import {
Controller,
Get,
INestApplication,
Injectable,
MiddlewareConsumer,
NestMiddleware,
Module,
} from '@nestjs/common';
import { Test } from '../../../packages/testing';
import * as request from 'supertest';
import { expect } from 'chai';
/**
* Number of times that the middleware was executed.
*/
let triggerCounter = 0;
@Injectable()
class Middleware implements NestMiddleware {
use(req, res, next) {
triggerCounter++;
next();
}
}
@Controller()
class TestController {
@Get('/test')
testA() {}
@Get('/:id')
testB() {}
@Get('/static/route')
testC() {}
@Get('/:id/:nested')
testD() {}
}
@Module({
controllers: [TestController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(Middleware).forRoutes(TestController);
}
}
describe('Middleware (run on route match)', () => {
let app: INestApplication;
beforeEach(async () => {
triggerCounter = 0;
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication();
await app.init();
});
it(`forRoutes(TestController) should execute middleware once when request url is equal match`, () => {
return request(app.getHttpServer())
.get('/test')
.expect(200)
.then(() => {
expect(triggerCounter).to.be.eq(1);
});
});
it(`forRoutes(TestController) should execute middleware once when request url is not equal match`, () => {
return request(app.getHttpServer())
.get('/1')
.expect(200)
.then(() => {
expect(triggerCounter).to.be.eq(1);
});
});
it(`forRoutes(TestController) should execute middleware once when request url is not of nested params`, () => {
return request(app.getHttpServer())
.get('/static/route')
.expect(200)
.then(() => {
expect(triggerCounter).to.be.eq(1);
});
});
it(`forRoutes(TestController) should execute middleware once when request url is of nested params`, () => {
return request(app.getHttpServer())
.get('/1/abc')
.expect(200)
.then(() => {
expect(triggerCounter).to.be.eq(1);
});
});
afterEach(async () => {
await app.close();
});
});

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -822,23 +822,6 @@
"token": "HttpAdapterHost"
}
},
"-553129559": {
"id": "-553129559",
"label": "HttpAdapterHost",
"parent": "555176277",
"metadata": {
"type": "provider",
"internal": true,
"sourceModuleName": "InternalCoreModule",
"durable": false,
"static": true,
"scope": 0,
"transient": false,
"exported": true,
"token": "HttpAdapterHost",
"initTime": 0
}
},
"-702581189": {
"id": "-702581189",
"label": "LazyModuleLoader",
@@ -2214,23 +2197,6 @@
},
"id": "1976848738"
},
"-1759625292": {
"source": "-553129559",
"target": "-326832201",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "InternalCoreModule",
"sourceClassName": "HttpAdapterHost",
"targetClassName": "HttpAdapterHost",
"sourceClassToken": "HttpAdapterHost",
"targetClassToken": "HttpAdapterHost",
"targetModuleName": "InternalCoreModule",
"keyOrIndex": 0,
"injectionType": "constructor",
"internal": true
},
"id": "-1759625292"
},
"-2105726668": {
"source": "-1803759743",
"target": "1010833816",
@@ -2756,7 +2722,8 @@
"patterns": [
"createExternalSvc"
],
"isEventHandler": false
"isEventHandler": false,
"extras": {}
},
"id": "-349899408_create"
},
@@ -2771,7 +2738,8 @@
"patterns": [
"findAllExternalSvc"
],
"isEventHandler": false
"isEventHandler": false,
"extras": {}
},
"id": "-349899408_findAll"
},
@@ -2786,7 +2754,8 @@
"patterns": [
"findOneExternalSvc"
],
"isEventHandler": false
"isEventHandler": false,
"extras": {}
},
"id": "-349899408_findOne"
},
@@ -2801,7 +2770,8 @@
"patterns": [
"updateExternalSvc"
],
"isEventHandler": false
"isEventHandler": false,
"extras": {}
},
"id": "-349899408_update"
},
@@ -2816,7 +2786,8 @@
"patterns": [
"removeExternalSvc"
],
"isEventHandler": false
"isEventHandler": false,
"extras": {}
},
"id": "-349899408_remove"
}

View File

@@ -806,23 +806,6 @@
"token": "HttpAdapterHost"
}
},
"-553129559": {
"id": "-553129559",
"label": "HttpAdapterHost",
"parent": "555176277",
"metadata": {
"type": "provider",
"internal": true,
"sourceModuleName": "InternalCoreModule",
"durable": false,
"static": true,
"scope": 0,
"transient": false,
"exported": true,
"token": "HttpAdapterHost",
"initTime": 0
}
},
"-702581189": {
"id": "-702581189",
"label": "LazyModuleLoader",
@@ -2198,23 +2181,6 @@
},
"id": "1976848738"
},
"-1759625292": {
"source": "-553129559",
"target": "-326832201",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "InternalCoreModule",
"sourceClassName": "HttpAdapterHost",
"targetClassName": "HttpAdapterHost",
"sourceClassToken": "HttpAdapterHost",
"targetClassToken": "HttpAdapterHost",
"targetModuleName": "InternalCoreModule",
"keyOrIndex": 0,
"injectionType": "constructor",
"internal": true
},
"id": "-1759625292"
},
"-2105726668": {
"source": "-1803759743",
"target": "1010833816",

View File

@@ -4,7 +4,7 @@ import { SerializedGraph } from '@nestjs/core/inspector/serialized-graph';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { Test, TestingModule } from '@nestjs/testing';
import { expect } from 'chai';
import { readFileSync, writeFileSync } from 'fs';
import { readFileSync } from 'fs';
import { join } from 'path';
import * as sinon from 'sinon';
import { AppModule } from '../src/app.module';
@@ -26,10 +26,10 @@ describe('Graph inspector', () => {
const graph = testingModule.get(SerializedGraph);
// Update snapshot:
writeFileSync(
join(__dirname, 'fixtures', 'pre-init-graph.json'),
graph.toString(),
);
// writeFileSync(
// join(__dirname, 'fixtures', 'pre-init-graph.json'),
// graph.toString(),
// );
const snapshot = readFileSync(
join(__dirname, 'fixtures', 'pre-init-graph.json'),
@@ -54,10 +54,10 @@ describe('Graph inspector', () => {
const graph = testingModule.get(SerializedGraph);
// Update snapshot:
writeFileSync(
join(__dirname, 'fixtures', 'post-init-graph.json'),
graph.toString(),
);
// writeFileSync(
// join(__dirname, 'fixtures', 'post-init-graph.json'),
// graph.toString(),
// );
const snapshot = readFileSync(
join(__dirname, 'fixtures', 'post-init-graph.json'),

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"lib": ["dom"],

View File

@@ -8,7 +8,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist"

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -0,0 +1,12 @@
import { Injectable, Module, forwardRef } from '@nestjs/common';
import { BModule, BProvider } from './b.module';
@Injectable()
export class AProvider {}
@Module({
imports: [forwardRef(() => BModule)],
providers: [AProvider],
exports: [AProvider],
})
export class AModule {}

View File

@@ -0,0 +1,12 @@
import { Injectable, Module, forwardRef } from '@nestjs/common';
import { AModule, AProvider } from './a.module';
@Injectable()
export class BProvider {}
@Module({
imports: [forwardRef(() => AModule)],
providers: [BProvider],
exports: [BProvider],
})
export class BModule {}

View File

@@ -0,0 +1,309 @@
import {
Controller,
DynamicModule,
forwardRef,
Global,
Injectable,
Module,
} from '@nestjs/common';
import { LazyModuleLoader } from '@nestjs/core';
import { Test, TestingModule } from '@nestjs/testing';
import { expect } from 'chai';
import { AModule, AProvider } from './circular-dependency/a.module';
import { BModule, BProvider } from './circular-dependency/b.module';
describe('Modules overriding', () => {
describe('Top-level module', () => {
@Controller()
class ControllerOverwritten {}
@Module({
controllers: [ControllerOverwritten],
})
class ModuleToBeOverwritten {}
@Controller()
class ControllerOverride {}
@Module({
controllers: [ControllerOverride],
})
class ModuleOverride {}
let testingModule: TestingModule;
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [ModuleToBeOverwritten],
})
.overrideModule(ModuleToBeOverwritten)
.useModule(ModuleOverride)
.compile();
});
it('should override top-level modules using testing module builder', () => {
expect(() =>
testingModule.get<ControllerOverwritten>(ControllerOverwritten),
).to.throw();
expect(
testingModule.get<ControllerOverride>(ControllerOverride),
).to.be.an.instanceof(ControllerOverride);
});
});
describe('Dynamic module', () => {
@Controller()
class ControllerOverwritten {}
@Module({})
class DynamicModuleToBeOverwritten {}
const dynamicModuleOverwritten: DynamicModule = {
module: DynamicModuleToBeOverwritten,
controllers: [ControllerOverwritten],
};
@Controller()
class ControllerOverride {}
@Module({})
class DynamicModuleOverride {}
const dynamicModuleOverride: DynamicModule = {
module: DynamicModuleOverride,
controllers: [ControllerOverride],
};
let testingModule: TestingModule;
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [dynamicModuleOverwritten],
})
.overrideModule(dynamicModuleOverwritten)
.useModule(dynamicModuleOverride)
.compile();
});
it('should override dynamic modules using testing module builder', () => {
expect(() =>
testingModule.get<ControllerOverwritten>(ControllerOverwritten),
).to.throw();
expect(
testingModule.get<ControllerOverride>(ControllerOverride),
).to.be.an.instanceof(ControllerOverride);
});
});
describe('Circular dependency module', () => {
let testingModule: TestingModule;
@Injectable()
class CProvider {}
@Module({
providers: [CProvider],
})
class CModule {}
@Injectable()
class BProviderOverride {}
@Module({
imports: [forwardRef(() => AModule), forwardRef(() => CModule)],
providers: [BProviderOverride],
exports: [BProviderOverride],
})
class BModuleOverride {}
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [AModule],
})
.overrideModule(BModule)
.useModule(BModuleOverride)
.compile();
});
it('should override top-level modules using testing module builder', () => {
expect(testingModule.get<AProvider>(AProvider)).to.be.an.instanceof(
AProvider,
);
expect(() => testingModule.get<BProvider>(BProvider)).to.throw();
expect(testingModule.get<CProvider>(CProvider)).to.be.an.instanceof(
CProvider,
);
expect(
testingModule.get<BProviderOverride>(BProviderOverride),
).to.be.an.instanceof(BProviderOverride);
});
});
describe('Nested module', () => {
let testingModule: TestingModule;
@Controller()
class OverwrittenNestedModuleController {}
@Module({
controllers: [OverwrittenNestedModuleController],
})
class OverwrittenNestedModule {}
@Controller()
class OverrideNestedModuleController {}
@Module({
controllers: [OverrideNestedModuleController],
})
class OverrideNestedModule {}
@Module({
imports: [OverwrittenNestedModule],
})
class AppModule {}
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideModule(OverwrittenNestedModule)
.useModule(OverrideNestedModule)
.compile();
});
it('should override nested modules using testing module builder', () => {
expect(
testingModule.get<OverrideNestedModuleController>(
OverrideNestedModuleController,
),
).to.be.an.instanceof(OverrideNestedModuleController);
expect(() =>
testingModule.get<OverwrittenNestedModuleController>(
OverwrittenNestedModuleController,
),
).to.throw();
});
});
describe('Lazy-loaded module', () => {
let testingModule: TestingModule;
@Injectable()
class OverwrittenLazyProvider {
value() {
return 'overwritten lazy';
}
}
@Module({
providers: [
{
provide: 'LAZY_PROVIDER',
useClass: OverwrittenLazyProvider,
},
],
})
class OverwrittenLazyModule {}
@Injectable()
class OverrideLazyProvider {
value() {
return 'override lazy';
}
}
@Module({
providers: [
{
provide: 'LAZY_PROVIDER',
useClass: OverrideLazyProvider,
},
],
})
class OverrideLazyModule {}
@Injectable()
class AppService {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async value() {
const moduleRef = await this.lazyModuleLoader.load(
() => OverwrittenLazyModule,
);
return moduleRef.get('LAZY_PROVIDER').value();
}
}
@Module({
imports: [],
providers: [AppService],
})
class AppModule {}
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideModule(OverwrittenLazyModule)
.useModule(OverrideLazyModule)
.compile();
});
it('should override lazy loaded modules using testing module builder', async () => {
const result = await testingModule.get<AppService>(AppService).value();
expect(result).to.be.equal('override lazy');
});
});
describe('Global module', () => {
let testingModule: TestingModule;
@Injectable()
class OverwrittenProvider {
value() {
return 'overwritten lazy';
}
}
@Global()
@Module({
providers: [OverwrittenProvider],
exports: [OverwrittenProvider],
})
class OverwrittenModule {}
@Injectable()
class OverrideProvider {
value() {
return 'override lazy';
}
}
@Global()
@Module({
providers: [OverrideProvider],
exports: [OverrideProvider],
})
class OverrideModule {}
beforeEach(async () => {
testingModule = await Test.createTestingModule({
imports: [OverwrittenModule],
})
.overrideModule(OverwrittenModule)
.useModule(OverrideModule)
.compile();
});
it('should override global modules using testing module builder', () => {
expect(
testingModule.get<OverrideProvider>(OverrideProvider),
).to.be.an.instanceof(OverrideProvider);
expect(() =>
testingModule.get<OverwrittenProvider>(OverwrittenProvider),
).to.throw();
});
});
});

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -352,4 +352,70 @@ describe('URI Versioning', () => {
await app.close();
});
});
// ======================================================================== //
describe('with the global prefix enabled and an excluded route', () => {
before(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
app.setGlobalPrefix('api', { exclude: ['/foo/bar'] });
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
await app.init();
});
describe('GET /', () => {
it('V1', () => {
return request(app.getHttpServer())
.get('/api/v1')
.expect(200)
.expect('Hello World V1!');
});
it('V2', () => {
return request(app.getHttpServer())
.get('/api/v2')
.expect(200)
.expect('Hello World V2!');
});
it('V3', () => {
return request(app.getHttpServer()).get('/api/v3').expect(404);
});
it('No Version', () => {
return request(app.getHttpServer()).get('/api').expect(404);
});
});
describe('GET /foo/bar (excluded from the API prefix)', () => {
it('V1', () => {
return request(app.getHttpServer())
.get('/v1/foo/bar')
.expect(200)
.expect('Hello FooBar!');
});
it('V2', () => {
return request(app.getHttpServer()).get('/v2/foo/bar').expect(404);
});
it('V3', () => {
return request(app.getHttpServer()).get('/v3/foo/bar').expect(404);
});
it('No Version', () => {
return request(app.getHttpServer()).get('/foo/bar').expect(404);
});
});
after(async () => {
await app.close();
});
});
});

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

View File

@@ -145,7 +145,7 @@ describe('WebSocketGateway (WsAdapter)', () => {
}),
);
});
});
}).timeout(5000);
it(`should support 2 different gateways running on the same path (but different ports)`, async function () {
this.retries(10);

View File

@@ -7,7 +7,7 @@
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"target": "ES2021",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist",

39395
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -176,7 +176,7 @@
"ws": "8.13.0"
},
"engines": {
"node": ">= 12.9.0"
"node": ">= 16"
},
"collective": {
"type": "opencollective",

View File

@@ -1,16 +0,0 @@
import { MODULE_OPTIONS_TOKEN } from './cache.module-definition';
/**
* @deprecated `CacheModule` (from the `@nestjs/common` package) is deprecated and will be removed in the next major release. Please, use the `@nestjs/cache-manager` package instead
* @publicApi
*/
export const CACHE_MANAGER = 'CACHE_MANAGER';
export const CACHE_KEY_METADATA = 'cache_module:cache_key';
export const CACHE_TTL_METADATA = 'cache_module:cache_ttl';
/**
* @deprecated `CacheModule` (from the `@nestjs/common` package) is deprecated and will be removed in the next major release. Please, use the `@nestjs/cache-manager` package instead
* @publicApi
*/
export const CACHE_MODULE_OPTIONS = MODULE_OPTIONS_TOKEN;

View File

@@ -1,12 +0,0 @@
import { ConfigurableModuleBuilder } from '../module-utils';
import {
CacheModuleOptions,
CacheOptionsFactory,
} from './interfaces/cache-module.interface';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
new ConfigurableModuleBuilder<CacheModuleOptions>({
moduleName: 'Cache',
})
.setFactoryMethodName('createCacheOptions' as keyof CacheOptionsFactory)
.build();

View File

@@ -1,60 +0,0 @@
import { Module } from '../decorators';
import { DynamicModule } from '../interfaces';
import { CACHE_MANAGER } from './cache.constants';
import { ConfigurableModuleClass } from './cache.module-definition';
import { createCacheManager } from './cache.providers';
import {
CacheModuleAsyncOptions,
CacheModuleOptions,
} from './interfaces/cache-module.interface';
/**
* Module that provides Nest cache-manager.
*
* @see [Caching](https://docs.nestjs.com/techniques/caching)
* @deprecated `CacheModule` (from the `@nestjs/common` package) is deprecated and will be removed in the next major release. Please, use the `@nestjs/cache-manager` package instead
* @publicApi
*/
@Module({
providers: [createCacheManager()],
exports: [CACHE_MANAGER],
})
export class CacheModule extends ConfigurableModuleClass {
/**
* Configure the cache manager statically.
*
* @param options options to configure the cache manager
*
* @see [Customize caching](https://docs.nestjs.com/techniques/caching#customize-caching)
*/
static register<StoreConfig extends Record<any, any> = Record<string, any>>(
options: CacheModuleOptions<StoreConfig> = {} as any,
): DynamicModule {
return {
global: options.isGlobal,
...super.register(options),
};
}
/**
* Configure the cache manager dynamically.
*
* @param options method for dynamically supplying cache manager configuration
* options
*
* @see [Async configuration](https://docs.nestjs.com/techniques/caching#async-configuration)
*/
static registerAsync<
StoreConfig extends Record<any, any> = Record<string, any>,
>(options: CacheModuleAsyncOptions<StoreConfig>): DynamicModule {
const moduleDefinition = super.registerAsync(options);
return {
global: options.isGlobal,
...moduleDefinition,
providers: options.extraProviders
? moduleDefinition.providers.concat(options.extraProviders)
: moduleDefinition.providers,
};
}
}

View File

@@ -1,61 +0,0 @@
import { Provider } from '../interfaces';
import { loadPackage } from '../utils/load-package.util';
import { CACHE_MANAGER } from './cache.constants';
import { MODULE_OPTIONS_TOKEN } from './cache.module-definition';
import { defaultCacheOptions } from './default-options';
import {
CacheManagerOptions,
CacheStore,
} from './interfaces/cache-manager.interface';
/**
* Creates a CacheManager Provider.
*
* @publicApi
*/
export function createCacheManager(): Provider {
return {
provide: CACHE_MANAGER,
useFactory: async (options: CacheManagerOptions) => {
const cacheManager = loadPackage('cache-manager', 'CacheModule', () =>
require('cache-manager'),
);
const cacheManagerIsv5OrGreater = 'memoryStore' in cacheManager;
const cachingFactory = async (
store: CacheManagerOptions['store'],
options: Omit<CacheManagerOptions, 'store'>,
): Promise<Record<string, any>> => {
if (!cacheManagerIsv5OrGreater) {
return cacheManager.caching({
...defaultCacheOptions,
...{ ...options, store },
});
}
let cache: string | Function | CacheStore = 'memory';
defaultCacheOptions.ttl *= 1000;
if (typeof store === 'object') {
if ('create' in store) {
cache = store.create;
} else {
cache = store;
}
} else if (typeof store === 'function') {
cache = store;
}
return cacheManager.caching(cache, {
...defaultCacheOptions,
...options,
});
};
return Array.isArray(options)
? cacheManager.multiCaching(
await Promise.all(
options.map(option => cachingFactory(option.store, option)),
),
)
: cachingFactory(options.store, options);
},
inject: [MODULE_OPTIONS_TOKEN],
};
}

View File

@@ -1,18 +0,0 @@
import { SetMetadata } from '../../decorators';
import { CACHE_KEY_METADATA } from '../cache.constants';
/**
* Decorator that sets the caching key used to store/retrieve cached items for
* Web sockets or Microservice based apps.
*
* For example:
* `@CacheKey('events')`
*
* @param key string naming the field to be used as a cache key
*
* @see [Caching](https://docs.nestjs.com/techniques/caching)
*
* @deprecated `CacheModule` (from the `@nestjs/common` package) is deprecated and will be removed in the next major release. Please, use the `@nestjs/cache-manager` package instead
* @publicApi
*/
export const CacheKey = (key: string) => SetMetadata(CACHE_KEY_METADATA, key);

View File

@@ -1,19 +0,0 @@
import { SetMetadata } from '../../decorators';
import { ExecutionContext } from '../../interfaces/features/execution-context.interface';
import { CACHE_TTL_METADATA } from '../cache.constants';
/**
* Decorator that sets the cache ttl setting the duration for cache expiration.
*
* For example: `@CacheTTL(5)`
*
* @param ttl number set the cache expiration time
*
* @see [Caching](https://docs.nestjs.com/techniques/caching)
*
* @deprecated `CacheModule` (from the `@nestjs/common` package) is deprecated and will be removed in the next major release. Please, use the `@nestjs/cache-manager` package instead
* @publicApi
*/
type CacheTTLFactory = (ctx: ExecutionContext) => Promise<number> | number;
export const CacheTTL = (ttl: number | CacheTTLFactory) =>
SetMetadata(CACHE_TTL_METADATA, ttl);

View File

@@ -1,2 +0,0 @@
export * from './cache-key.decorator';
export * from './cache-ttl.decorator';

View File

@@ -1,5 +0,0 @@
export const defaultCacheOptions = {
ttl: 5,
max: 100,
store: 'memory',
};

View File

@@ -1,5 +0,0 @@
export * from './cache.constants';
export * from './cache.module';
export * from './decorators';
export * from './interceptors';
export * from './interfaces';

View File

@@ -1,134 +0,0 @@
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Inject, Injectable, Optional } from '../../decorators';
import { StreamableFile } from '../../file-stream';
import {
CallHandler,
ExecutionContext,
HttpServer,
NestInterceptor,
} from '../../interfaces';
import { Logger } from '../../services/logger.service';
import { loadPackage } from '../../utils/load-package.util';
import { isFunction, isNil } from '../../utils/shared.utils';
import {
CACHE_KEY_METADATA,
CACHE_MANAGER,
CACHE_TTL_METADATA,
} from '../cache.constants';
/** @deprecated */
const HTTP_ADAPTER_HOST = 'HttpAdapterHost';
/** @deprecated */
const REFLECTOR = 'Reflector';
/** @deprecated Import from the "@nestjs/core" instead. */
export interface HttpAdapterHost<T extends HttpServer = any> {
httpAdapter: T;
}
/**
* @see [Caching](https://docs.nestjs.com/techniques/caching)
*
* @deprecated `CacheModule` (from the `@nestjs/common` package) is deprecated and will be removed in the next major release. Please, use the `@nestjs/cache-manager` package instead
* @publicApi
*/
@Injectable()
export class CacheInterceptor implements NestInterceptor {
@Optional()
@Inject(HTTP_ADAPTER_HOST)
protected readonly httpAdapterHost: HttpAdapterHost;
protected allowedMethods = ['GET'];
private cacheManagerIsv5OrGreater: boolean;
constructor(
@Inject(CACHE_MANAGER) protected readonly cacheManager: any,
@Inject(REFLECTOR) protected readonly reflector: any,
) {
// We need to check if the cache-manager package is v5 or greater
// because the set method signature changed in v5
const cacheManagerPackage = loadPackage(
'cache-manager',
'CacheModule',
() => require('cache-manager'),
);
this.cacheManagerIsv5OrGreater = 'memoryStore' in cacheManagerPackage;
Logger.warn(
'DEPRECATED! "CacheModule" (from the "@nestjs/common" package) is deprecated and will be removed in the next major release. Please, use the "@nestjs/cache-manager" package instead.',
);
}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const key = this.trackBy(context);
const ttlValueOrFactory =
this.reflector.get(CACHE_TTL_METADATA, context.getHandler()) ?? null;
if (!key) {
return next.handle();
}
try {
const value = await this.cacheManager.get(key);
if (!isNil(value)) {
return of(value);
}
const ttl = isFunction(ttlValueOrFactory)
? await ttlValueOrFactory(context)
: ttlValueOrFactory;
return next.handle().pipe(
tap(async response => {
if (response instanceof StreamableFile) {
return;
}
const args = [key, response];
if (!isNil(ttl)) {
args.push(this.cacheManagerIsv5OrGreater ? ttl : { ttl });
}
try {
await this.cacheManager.set(...args);
} catch (err) {
Logger.error(
`An error has occurred when inserting "key: ${key}", "value: ${response}"`,
'CacheInterceptor',
);
}
}),
);
} catch {
return next.handle();
}
}
protected trackBy(context: ExecutionContext): string | undefined {
const httpAdapter = this.httpAdapterHost.httpAdapter;
const isHttpApp = httpAdapter && !!httpAdapter.getRequestMethod;
const cacheMetadata = this.reflector.get(
CACHE_KEY_METADATA,
context.getHandler(),
);
if (!isHttpApp || cacheMetadata) {
return cacheMetadata;
}
const request = context.getArgByIndex(0);
if (!this.isRequestCacheable(context)) {
return undefined;
}
return httpAdapter.getRequestUrl(request);
}
protected isRequestCacheable(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
return this.allowedMethods.includes(req.method);
}
}

View File

@@ -1 +0,0 @@
export * from './cache.interceptor';

View File

@@ -1,86 +0,0 @@
export interface LiteralObject {
[key: string]: any;
}
/**
* Interface defining a cache store. Implement this interface to create a custom
* cache store.
*
* @publicApi
*/
export interface CacheStore {
/**
* Create a key/value pair in the cache.
*
* @param key cache key
* @param value cache value
*/
set<T>(
key: string,
value: T,
options?: CacheStoreSetOptions<T> | number,
): Promise<void> | void;
/**
* Retrieve a key/value pair from the cache.
*
* @param key cache key
*/
get<T>(key: string): Promise<T | undefined> | T | undefined;
/**
* Destroy a key/value pair from the cache.
*
* @param key cache key
*/
del?(key: string): void | Promise<void>;
}
export interface CacheStoreSetOptions<T> {
/**
* Time to live - amount of time in seconds that a response is cached before it
* is deleted. Defaults based on your cache manager settings.
*/
ttl?: ((value: T) => number) | number;
}
/**
* Interface defining a factory to create a cache store.
*
* @publicApi
*/
export type CacheStoreFactory =
| {
/**
* Return a configured cache store.
*
* @param args Cache manager options received from `CacheModule.register()`
* or `CacheModule.registerAsync()`
*/
create(args: LiteralObject): CacheStore;
}
| ((args: LiteralObject) => CacheStore | Promise<CacheStore>);
/**
* Interface defining Cache Manager configuration options.
*
* @publicApi
*/
export interface CacheManagerOptions {
/**
* Cache storage manager. Default is `'memory'` (in-memory store). See
* [Different stores](https://docs.nestjs.com/techniques/caching#different-stores)
* for more info.
*/
store?: string | CacheStoreFactory | CacheStore;
/**
* Time to live - amount of time that a response is cached before it
* is deleted. Subsequent request will call through the route handler and refresh
* the cache. Defaults to 5 seconds. In `cache-manager@^4` this value is in seconds.
* In `cache-manager@^5` this value is in milliseconds.
*/
ttl?: number;
/**
* Maximum number of responses to store in the cache. Defaults to 100.
*/
max?: number;
isCacheableValue?: (value: any) => boolean;
}

View File

@@ -1,78 +0,0 @@
import { Provider, Type } from '../../interfaces';
import { ConfigurableModuleAsyncOptions } from '../../module-utils';
import { CacheManagerOptions } from './cache-manager.interface';
export type CacheModuleOptions<
StoreConfig extends Record<any, any> = Record<string, any>,
> =
// Store-specific configuration takes precedence over cache module options due
// to how `createCacheManager` is implemented.
CacheManagerOptions &
StoreConfig & {
/**
* If "true', register `CacheModule` as a global module.
*/
isGlobal?: boolean;
};
/**
* Interface describing a `CacheOptionsFactory`. Providers supplying configuration
* options for the Cache module must implement this interface.
*
* @see [Async configuration](https://docs.nestjs.com/techniques/caching#async-configuration)
*
* @publicApi
*/
export interface CacheOptionsFactory<
StoreConfig extends Record<any, any> = Record<string, any>,
> {
createCacheOptions():
| Promise<CacheModuleOptions<StoreConfig>>
| CacheModuleOptions<StoreConfig>;
}
/**
* Options for dynamically configuring the Cache module.
*
* @see [Async configuration](https://docs.nestjs.com/techniques/caching#async-configuration)
*
* @publicApi
*/
export interface CacheModuleAsyncOptions<
StoreConfig extends Record<any, any> = Record<string, any>,
> extends ConfigurableModuleAsyncOptions<
CacheModuleOptions<StoreConfig>,
keyof CacheOptionsFactory
> {
/**
* Injection token resolving to an existing provider. The provider must implement
* the `CacheOptionsFactory` interface.
*/
useExisting?: Type<CacheOptionsFactory<StoreConfig>>;
/**
* Injection token resolving to a class that will be instantiated as a provider.
* The class must implement the `CacheOptionsFactory` interface.
*/
useClass?: Type<CacheOptionsFactory<StoreConfig>>;
/**
* Function returning options (or a Promise resolving to options) to configure the
* cache module.
*/
useFactory?: (
...args: any[]
) =>
| Promise<CacheModuleOptions<StoreConfig>>
| CacheModuleOptions<StoreConfig>;
/**
* Dependencies that a Factory may inject.
*/
inject?: any[];
/**
* Extra providers to be registered within a scope of this module.
*/
extraProviders?: Provider[];
/**
* If "true', register `CacheModule` as a global module.
*/
isGlobal?: boolean;
}

View File

@@ -1,2 +0,0 @@
export * from './cache-manager.interface';
export * from './cache-module.interface';

View File

@@ -17,10 +17,6 @@ export const SCOPE_OPTIONS_METADATA = 'scope:options';
export const METHOD_METADATA = 'method';
export const ROUTE_ARGS_METADATA = '__routeArguments__';
export const CUSTOM_ROUTE_ARGS_METADATA = '__customRouteArgs__';
/**
* @deprecated Use `CUSTOM_ROUTE_ARGS_METADATA` instead
*/
export const CUSTOM_ROUTE_AGRS_METADATA = CUSTOM_ROUTE_ARGS_METADATA;
export const FILTER_CATCH_EXCEPTIONS = '__filterCatchExceptions__';
export const PIPES_METADATA = '__pipes__';

View File

@@ -33,7 +33,9 @@ import { isUndefined } from '../../utils/shared.utils';
*
* @publicApi
*/
export function Inject<T = any>(token?: T) {
export function Inject<T = any>(
token?: T,
): PropertyDecorator & ParameterDecorator {
return (target: object, key: string | symbol | undefined, index?: number) => {
const type = token || Reflect.getMetadata('design:type', target, key);

View File

@@ -17,7 +17,7 @@ import { isUndefined } from '../../utils/shared.utils';
*
* @publicApi
*/
export function Optional() {
export function Optional(): PropertyDecorator & ParameterDecorator {
return (target: object, key: string | symbol | undefined, index?: number) => {
if (!isUndefined(index)) {
const args = Reflect.getMetadata(OPTIONAL_DEPS_METADATA, target) || [];

View File

@@ -109,3 +109,12 @@ export const Head = createMappingDecorator(RequestMethod.HEAD);
* @publicApi
*/
export const All = createMappingDecorator(RequestMethod.ALL);
/**
* Route handler (method) Decorator. Routes all HTTP requests to the specified path.
*
* @see [Routing](https://docs.nestjs.com/controllers#routing)
*
* @publicApi
*/
export const Search = createMappingDecorator(RequestMethod.SEARCH);

View File

@@ -7,4 +7,5 @@ export enum RequestMethod {
ALL,
OPTIONS,
HEAD,
SEARCH,
}

View File

@@ -1,4 +1,7 @@
import { Logger } from '../services';
import {
HttpExceptionBody,
HttpExceptionBodyMessage,
} from '../interfaces/http/http-exception-body.interface';
import { isObject, isString } from '../utils/shared.utils';
export interface HttpExceptionOptions {
@@ -26,8 +29,6 @@ export class HttpException extends Error {
* @example
* throw new HttpException()
* throw new HttpException('message', HttpStatus.BAD_REQUEST)
* throw new HttpException({ reason: 'this can be a human readable reason' }, HttpStatus.BAD_REQUEST)
* throw new HttpException(new Error('Cause Error'), HttpStatus.BAD_REQUEST)
* throw new HttpException('custom message', HttpStatus.BAD_REQUEST, {
* cause: new Error('Cause Error'),
* })
@@ -81,13 +82,6 @@ export class HttpException extends Error {
this.cause = this.options.cause;
return;
}
if (this.response instanceof Error) {
Logger.warn(
'DEPRECATED! Passing the error cause as the first argument to HttpException constructor is deprecated. You should use the "options" parameter instead: new HttpException("message", 400, { cause: new Error("Some Error") }) ',
);
this.cause = this.response;
}
}
public initMessage() {
@@ -118,17 +112,42 @@ export class HttpException extends Error {
}
public static createBody(
objectOrErrorMessage: object | string,
description?: string,
nil: null | '',
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,
statusCode?: number,
) {
if (!objectOrErrorMessage) {
return { statusCode, message: description };
): HttpExceptionBody | Body {
if (!arg0) {
return {
message: arg1,
statusCode: statusCode,
};
}
return isObject(objectOrErrorMessage) &&
!Array.isArray(objectOrErrorMessage)
? objectOrErrorMessage
: { statusCode, message: objectOrErrorMessage, error: description };
if (isString(arg0) || Array.isArray(arg0)) {
return {
message: arg0,
error: arg1 as string,
statusCode: statusCode,
};
}
return arg0;
}
public static getDescriptionFrom(

View File

@@ -6,7 +6,6 @@
*/
import 'reflect-metadata';
export * from './cache';
export * from './decorators';
export * from './enums';
export * from './exceptions';
@@ -27,6 +26,8 @@ export {
FactoryProvider,
ForwardReference,
HttpServer,
HttpExceptionBody,
HttpExceptionBodyMessage,
INestApplication,
INestApplicationContext,
INestMicroservice,

View File

@@ -0,0 +1,7 @@
export type HttpExceptionBodyMessage = string | string[];
export interface HttpExceptionBody {
message: HttpExceptionBodyMessage;
error?: string;
statusCode: number;
}

View File

@@ -18,7 +18,11 @@ export type RequestHandler<TRequest = any, TResponse = any> = (
next?: Function,
) => any;
export interface HttpServer<TRequest = any, TResponse = any> {
export interface HttpServer<
TRequest = any,
TResponse = any,
ServerInstance = any,
> {
use(
handler:
| RequestHandler<TRequest, TResponse>
@@ -69,7 +73,7 @@ export interface HttpServer<TRequest = any, TResponse = any> {
getRequestHostname?(request: TRequest): string;
getRequestMethod?(request: TRequest): string;
getRequestUrl?(request: TRequest): string;
getInstance(): any;
getInstance(): ServerInstance;
registerParserMiddleware(...args: any[]): any;
enableCors(options: CorsOptions | CorsOptionsDelegate<TRequest>): any;
getHttpServer(): any;

View File

@@ -1,3 +1,4 @@
export * from './http-exception-body.interface';
export * from './http-server.interface';
export * from './message-event.interface';
export * from './raw-body-request.interface';

View File

@@ -4,9 +4,9 @@ import { Type } from '../type.interface';
/**
* @publicApi
*/
export type InjectionToken =
export type InjectionToken<T = any> =
| string
| symbol
| Type<any>
| Abstract<any>
| Type<T>
| Abstract<T>
| Function;

View File

@@ -21,7 +21,8 @@ import { WebSocketAdapter } from './websockets/web-socket-adapter.interface';
*
* @publicApi
*/
export interface INestApplication extends INestApplicationContext {
export interface INestApplication<TServer = any>
extends INestApplicationContext {
/**
* A wrapper function around HTTP adapter method: `adapter.use()`.
* Example `app.use(cors())`
@@ -110,9 +111,9 @@ export interface INestApplication extends INestApplicationContext {
/**
* Returns the underlying native HTTP server.
*
* @returns {*}
* @returns {TServer}
*/
getHttpServer(): any;
getHttpServer(): TServer;
/**
* Returns the underlying HTTP adapter.

View File

@@ -23,16 +23,12 @@
"uid": "2.0.2"
},
"peerDependencies": {
"cache-manager": "<=5",
"class-transformer": "*",
"class-validator": "*",
"reflect-metadata": "^0.1.12",
"rxjs": "^7.1.0"
},
"peerDependenciesMeta": {
"cache-manager": {
"optional": true
},
"class-validator": {
"optional": true
},

View File

@@ -1,4 +1,5 @@
import { FileValidator } from './file-validator.interface';
import { IFile } from './interfaces';
export type FileTypeValidatorOptions = {
fileType: string | RegExp;
@@ -20,17 +21,14 @@ export class FileTypeValidator extends FileValidator<FileTypeValidatorOptions> {
return `Validation failed (expected type is ${this.validationOptions.fileType})`;
}
isValid(file: any): boolean {
isValid<TFile extends IFile = any>(file: TFile): boolean {
if (!this.validationOptions) {
return true;
}
if (!file.mimetype) {
return false;
}
return Boolean(
(file.mimetype as string).match(this.validationOptions.fileType),
return (
'mimetype' in file &&
!!file.mimetype.match(this.validationOptions.fileType)
);
}
}

View File

@@ -1,3 +1,5 @@
import { IFile } from './interfaces';
/**
* Interface describing FileValidators, which can be added to a ParseFilePipe
*
@@ -11,7 +13,9 @@ export abstract class FileValidator<TValidationOptions = Record<string, any>> {
* Indicates if this file should be considered valid, according to the options passed in the constructor.
* @param file the file from the request object
*/
abstract isValid(file?: any): boolean | Promise<boolean>;
abstract isValid<TFile extends IFile = any>(
file?: TFile,
): boolean | Promise<boolean>;
/**
* Builds an error message in case the validation fails.

View File

@@ -0,0 +1,4 @@
export interface IFile {
mimetype: string;
size: number;
}

View File

@@ -0,0 +1 @@
export * from './file.interface';

View File

@@ -1,4 +1,5 @@
import { FileValidator } from './file-validator.interface';
import { IFile } from './interfaces';
export type MaxFileSizeValidatorOptions = {
maxSize: number;
@@ -25,11 +26,11 @@ export class MaxFileSizeValidator extends FileValidator<MaxFileSizeValidatorOpti
return `Validation failed (expected size is less than ${this.validationOptions.maxSize})`;
}
public isValid(file: any): boolean {
public isValid<TFile extends IFile = any>(file: TFile): boolean {
if (!this.validationOptions) {
return true;
}
return file.size < this.validationOptions.maxSize;
return 'size' in file && file.size < this.validationOptions.maxSize;
}
}

View File

@@ -18,7 +18,7 @@ import {
HttpErrorByCode,
} from '../utils/http-error-by-code.util';
import { loadPackage } from '../utils/load-package.util';
import { isNil } from '../utils/shared.utils';
import { isNil, isUndefined } from '../utils/shared.utils';
/**
* @publicApi
@@ -193,6 +193,13 @@ export class ValidationPipe implements PipeTransform<any> {
return value;
}
if (metatype === Boolean) {
if (isUndefined(value)) {
// This is an workaround to deal with optional boolean values since
// optional booleans shouldn't be parsed to a valid boolean when
// they were not defined
return undefined;
}
// Any fasly value but `undefined` will be parsed to `false`
return value === true || value === 'true';
}
if (metatype === Number) {

View File

@@ -1,47 +0,0 @@
import { expect } from 'chai';
import { CacheKey, CacheInterceptor, CacheTTL } from '../../cache';
import {
CACHE_KEY_METADATA,
CACHE_TTL_METADATA,
} from '../../cache/cache.constants';
import { UseInterceptors } from '../../decorators';
import { Controller } from '../../decorators/core/controller.decorator';
describe('@Cache', () => {
@Controller('test')
@CacheKey('/a_different_cache_key')
@CacheTTL(99999)
@UseInterceptors(CacheInterceptor)
class TestAll {}
@Controller()
@UseInterceptors(CacheInterceptor)
@CacheKey('/a_different_cache_key')
class TestKey {}
@Controller()
@UseInterceptors(CacheInterceptor)
@CacheTTL(99999)
class TestTTL {}
it('should override global defaults for CacheKey and CacheTTL', () => {
expect(Reflect.getMetadata(CACHE_KEY_METADATA, TestAll)).to.be.eql(
'/a_different_cache_key',
);
expect(Reflect.getMetadata(CACHE_TTL_METADATA, TestAll)).to.be.greaterThan(
9999,
);
});
it('should override only the TTL', () => {
expect(Reflect.getMetadata(CACHE_TTL_METADATA, TestTTL)).to.be.greaterThan(
9999,
);
});
it('should override only the Key', () => {
expect(Reflect.getMetadata(CACHE_KEY_METADATA, TestKey)).to.be.eql(
'/a_different_cache_key',
);
});
});

View File

@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { Body, HostParam, Param, Query } from '../../decorators';
import { Body, HostParam, Param, Query, Search } from '../../decorators';
import { RequestMethod } from '../../enums/request-method.enum';
import { All, Delete, Get, Patch, Post, Put } from '../../index';
@@ -311,6 +311,63 @@ describe('@Patch', () => {
});
});
describe('@Search', () => {
const requestPath = 'test';
const requestProps = {
path: requestPath,
method: RequestMethod.SEARCH,
};
const requestPathUsingArray = ['foo', 'bar'];
const requestPropsUsingArray = {
path: requestPathUsingArray,
method: RequestMethod.SEARCH,
};
it('should enhance class with expected request metadata', () => {
class Test {
@Search(requestPath)
public static test() {}
@Search(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 {
@Search()
public static test(
@Query() query,
@Param() params,
@HostParam() hostParams,
) {}
@Search([])
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('Inheritance', () => {
const requestPath = 'test';
const requestProps = {

View File

@@ -1,5 +1,5 @@
import { Type } from '../../../common';
import { expect } from 'chai';
import { Type } from '../../../common';
import {
BadGatewayException,
BadRequestException,
@@ -220,15 +220,6 @@ describe('HttpException', () => {
const errorCause = new Error('An internal error cause');
const customDescription = 'custom description';
it('configures a cause when message is an instance of error', () => {
const message = new Error('Some Error');
const error = new HttpException(message, 400);
expect(`${error}`).to.be.eql(`HttpException: ${message.message}`);
const { cause } = error;
expect(cause).to.be.eql(message);
});
it('configures a cause when message is a string and the options object is passed', () => {
const error = new HttpException(customDescription, 400, {
cause: errorCause,

View File

@@ -1,5 +1,5 @@
import { FileTypeValidator } from '../../../pipes';
import { expect } from 'chai';
import { FileTypeValidator } from '../../../pipes';
describe('FileTypeValidator', () => {
describe('isValid', () => {
@@ -10,7 +10,7 @@ describe('FileTypeValidator', () => {
const requestFile = {
mimetype: 'image/jpeg',
};
} as any;
expect(fileTypeValidator.isValid(requestFile)).to.equal(true);
});
@@ -22,7 +22,7 @@ describe('FileTypeValidator', () => {
const requestFile = {
mimetype: 'image/jpeg',
};
} as any;
expect(fileTypeValidator.isValid(requestFile)).to.equal(true);
});
@@ -34,7 +34,7 @@ describe('FileTypeValidator', () => {
const requestFile = {
mimetype: 'application/msword',
};
} as any;
expect(fileTypeValidator.isValid(requestFile)).to.equal(true);
});
@@ -46,7 +46,7 @@ describe('FileTypeValidator', () => {
const requestFile = {
mimetype: 'image/png',
};
} as any;
expect(fileTypeValidator.isValid(requestFile)).to.equal(false);
});
@@ -58,7 +58,7 @@ describe('FileTypeValidator', () => {
const requestFile = {
mimetype: 'image/png',
};
} as any;
expect(fileTypeValidator.isValid(requestFile)).to.equal(false);
});
@@ -68,7 +68,7 @@ describe('FileTypeValidator', () => {
fileType: 'image/jpeg',
});
const requestFile = {};
const requestFile = {} as any;
expect(fileTypeValidator.isValid(requestFile)).to.equal(false);
});

View File

@@ -12,7 +12,7 @@ describe('MaxFileSizeValidator', () => {
const requestFile = {
size: 100,
};
} as any;
expect(maxFileSizeValidator.isValid(requestFile)).to.equal(true);
});
@@ -24,7 +24,7 @@ describe('MaxFileSizeValidator', () => {
const requestFile = {
size: oneKb + 1,
};
} as any;
expect(maxFileSizeValidator.isValid(requestFile)).to.equal(false);
});
@@ -36,7 +36,7 @@ describe('MaxFileSizeValidator', () => {
const requestFile = {
size: oneKb,
};
} as any;
expect(maxFileSizeValidator.isValid(requestFile)).to.equal(false);
});

View File

@@ -1,7 +1,6 @@
import { expect } from 'chai';
import {
FileTypeValidator,
FileTypeValidatorOptions,
FileValidator,
MaxFileSizeValidator,
ParseFilePipeBuilder,

View File

@@ -221,7 +221,7 @@ describe('ValidationPipe', () => {
});
});
describe('when input is a query parameter (boolean)', () => {
it('should parse to boolean', async () => {
it('should parse the string "true" to the boolean true', async () => {
target = new ValidationPipe({ transform: true });
const value = 'true';
@@ -233,9 +233,45 @@ describe('ValidationPipe', () => {
}),
).to.be.true;
});
it('should parse the string "false" to the boolean false', async () => {
target = new ValidationPipe({ transform: true });
const value = 'false';
expect(
await target.transform(value, {
metatype: Boolean,
data: 'test',
type: 'query',
}),
).to.be.false;
});
it('should parse an empty string to false', async () => {
target = new ValidationPipe({ transform: true });
const value = '';
expect(
await target.transform(value, {
metatype: Boolean,
data: 'test',
type: 'query',
}),
).to.be.false;
});
it('should parse undefined to undefined', async () => {
target = new ValidationPipe({ transform: true });
const value = undefined;
expect(
await target.transform(value, {
metatype: Boolean,
data: 'test',
type: 'query',
}),
).to.be.undefined;
});
});
describe('when input is a path parameter (boolean)', () => {
it('should parse to boolean', async () => {
it('should parse the string "true" to boolean true', async () => {
target = new ValidationPipe({ transform: true });
const value = 'true';
@@ -247,6 +283,42 @@ describe('ValidationPipe', () => {
}),
).to.be.true;
});
it('should parse the string "false" to boolean false', async () => {
target = new ValidationPipe({ transform: true });
const value = 'false';
expect(
await target.transform(value, {
metatype: Boolean,
data: 'test',
type: 'param',
}),
).to.be.false;
});
it('should parse an empty string to false', async () => {
target = new ValidationPipe({ transform: true });
const value = '';
expect(
await target.transform(value, {
metatype: Boolean,
data: 'test',
type: 'param',
}),
).to.be.false;
});
it('should parse undefined to undefined', async () => {
target = new ValidationPipe({ transform: true });
const value = undefined;
expect(
await target.transform(value, {
metatype: Boolean,
data: 'test',
type: 'param',
}),
).to.be.undefined;
});
});
describe('when validation strips', () => {
it('should return a TestModel without extra properties', async () => {

View File

@@ -0,0 +1,85 @@
import { expect } from 'chai';
import { selectExceptionFilterMetadata } from '../../utils/select-exception-filter-metadata.util';
class FirstError {}
class SecondError {}
class ThirdError {}
class FourthError {}
describe('selectExceptionFilterMetadata', () => {
it('should pass error handling to first suitable error handler', () => {
const metadataList = [
{
exceptionMetatypes: [FirstError, SecondError],
func: () => {},
},
{
exceptionMetatypes: [ThirdError, FourthError],
func: () => {},
},
];
expect(
selectExceptionFilterMetadata(metadataList, new FourthError()),
).to.be.equal(metadataList[1]);
});
describe('when multiple exception handlers are accepting error handling', () => {
it('should pass exception handling to the first one', () => {
const metadataList = [
{
exceptionMetatypes: [FirstError, SecondError],
func: () => {},
},
{
exceptionMetatypes: [FirstError, FourthError],
func: () => {},
},
];
expect(
selectExceptionFilterMetadata(metadataList, new FirstError()),
).to.be.equal(metadataList[0]);
});
});
describe('when no exception handler is accepting error handling', () => {
it('should return undefined', () => {
const metadataList = [
{
exceptionMetatypes: [FirstError, SecondError],
func: () => {},
},
{
exceptionMetatypes: [FirstError, FourthError],
func: () => {},
},
];
expect(selectExceptionFilterMetadata(metadataList, new ThirdError())).to
.be.undefined;
});
});
describe('when exception handler has empty list of meta types', () => {
it('should pass any remaining error handling to it', () => {
const metadataList = [
{
exceptionMetatypes: [FirstError, SecondError],
func: () => {},
},
{
exceptionMetatypes: [],
func: () => {},
},
];
expect(
selectExceptionFilterMetadata(metadataList, new ThirdError()),
).to.be.equal(metadataList[1]);
});
});
});

View File

@@ -0,0 +1,13 @@
import { ExceptionFilterMetadata } from '../interfaces/exceptions';
export const selectExceptionFilterMetadata = <T = any>(
filters: ExceptionFilterMetadata[],
exception: T,
): ExceptionFilterMetadata | undefined =>
filters.find(
({ exceptionMetatypes }) =>
!exceptionMetatypes.length ||
exceptionMetatypes.some(
ExceptionMetaType => exception instanceof ExceptionMetaType,
),
);

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