Compare commits

..

45 Commits

Author SHA1 Message Date
Kamil Myśliwiec
d616861698 style: minor formatting tweaks 2023-02-01 13:09:18 +01:00
Kamil Myśliwiec
c64312fded Merge branch 'master' of github.com:e-dot/nest into e-dot-master 2023-02-01 13:08:15 +01:00
Kamil Myśliwiec
304e6bf5bb Merge branch 'master' of https://github.com/nestjs/nest 2023-02-01 13:03:41 +01:00
Kamil Myśliwiec
161198379a Merge branch 'tolgap-fix/10471-use-body-parser' 2023-02-01 13:03:34 +01:00
Kamil Myśliwiec
661104d981 chore: resolve merge conflicts 2023-02-01 13:03:19 +01:00
Kamil Mysliwiec
f71554ca96 Merge pull request #10696 from kos984/fix/NestApplicationContext-listenToShutdownSignals
fix(core): process exit before shutdown hook end
2023-02-01 13:01:01 +01:00
Kamil Myśliwiec
a986e7e907 Merge branch 'master' of https://github.com/nestjs/nest 2023-02-01 13:00:10 +01:00
Kamil Myśliwiec
920f430d4d Merge branch 'zanminkian-zmj-fix_gloabl_prefix' 2023-02-01 13:00:03 +01:00
Kamil Myśliwiec
cd0462fbcb style: minor formatting tweaks 2023-02-01 12:59:40 +01:00
Kamil Myśliwiec
b9910a400a Merge branch 'zmj-fix_gloabl_prefix' of github.com:zanminkian/nest into zanminkian-zmj-fix_gloabl_prefix 2023-02-01 12:58:09 +01:00
Kamil Mysliwiec
6d88e3cd3c Merge pull request #10809 from micalevisk/fix/durable-payload-regression
fix(core,microservices): inject the context when the tree is not durable
2023-02-01 12:56:47 +01:00
Kamil Myśliwiec
fbf1ab7a81 fix(core): minor fix - reset to random if snapshot is off 2023-02-01 12:55:27 +01:00
Kamil Myśliwiec
868bbcd8b9 Merge branch 'master' of https://github.com/nestjs/nest 2023-02-01 12:53:19 +01:00
Kamil Myśliwiec
4e783852b3 feat(core): use deterministic uuids if absolutely necessary 2023-02-01 12:53:08 +01:00
Kamil Mysliwiec
07a84ae224 Merge pull request #10888 from Tony133/refactor/improvements-imports
refactor(): improvements the imports
2023-02-01 12:39:36 +01:00
Kamil Myśliwiec
7bc72671b6 build: fix ts build error 2023-02-01 12:38:47 +01:00
Kamil Myśliwiec
a560c3466a Merge branch 'H4ad-perf/metadata-scanner' 2023-02-01 12:36:49 +01:00
Kamil Myśliwiec
f10c917b71 style: change inline ifs to code blocks 2023-02-01 12:36:36 +01:00
Kamil Myśliwiec
208d8ca184 Merge branch 'perf/metadata-scanner' of github.com:H4ad/nest into H4ad-perf/metadata-scanner 2023-02-01 12:35:27 +01:00
Kamil Myśliwiec
32aeb7a8a3 Merge branch 'H4ad-perf/random-string-generator' 2023-02-01 12:32:55 +01:00
Antonio Tripodi
a185e63281 Merge branch 'master' into refactor/improvements-imports 2023-02-01 11:40:47 +01:00
Micael Levi L. Cavalcante
f396dc1ddf refactor(core): extract get context id logic into its own method 2023-01-27 12:08:12 -04:00
Micael Levi L. Cavalcante
aafdac62a0 fix(core,microservices): inject the context when the tree is not durable 2023-01-27 12:08:10 -04:00
Tony133
e8944675cb refactor(): improvements the imports 2023-01-16 22:27:58 +01:00
Tony133
4d4f3f82de refactor(): improvements the imports 2023-01-16 16:08:53 +01:00
Vinícius Lourenço
994f8c4e42 perf(core): better performance to get method names 2023-01-06 22:13:23 -03:00
曾明健
58ee3bf2f3 test: fix testing problem 2023-01-04 10:12:16 +00:00
曾明健
5deabef549 fix(core): fix global prefix not working 2023-01-04 10:05:05 +00:00
e-dot
9a47a6ce70 Merge branch 'master' of https://github.com/e-dot/nest 2022-12-14 14:07:25 +01:00
e-dot
eee8968d82 Fix port = 0 issue
When a user passes port:0 to this function, it's legit : in order to detect the presence of attributes, we use another comparison with !== undefined (works with 0).
See PR review here: 35357053b1
2022-12-14 14:06:19 +01:00
e-dot
35357053b1 Update packages/platform-fastify/adapters/fastify-adapter.ts
Ok - code has been rewritten afterwards - with better type support

Co-authored-by: Micael Levi L. Cavalcante <mllc@icomp.ufam.edu.br>
2022-12-12 17:45:03 +01:00
Konstantin Upir
ceea86d6ed fix(core): process exit before shutdown hook end 2022-12-12 12:37:08 +01:00
Tolga Paksoy
363ab79b5f chore(platform): use type imports 2022-12-07 17:32:39 +01:00
Tolga Paksoy
9efb84fc22 refactor(platform): add example and simplify types 2022-12-07 17:32:36 +01:00
Tolga Paksoy
1b90339d8b fix(core): remove empty optional adapter method 2022-12-07 17:05:00 +01:00
Tolga Paksoy
a25bfa86ed refactor(platform): extract platform implementation detail to interfaces 2022-12-05 18:03:10 +01:00
Tolga Paksoy
d5e1cd18fc refactor(fastify): re-use use body parser function 2022-12-05 18:03:08 +01:00
Tolga Paksoy
9b073748cb test(): add integration test for raw body 2022-12-05 17:52:11 +01:00
Tolga Paksoy
0444f9c89e feat(platform): enable body parser with raw body support 2022-12-05 17:52:08 +01:00
e-dot
1b9bb76d07 Merge branch 'master' of https://github.com/e-dot/nest 2022-12-02 18:50:17 +01:00
e-dot
c817014c7d Windows IIS Compatibility : listen(FastifyListenOptions)
Adapt the fastify adapter to be compatible with Microsoft IIS : requires the new listen() API with a FastifyListenOptions parameter
2022-12-02 18:48:21 +01:00
e-dot
accb1dc014 Adapt Fastify Adapter (!) to use listen options
Improve the fastify adapter to use an object (FastifyListenOptions) in order to be able to totally configure fastify. In particular, enable use of "named pipes" for port, and render nestJs compatible with IIS and IISNode on Windows.
2022-11-30 10:08:24 +01:00
e-dot
1755537a77 Improve type checking
Use class FastifyListenOptions to restrict type checking
And check that one of the attributes is really defined (path port or host) in order to use it as the listen option
2022-11-29 16:23:44 +01:00
e-dot
7b81d0571d Merge pull request #1 from e-dot/e-dot-patch-1
Compatibility with Microsoft IIS, iisnode and fastify
2022-11-25 18:01:59 +01:00
e-dot
7afed825be Compatibility with Microsoft IIS, iisnode and fastify
To enable compatibilty with iisnode and fastify, you need to enable use of an object - passed as-is to the fastify listen function (see https://www.fastify.io/docs/latest/Reference/Server/#listen)
2022-11-25 17:56:51 +01:00
35 changed files with 659 additions and 131 deletions

View File

@@ -0,0 +1,95 @@
import { NestExpressApplication } from '@nestjs/platform-express';
import { Test } from '@nestjs/testing';
import { OptionsUrlencoded } from 'body-parser';
import { expect } from 'chai';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Body Parser (Express Application)', () => {
const moduleFixture = Test.createTestingModule({
imports: [AppModule],
});
let app: NestExpressApplication;
afterEach(async () => {
await app.close();
});
describe('application/json', () => {
const stringLimit = '{ "msg": "Hello, World" }';
const stringOverLimit = '{ "msg": "Hello, World!" }';
beforeEach(async () => {
const testFixture = await moduleFixture.compile();
app = testFixture
.createNestApplication<NestExpressApplication>({
rawBody: true,
logger: false,
})
.useBodyParser('json', { limit: Buffer.from(stringLimit).byteLength });
await app.init();
});
it('should allow request with matching body limit', async () => {
const response = await request(app.getHttpServer())
.post('/')
.set('Content-Type', 'application/json')
.send(stringLimit)
.expect(201);
expect(response.body).to.eql({
raw: stringLimit,
});
});
it('should fail if post body is larger than limit', async () => {
await request(app.getHttpServer())
.post('/')
.set('Content-Type', 'application/json')
.send(stringOverLimit)
.expect(413);
});
});
describe('application/x-www-form-urlencoded', () => {
const stringLimit = 'msg=Hello, World';
const stringOverLimit = 'msg=Hello, World!';
beforeEach(async () => {
const testFixture = await moduleFixture.compile();
app = testFixture
.createNestApplication<NestExpressApplication>({
rawBody: true,
logger: false,
})
.useBodyParser<OptionsUrlencoded>('urlencoded', {
limit: Buffer.from(stringLimit).byteLength,
extended: true,
});
await app.init();
});
it('should allow request with matching body limit', async () => {
const response = await request(app.getHttpServer())
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(stringLimit)
.expect(201);
expect(response.body).to.eql({
raw: stringLimit,
});
});
it('should fail if post body is larger than limit', async () => {
await request(app.getHttpServer())
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(stringOverLimit)
.expect(413);
});
});
});

View File

@@ -0,0 +1,106 @@
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { AppModule } from '../src/app.module';
describe('Body Parser (Fastify Application)', () => {
const moduleFixture = Test.createTestingModule({
imports: [AppModule],
});
let app: NestFastifyApplication;
afterEach(async () => {
await app.close();
});
describe('application/json', () => {
const stringLimit = '{ "msg": "Hello, World" }';
const stringOverLimit = '{ "msg": "Hello, World!" }';
beforeEach(async () => {
const testFixture = await moduleFixture.compile();
app = testFixture
.createNestApplication<NestFastifyApplication>(new FastifyAdapter(), {
rawBody: true,
logger: false,
})
.useBodyParser('application/json', {
bodyLimit: Buffer.from(stringLimit).byteLength,
});
await app.init();
});
it('should allow request with matching body limit', async () => {
const response = await app.inject({
method: 'POST',
url: '/',
headers: { 'content-type': 'application/json' },
payload: stringLimit,
});
expect(JSON.parse(response.body)).to.eql({
raw: stringLimit,
});
});
it('should fail if post body is larger than limit', async () => {
const response = await app.inject({
method: 'POST',
url: '/',
headers: { 'content-type': 'application/json' },
payload: stringOverLimit,
});
expect(response.statusCode).to.equal(413);
});
});
describe('application/x-www-form-urlencoded', () => {
const stringLimit = 'msg=Hello, World';
const stringOverLimit = 'msg=Hello, World!';
beforeEach(async () => {
const testFixture = await moduleFixture.compile();
app = testFixture
.createNestApplication<NestFastifyApplication>(new FastifyAdapter(), {
rawBody: true,
logger: false,
})
.useBodyParser('application/x-www-form-urlencoded', {
bodyLimit: Buffer.from(stringLimit).byteLength,
});
await app.init();
});
it('should allow request with matching body limit', async () => {
const response = await app.inject({
method: 'POST',
url: '/',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
payload: stringLimit,
});
expect(JSON.parse(response.body)).to.eql({
raw: stringLimit,
});
});
it('should fail if post body is larger than limit', async () => {
const response = await app.inject({
method: 'POST',
url: '/',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
payload: stringOverLimit,
});
expect(response.statusCode).to.equal(413);
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Post, Req, RawBodyRequest } from '@nestjs/common';
import { IncomingMessage } from 'http';
@Controller()
export class AppController {
@Post()
index(@Req() req: RawBodyRequest<IncomingMessage>) {
return {
raw: req.rawBody?.toString(),
};
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
controllers: [AppController],
})
export class AppModule {}

View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"lib": ["dom"],
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist"
},
"include": [
"src/**/*",
"e2e/**/*"
],
"exclude": [
"node_modules"
]
}

View File

@@ -30,6 +30,7 @@ export interface HttpServer<TRequest = any, TResponse = any> {
| RequestHandler<TRequest, TResponse>
| ErrorHandler<TRequest, TResponse>,
): any;
useBodyParser?(...args: any[]): any;
get(handler: RequestHandler<TRequest, TResponse>): any;
get(path: string, handler: RequestHandler<TRequest, TResponse>): any;
post(handler: RequestHandler<TRequest, TResponse>): any;

View File

@@ -205,7 +205,7 @@ export class ConfigurableModuleBuilder<
static [self.staticMethodKey](
options: ModuleOptions & ExtraModuleDefinitionOptions,
): DynamicModule {
const providers = [
const providers: Array<Provider> = [
{
provide: self.options.optionsInjectionToken,
useValue: this.omitExtras(options, self.extras),

View File

@@ -1,5 +1,4 @@
import { Injectable } from '../decorators/core/injectable.decorator';
import { Optional } from '../decorators/core/optional.decorator';
import { Injectable, Optional } from '../decorators/core';
import { isObject } from '../utils/shared.utils';
import { ConsoleLogger } from './console-logger.service';
import { isLogLevelEnabled } from './utils';

View File

@@ -9,16 +9,16 @@ import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { lastValueFrom, isObservable } from 'rxjs';
import { ExternalExceptionFilterContext } from '../exceptions/external-exception-filter-context';
import { FORBIDDEN_MESSAGE } from '../guards/constants';
import { GuardsConsumer } from '../guards/guards-consumer';
import { GuardsContextCreator } from '../guards/guards-context-creator';
import { GuardsConsumer, GuardsContextCreator } from '../guards';
import { STATIC_CONTEXT } from '../injector/constants';
import { NestContainer } from '../injector/container';
import { ContextId } from '../injector/instance-wrapper';
import { ModulesContainer } from '../injector/modules-container';
import { InterceptorsConsumer } from '../interceptors/interceptors-consumer';
import { InterceptorsContextCreator } from '../interceptors/interceptors-context-creator';
import { PipesConsumer } from '../pipes/pipes-consumer';
import { PipesContextCreator } from '../pipes/pipes-context-creator';
import {
InterceptorsConsumer,
InterceptorsContextCreator,
} from '../interceptors';
import { PipesConsumer, PipesContextCreator } from '../pipes';
import { ContextUtils, ParamProperties } from './context-utils';
import { ExternalErrorProxy } from './external-proxy';
import { HandlerMetadataStorage } from './handler-metadata-storage';

View File

@@ -9,7 +9,7 @@ import {
isUndefined,
} from '@nestjs/common/utils/shared.utils';
import { iterate } from 'iterare';
import { DeterministicUuidRegistry } from '../inspector/deterministic-uuid-registry';
import { UuidFactory } from '../inspector/uuid-factory';
import { STATIC_CONTEXT } from './constants';
import {
isClassProvider,
@@ -469,6 +469,6 @@ export class InstanceWrapper<T = any> {
let key = this.name?.toString() ?? this.token?.toString();
key += this.host?.name ?? '';
return key ? DeterministicUuidRegistry.get(key) : randomStringGenerator();
return key ? UuidFactory.get(key) : randomStringGenerator();
}
}

View File

@@ -30,11 +30,11 @@ import {
import { createContextId } from '../helpers/context-id-factory';
import { getClassScope } from '../helpers/get-class-scope';
import { isDurable } from '../helpers/is-durable';
import { DeterministicUuidRegistry } from '../inspector/deterministic-uuid-registry';
import { UuidFactory } from '../inspector/uuid-factory';
import { CONTROLLER_ID_KEY } from './constants';
import { NestContainer } from './container';
import { InstanceWrapper } from './instance-wrapper';
import { ModuleRefGetOrResolveOpts, ModuleRef } from './module-ref';
import { ModuleRef, ModuleRefGetOrResolveOpts } from './module-ref';
/**
* @note
@@ -626,8 +626,6 @@ export class Module {
private generateUuid(): string {
const UUID_NAMESPACE = 'fb848993-0c82-4b9e-ae95-3c3c1dbe3d6b';
const key = this.name?.toString() ?? this.token?.toString();
return key
? DeterministicUuidRegistry.get(key, UUID_NAMESPACE)
: randomStringGenerator();
return key ? UuidFactory.get(key, UUID_NAMESPACE) : randomStringGenerator();
}
}

View File

@@ -0,0 +1,21 @@
import { randomStringGenerator } from '../../common/utils/random-string-generator.util';
import { DeterministicUuidRegistry } from './deterministic-uuid-registry';
export enum UuidFactoryMode {
Random = 'random',
Deterministic = 'deterministic',
}
export class UuidFactory {
private static _mode: UuidFactoryMode = UuidFactoryMode.Random;
static set mode(value: UuidFactoryMode) {
this._mode = value;
}
static get(key: string = '', namespace?: string) {
return this._mode === UuidFactoryMode.Deterministic
? DeterministicUuidRegistry.get(key, namespace)
: randomStringGenerator();
}
}

View File

@@ -4,36 +4,84 @@ import {
isFunction,
isNil,
} from '@nestjs/common/utils/shared.utils';
import { iterate } from 'iterare';
export class MetadataScanner {
public scanFromPrototype<T extends Injectable, R = any>(
instance: T,
prototype: object,
prototype: object | null,
callback: (name: string) => R,
): R[] {
const methodNames = new Set(this.getAllFilteredMethodNames(prototype));
return iterate(methodNames)
.map(callback)
.filter(metadata => !isNil(metadata))
.toArray();
}
if (!prototype) {
return [];
}
const visitedNames = new Map<string, boolean>();
const result: R[] = [];
*getAllFilteredMethodNames(prototype: object): IterableIterator<string> {
const isMethod = (prop: string) => {
const descriptor = Object.getOwnPropertyDescriptor(prototype, prop);
if (descriptor.set || descriptor.get) {
return false;
}
return !isConstructor(prop) && isFunction(prototype[prop]);
};
do {
yield* iterate(Object.getOwnPropertyNames(prototype))
.filter(isMethod)
.toArray();
for (const property of Object.getOwnPropertyNames(prototype)) {
if (visitedNames.has(property)) {
continue;
}
visitedNames.set(property, true);
const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
if (
descriptor.set ||
descriptor.get ||
isConstructor(property) ||
!isFunction(prototype[property])
) {
continue;
}
const value = callback(property);
if (isNil(value)) {
continue;
}
result.push(value);
}
} while (
(prototype = Reflect.getPrototypeOf(prototype)) &&
prototype !== Object.prototype
);
return result;
}
*getAllFilteredMethodNames(
prototype: object | null,
): IterableIterator<string> {
if (!prototype) {
return [];
}
const visitedNames = new Map<string, boolean>();
const result: string[] = [];
do {
for (const property of Object.getOwnPropertyNames(prototype)) {
if (visitedNames.has(property)) {
continue;
}
visitedNames.set(property, true);
const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
if (
descriptor.set ||
descriptor.get ||
isConstructor(property) ||
!isFunction(prototype[property])
) {
continue;
}
result.push(property);
}
} while (
(prototype = Reflect.getPrototypeOf(prototype)) &&
prototype !== Object.prototype
);
return result.values();
}
}

View File

@@ -18,7 +18,7 @@ import { ExecutionContextHost } from '../helpers/execution-context-host';
import { STATIC_CONTEXT } from '../injector/constants';
import { NestContainer } from '../injector/container';
import { Injector } from '../injector/injector';
import { InstanceWrapper } from '../injector/instance-wrapper';
import { ContextId, InstanceWrapper } from '../injector/instance-wrapper';
import { InstanceToken, Module } from '../injector/module';
import { GraphInspector } from '../inspector/graph-inspector';
import {
@@ -250,6 +250,9 @@ export class MiddlewareModule<
const proxy = await this.createProxy(instance);
return this.registerHandler(applicationRef, routeInfo, proxy);
}
const isTreeDurable = wrapper.isDependencyTreeDurable();
await this.registerHandler(
applicationRef,
routeInfo,
@@ -259,19 +262,7 @@ export class MiddlewareModule<
next: () => void,
) => {
try {
const contextId = ContextIdFactory.getByRequest(req);
if (!req[REQUEST_CONTEXT_ID]) {
Object.defineProperty(req, REQUEST_CONTEXT_ID, {
value: contextId,
enumerable: false,
writable: false,
configurable: false,
});
this.container.registerRequestProvider(
contextId.getParent ? contextId.payload : req,
contextId,
);
}
const contextId = this.getContextId(req, isTreeDurable);
const contextInstance = await this.injector.loadPerContext(
instance,
moduleRef,
@@ -325,10 +316,10 @@ export class MiddlewareModule<
) {
const prefix = this.config.getGlobalPrefix();
const excludedRoutes = this.config.getGlobalPrefixOptions().exclude;
if (
(Array.isArray(excludedRoutes) &&
isRouteExcluded(excludedRoutes, path, method)) ||
['*', '/*', '(.*)', '/(.*)'].includes(path)
Array.isArray(excludedRoutes) &&
isRouteExcluded(excludedRoutes, path, method)
) {
path = addLeadingSlash(path);
} else {
@@ -352,20 +343,40 @@ export class MiddlewareModule<
const isMethodAll = isRequestMethodAll(method);
const requestMethod = RequestMethod[method];
const router = await applicationRef.createMiddlewareFactory(method);
router(
path,
isMethodAll
? proxy
: <TRequest, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
if (applicationRef.getRequestMethod(req) === requestMethod) {
return proxy(req, res, next);
}
return next();
},
);
const middlewareFunction = isMethodAll
? proxy
: <TRequest, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
if (applicationRef.getRequestMethod(req) === requestMethod) {
return proxy(req, res, next);
}
return next();
};
const isAWildcard = ['*', '/*', '(.*)', '/(.*)'].includes(path);
if (isAWildcard && Array.isArray(excludedRoutes)) {
excludedRoutes.forEach(route => router(route.path, middlewareFunction));
}
router(path, middlewareFunction);
}
private getContextId(request: unknown, isTreeDurable: boolean): ContextId {
const contextId = ContextIdFactory.getByRequest(request);
if (!request[REQUEST_CONTEXT_ID]) {
Object.defineProperty(request, REQUEST_CONTEXT_ID, {
value: contextId,
enumerable: false,
writable: false,
configurable: false,
});
const requestProviderValue = isTreeDurable ? contextId.payload : request;
this.container.registerRequestProvider(requestProviderValue, contextId);
}
return contextId;
}
}

View File

@@ -13,6 +13,7 @@ export const mapToExcludeRoute = (
return routes.map(({ path, method }) => ({
pathRegex: pathToRegexp(path),
requestMethod: method,
path,
}));
};

View File

@@ -323,13 +323,20 @@ export class NestApplicationContext<
* @param {string[]} signals The system signals it should listen to
*/
protected listenToShutdownSignals(signals: string[]) {
let receivedSignal = false;
const cleanup = async (signal: string) => {
try {
signals.forEach(sig => process.removeListener(sig, cleanup));
if (receivedSignal) {
// If we receive another signal while we're waiting
// for the server to stop, just ignore it.
return;
}
receivedSignal = true;
await this.callDestroyHook();
await this.callBeforeShutdownHook(signal);
await this.dispose();
await this.callShutdownHook(signal);
signals.forEach(sig => process.removeListener(sig, cleanup));
process.kill(process.pid, signal);
} catch (err) {
Logger.error(

View File

@@ -266,6 +266,20 @@ export class NestApplication
return this;
}
public useBodyParser(...args: [any, any?]): this {
if (!('useBodyParser' in this.httpAdapter)) {
this.logger.warn('Your HTTP Adapter does not support `.useBodyParser`.');
return this;
}
const [parserType, ...otherArgs] = args;
const rawBody = !!this.appOptions.rawBody;
this.httpAdapter.useBodyParser(...[parserType, rawBody, ...otherArgs]);
return this;
}
public enableCors(options?: CorsOptions | CorsOptionsDelegate<any>): void {
this.httpAdapter.enableCors(options);
}
@@ -364,11 +378,13 @@ export class NestApplication
return {
requestMethod: RequestMethod.ALL,
pathRegex: pathToRegexp(addLeadingSlash(route)),
path: route,
};
}
return {
requestMethod: route.method,
pathRegex: pathToRegexp(addLeadingSlash(route.path)),
path: route.path,
};
},
);

View File

@@ -21,6 +21,7 @@ import { Injector } from './injector/injector';
import { InstanceLoader } from './injector/instance-loader';
import { GraphInspector } from './inspector/graph-inspector';
import { NoopGraphInspector } from './inspector/noop-graph-inspector';
import { UuidFactory, UuidFactoryMode } from './inspector/uuid-factory';
import { MetadataScanner } from './metadata-scanner';
import { NestApplication } from './nest-application';
import { NestApplicationContext } from './nest-application-context';
@@ -195,6 +196,10 @@ export class NestFactoryStatic {
options: NestApplicationContextOptions = {},
httpServer: HttpServer = null,
) {
UuidFactory.mode = options.snapshot
? UuidFactoryMode.Deterministic
: UuidFactoryMode.Random;
const injector = new Injector({ preview: options.preview });
const instanceLoader = new InstanceLoader(
container,

View File

@@ -1,6 +1,7 @@
import { RequestMethod } from '@nestjs/common';
export interface ExcludeRouteMetadata {
path: string;
/**
* Regular expression representing the route path.
*/

View File

@@ -13,8 +13,7 @@ import {
import * as pathToRegexp from 'path-to-regexp';
import { ApplicationConfig } from '../application-config';
import { UnknownRequestMappingException } from '../errors/exceptions/unknown-request-mapping.exception';
import { GuardsConsumer } from '../guards/guards-consumer';
import { GuardsContextCreator } from '../guards/guards-context-creator';
import { GuardsConsumer, GuardsContextCreator } from '../guards';
import { ContextIdFactory } from '../helpers/context-id-factory';
import { ExecutionContextHost } from '../helpers/execution-context-host';
import {
@@ -27,16 +26,17 @@ import { NestContainer } from '../injector/container';
import { Injector } from '../injector/injector';
import { ContextId, InstanceWrapper } from '../injector/instance-wrapper';
import { Module } from '../injector/module';
import {
InterceptorsConsumer,
InterceptorsContextCreator,
} from '../interceptors';
import { GraphInspector } from '../inspector/graph-inspector';
import {
Entrypoint,
HttpEntrypointMetadata,
} from '../inspector/interfaces/entrypoint.interface';
import { InterceptorsConsumer } from '../interceptors/interceptors-consumer';
import { InterceptorsContextCreator } from '../interceptors/interceptors-context-creator';
import { MetadataScanner } from '../metadata-scanner';
import { PipesConsumer } from '../pipes/pipes-consumer';
import { PipesContextCreator } from '../pipes/pipes-context-creator';
import { PipesConsumer, PipesContextCreator } from '../pipes';
import { ExceptionsFilter } from './interfaces/exceptions-filter.interface';
import { RoutePathMetadata } from './interfaces/route-path-metadata.interface';
import { PathsExplorer } from './paths-explorer';
@@ -355,13 +355,16 @@ export class RouterExplorer {
) {
const { instance } = instanceWrapper;
const collection = moduleRef.controllers;
const isTreeDurable = instanceWrapper.isDependencyTreeDurable();
return async <TRequest extends Record<any, any>, TResponse>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
try {
const contextId = this.getContextId(req);
const contextId = this.getContextId(req, isTreeDurable);
const contextInstance = await this.injector.loadPerContext(
instance,
moduleRef,
@@ -397,6 +400,7 @@ export class RouterExplorer {
private getContextId<T extends Record<any, unknown> = any>(
request: T,
isTreeDurable: boolean,
): ContextId {
const contextId = ContextIdFactory.getByRequest(request);
if (!request[REQUEST_CONTEXT_ID as any]) {
@@ -406,10 +410,9 @@ export class RouterExplorer {
writable: false,
configurable: false,
});
this.container.registerRequestProvider(
contextId.getParent ? contextId.payload : request,
contextId,
);
const requestProviderValue = isTreeDurable ? contextId.payload : request;
this.container.registerRequestProvider(requestProviderValue, contextId);
}
return contextId;
}

View File

@@ -23,10 +23,11 @@ import {
PipeTransform,
Scope,
ValueProvider,
Controller,
Injectable,
Type,
} from '@nestjs/common/interfaces';
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface';
import { Injectable } from '@nestjs/common/interfaces/injectable.interface';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import {
isFunction,
isNil,
@@ -50,8 +51,8 @@ import { NestContainer } from './injector/container';
import { InstanceWrapper } from './injector/instance-wrapper';
import { InternalCoreModuleFactory } from './injector/internal-core-module/internal-core-module-factory';
import { Module } from './injector/module';
import { DeterministicUuidRegistry } from './inspector/deterministic-uuid-registry';
import { GraphInspector } from './inspector/graph-inspector';
import { UuidFactory } from './inspector/uuid-factory';
import { MetadataScanner } from './metadata-scanner';
interface ApplicationProviderWrapper {
@@ -410,7 +411,7 @@ export class DependenciesScanner {
if (!providersKeys.includes(type as string)) {
return this.container.addProvider(provider as any, token);
}
const uuid = DeterministicUuidRegistry.get(type.toString());
const uuid = UuidFactory.get(type.toString());
const providerToken = `${type as string} (UUID: ${uuid})`;
let scope = (provider as ClassProvider | FactoryProvider).scope;

View File

@@ -23,7 +23,11 @@ describe('ApplicationConfig', () => {
it('should set global path options', () => {
const options: GlobalPrefixOptions<ExcludeRouteMetadata> = {
exclude: [
{ pathRegex: new RegExp(/health/), requestMethod: RequestMethod.GET },
{
pathRegex: new RegExp(/health/),
requestMethod: RequestMethod.GET,
path: 'health',
},
],
};
appConfig.setGlobalPrefixOptions(options);

View File

@@ -6,6 +6,7 @@ import { Injector } from '../injector/injector';
import { InstanceLoader } from '../injector/instance-loader';
import { GraphInspector } from '../inspector/graph-inspector';
import { NestApplicationContext } from '../nest-application-context';
import * as sinon from 'sinon';
describe('NestApplicationContext', () => {
class A {}
@@ -49,6 +50,54 @@ describe('NestApplicationContext', () => {
return applicationContext;
}
describe('listenToShutdownSignals', () => {
it('shutdown process should not be interrupted by another handler', async () => {
const signal = 'SIGTERM';
let processUp = true;
let promisesResolved = false;
const applicationContext = await testHelper(A, Scope.DEFAULT);
applicationContext.enableShutdownHooks([signal]);
const waitProcessDown = new Promise(resolve => {
const shutdownCleanupRef = applicationContext['shutdownCleanupRef'];
const handler = () => {
if (
!process
.listeners(signal)
.find(handler => handler == shutdownCleanupRef)
) {
processUp = false;
process.removeListener(signal, handler);
resolve(undefined);
}
return undefined;
};
process.on(signal, handler);
});
// add some third party handler
process.on(signal, signal => {
// do some work
process.kill(process.pid, signal);
});
const hookStub = sinon
.stub(applicationContext as any, 'callShutdownHook')
.callsFake(async () => {
// run some async code
await new Promise(resolve => setImmediate(() => resolve(undefined)));
if (processUp) {
promisesResolved = true;
}
});
process.kill(process.pid, signal);
await waitProcessDown;
hookStub.restore();
expect(processUp).to.be.false;
expect(promisesResolved).to.be.true;
});
});
describe('get', () => {
describe('when scope = DEFAULT', () => {
it('should get value with function injection key', async () => {

View File

@@ -249,6 +249,7 @@ describe('RoutePathFactory', () => {
{
pathRegex: pathToRegexp('/random'),
requestMethod: RequestMethod.ALL,
path: '/random',
},
],
});
@@ -267,6 +268,7 @@ describe('RoutePathFactory', () => {
{
pathRegex: pathToRegexp('/cats'),
requestMethod: RequestMethod.ALL,
path: '/cats',
},
],
});
@@ -285,6 +287,7 @@ describe('RoutePathFactory', () => {
{
pathRegex: pathToRegexp('/cats'),
requestMethod: RequestMethod.GET,
path: '/cats',
},
],
});

View File

@@ -205,13 +205,15 @@ export class ListenersController {
const collection = moduleRef.controllers;
const { instance } = wrapper;
const isTreeDurable = wrapper.isDependencyTreeDurable();
const requestScopedHandler: MessageHandler = async (...args: unknown[]) => {
try {
let contextId: ContextId;
let [dataOrContextHost] = args;
if (dataOrContextHost instanceof RequestContextHost) {
contextId = this.getContextId(dataOrContextHost);
contextId = this.getContextId(dataOrContextHost, isTreeDurable);
args.shift();
} else {
const [data, reqCtx] = args;
@@ -220,11 +222,7 @@ export class ListenersController {
data,
reqCtx as BaseRpcContext,
);
contextId = this.getContextId(request);
this.container.registerRequestProvider(
contextId.getParent ? contextId.payload : request,
contextId,
);
contextId = this.getContextId(request, isTreeDurable);
dataOrContextHost = request;
}
const contextInstance = await this.injector.loadPerContext(
@@ -270,7 +268,10 @@ export class ListenersController {
return requestScopedHandler;
}
private getContextId<T extends RequestContext = any>(request: T): ContextId {
private getContextId<T extends RequestContext = any>(
request: T,
isTreeDurable: boolean,
): ContextId {
const contextId = ContextIdFactory.getByRequest(request);
if (!request[REQUEST_CONTEXT_ID as any]) {
Object.defineProperty(request, REQUEST_CONTEXT_ID, {
@@ -279,10 +280,9 @@ export class ListenersController {
writable: false,
configurable: false,
});
this.container.registerRequestProvider(
contextId.getParent ? contextId.payload : request,
contextId,
);
const requestProviderValue = isTreeDurable ? contextId.payload : request;
this.container.registerRequestProvider(requestProviderValue, contextId);
}
return contextId;
}

View File

@@ -29,11 +29,13 @@ import {
OptionsUrlencoded,
urlencoded as bodyParserUrlencoded,
} from 'body-parser';
import * as bodyparser from 'body-parser';
import * as cors from 'cors';
import * as express from 'express';
import * as http from 'http';
import * as https from 'https';
import { Duplex, pipeline } from 'stream';
import { NestExpressBodyParserOptions } from '../interfaces/nest-express-body-parser-options.interface';
import { ServeStaticOptions } from '../interfaces/serve-static-options.interface';
import { getBodyParserOptions } from './utils/get-body-parser-options.util';
@@ -235,6 +237,19 @@ export class ExpressAdapter extends AbstractHttpAdapter {
.forEach(parserKey => this.use(parserMiddleware[parserKey]));
}
public useBodyParser<Options extends bodyparser.Options = bodyparser.Options>(
type: keyof bodyparser.BodyParser,
rawBody: boolean,
options?: NestExpressBodyParserOptions<Options>,
): this {
const parserOptions = getBodyParserOptions(rawBody, options || {});
const parser = bodyparser[type](parserOptions);
this.use(parser);
return this;
}
public setLocal(key: string, value: any) {
this.instance.locals[key] = value;
return this;

View File

@@ -1 +1,2 @@
export * from './nest-express-application.interface';
export * from './nest-express-body-parser-options.interface';

View File

@@ -1,5 +1,7 @@
import { Server } from 'net';
import { INestApplication } from '@nestjs/common';
import * as bodyparser from 'body-parser';
import { NestExpressBodyParserOptions } from './nest-express-body-parser-options.interface';
import { ServeStaticOptions } from './serve-static-options.interface';
/**
@@ -73,6 +75,24 @@ export interface NestExpressApplication extends INestApplication {
*/
useStaticAssets(path: string, options?: ServeStaticOptions): this;
/**
* Register Express body parsers on the fly. Will respect
* the application's `rawBody` option.
*
* @example
* const app = await NestFactory.create<NestExpressApplication>(
* AppModule,
* { rawBody: true }
* );
* app.useBodyParser('json', { limit: '50mb' });
*
* @returns {this}
*/
useBodyParser<Options extends bodyparser.Options = bodyparser.Options>(
parser: keyof bodyparser.BodyParser,
options?: NestExpressBodyParserOptions<Options>,
): this;
/**
* Sets one or multiple base directories for templates (views).
*

View File

@@ -0,0 +1,6 @@
import type { Options } from 'body-parser';
export type NestExpressBodyParserOptions<T extends Options = Options> = Omit<
T,
'verify'
>;

View File

@@ -18,9 +18,10 @@ import { isString, isUndefined } from '@nestjs/common/utils/shared.utils';
import { AbstractHttpAdapter } from '@nestjs/core/adapters/http-adapter';
import {
fastify,
FastifyBaseLogger,
FastifyBodyParser,
FastifyInstance,
FastifyBaseLogger,
FastifyListenOptions,
FastifyPluginAsync,
FastifyPluginCallback,
FastifyRegister,
@@ -45,6 +46,7 @@ import {
} from 'light-my-request';
// `querystring` is used internally in fastify for registering urlencoded body parser.
import { parse as querystringParse } from 'querystring';
import { NestFastifyBodyParserOptions } from '../interfaces';
import {
FastifyStaticOptions,
FastifyViewOptions,
@@ -227,12 +229,27 @@ export class FastifyAdapter<
hostname: string,
callback?: () => void,
): void;
public listen(port: string | number, ...args: any[]): void {
public listen(
listenOptions: string | number | FastifyListenOptions,
...args: any[]
): void {
const isFirstArgTypeofFunction = typeof args[0] === 'function';
const callback = isFirstArgTypeofFunction ? args[0] : args[1];
const options: Record<string, any> = {
port: +port,
};
let options: Record<string, any>;
if (
typeof listenOptions === 'object' &&
(listenOptions.host !== undefined ||
listenOptions.port !== undefined ||
listenOptions.path !== undefined)
) {
// First parameter is an object with a path, port and/or host attributes
options = listenOptions;
} else {
options = {
port: +listenOptions,
};
}
if (!isFirstArgTypeofFunction) {
options.host = args[0];
}
@@ -465,6 +482,43 @@ export class FastifyAdapter<
this._isParserRegistered = true;
}
public useBodyParser(
type: string | string[] | RegExp,
rawBody: boolean,
options?: NestFastifyBodyParserOptions,
parser?: FastifyBodyParser<Buffer, TServer>,
) {
const parserOptions = {
...(options || {}),
parseAs: 'buffer' as const,
};
this.getInstance().addContentTypeParser<Buffer>(
type,
parserOptions,
(
req: RawBodyRequest<FastifyRequest<unknown, TServer, TRawRequest>>,
body: Buffer,
done,
) => {
if (rawBody === true && Buffer.isBuffer(body)) {
req.rawBody = body;
}
if (parser) {
parser(req, body, done);
return;
}
done(null, body);
},
);
// To avoid the Nest application init to override our custom
// body parser, we mark the parsers as registered.
this._isParserRegistered = true;
}
public async createMiddlewareFactory(
requestMethod: RequestMethod,
): Promise<(path: string, callback: Function) => any> {
@@ -510,20 +564,15 @@ export class FastifyAdapter<
}
private registerJsonContentParser(rawBody?: boolean) {
const contentType = 'application/json';
const withRawBody = !!rawBody;
const { bodyLimit } = this.getInstance().initialConfig;
this.getInstance().addContentTypeParser<Buffer>(
'application/json',
{ parseAs: 'buffer', bodyLimit },
(
req: RawBodyRequest<FastifyRequest<unknown, TServer, TRawRequest>>,
body: Buffer,
done,
) => {
if (rawBody === true && Buffer.isBuffer(body)) {
req.rawBody = body;
}
this.useBodyParser(
contentType,
withRawBody,
{ bodyLimit },
(req, body, done) => {
const { onProtoPoisoning, onConstructorPoisoning } =
this.instance.initialConfig;
const defaultJsonParser = this.instance.getDefaultJsonParser(
@@ -536,20 +585,15 @@ export class FastifyAdapter<
}
private registerUrlencodedContentParser(rawBody?: boolean) {
const contentType = 'application/x-www-form-urlencoded';
const withRawBody = !!rawBody;
const { bodyLimit } = this.getInstance().initialConfig;
this.getInstance().addContentTypeParser<Buffer>(
'application/x-www-form-urlencoded',
{ parseAs: 'buffer', bodyLimit },
(
req: RawBodyRequest<FastifyRequest<unknown, TServer, TRawRequest>>,
body: Buffer,
done,
) => {
if (rawBody === true && Buffer.isBuffer(body)) {
req.rawBody = body;
}
this.useBodyParser(
contentType,
withRawBody,
{ bodyLimit },
(_req, body, done) => {
done(null, querystringParse(body.toString()));
},
);

View File

@@ -1 +1,2 @@
export * from './nest-fastify-application.interface';
export * from './nest-fastify-body-parser-options.interface';

View File

@@ -1,10 +1,12 @@
import { INestApplication } from '@nestjs/common';
import {
FastifyBodyParser,
FastifyInstance,
FastifyPluginAsync,
FastifyPluginCallback,
FastifyPluginOptions,
FastifyRegisterOptions,
RawServerBase,
} from 'fastify';
import {
Chain as LightMyRequestChain,
@@ -12,6 +14,7 @@ import {
Response as LightMyRequestResponse,
} from 'light-my-request';
import { FastifyStaticOptions, FastifyViewOptions } from './external';
import { NestFastifyBodyParserOptions } from './nest-fastify-body-parser-options.interface';
export interface NestFastifyApplication extends INestApplication {
/**
@@ -28,6 +31,27 @@ export interface NestFastifyApplication extends INestApplication {
opts?: FastifyRegisterOptions<Options>,
): Promise<FastifyInstance>;
/**
* Register Fastify body parsers on the fly. Will respect
* the application's `rawBody` option.
*
* @example
* const app = await NestFactory.create<NestFastifyApplication>(
* AppModule,
* new FastifyAdapter(),
* { rawBody: true }
* );
* // enable the json parser with a parser limit of 50mb
* app.useBodyParser('application/json', { bodyLimit: 50 * 1000 * 1024 });
*
* @returns {this}
*/
useBodyParser<TServer extends RawServerBase = RawServerBase>(
type: string | string[] | RegExp,
options?: NestFastifyBodyParserOptions,
parser?: FastifyBodyParser<Buffer, TServer>,
): this;
/**
* Sets a base directory for public assets.
* Example `app.useStaticAssets({ root: 'public' })`

View File

@@ -0,0 +1,6 @@
import type { AddContentTypeParser } from 'fastify';
export type NestFastifyBodyParserOptions = Omit<
Parameters<AddContentTypeParser>[1],
'parseAs'
>;

View File

@@ -1,5 +1,4 @@
import { Logger, LoggerService, Module } from '@nestjs/common';
import { ModuleMetadata } from '@nestjs/common/interfaces';
import { Logger, LoggerService, Module, ModuleMetadata } from '@nestjs/common';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
import { ApplicationConfig } from '@nestjs/core/application-config';
import { NestContainer } from '@nestjs/core/injector/container';

View File

@@ -18,10 +18,11 @@ import {
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
import { HandlerMetadataStorage } from '@nestjs/core/helpers/handler-metadata-storage';
import { ParamsMetadata } from '@nestjs/core/helpers/interfaces';
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator';
import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer';
import { PipesContextCreator } from '@nestjs/core/pipes/pipes-context-creator';
import {
InterceptorsConsumer,
InterceptorsContextCreator,
} from '@nestjs/core/interceptors';
import { PipesConsumer, PipesContextCreator } from '@nestjs/core/pipes';
import { PARAM_ARGS_METADATA } from '../constants';
import { WsException } from '../errors/ws-exception';
import { WsParamsFactory } from '../factories/ws-params-factory';