mirror of
https://github.com/nestjs/nest.git
synced 2026-02-24 00:02:56 +00:00
Compare commits
197 Commits
H4ad-perf/
...
v9.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f57eacc952 | ||
|
|
6c99b704f6 | ||
|
|
0a3a87673a | ||
|
|
36065d30f2 | ||
|
|
d323edbab9 | ||
|
|
71c987e490 | ||
|
|
7b69554776 | ||
|
|
1069796df7 | ||
|
|
cf88a7814e | ||
|
|
bf1071f142 | ||
|
|
e1c2d27966 | ||
|
|
76b7185623 | ||
|
|
dac1219879 | ||
|
|
97e9c184a2 | ||
|
|
1d5856c731 | ||
|
|
c9c6468861 | ||
|
|
216ce4c60f | ||
|
|
6b39aaa00e | ||
|
|
f211dee41a | ||
|
|
593e6ffb1a | ||
|
|
ab25307fc2 | ||
|
|
5a6bc1899b | ||
|
|
51dab24dad | ||
|
|
19492a9974 | ||
|
|
3806a7631c | ||
|
|
7f43821d48 | ||
|
|
713ece9e88 | ||
|
|
422eeb9ffa | ||
|
|
ab7cbf35f5 | ||
|
|
b8702d5dbd | ||
|
|
5a333d4fa7 | ||
|
|
c3ad373f91 | ||
|
|
e92ae413f4 | ||
|
|
ac6fc6ba9f | ||
|
|
2124bc874c | ||
|
|
c79f35c37c | ||
|
|
b1d53e227b | ||
|
|
15e4a5f9f1 | ||
|
|
6ab4b4891c | ||
|
|
2d7703af06 | ||
|
|
caf056fba5 | ||
|
|
423b28a8bf | ||
|
|
fe745831d5 | ||
|
|
20b545bf48 | ||
|
|
b92e0e99c7 | ||
|
|
d93b77a6ab | ||
|
|
bb283861fd | ||
|
|
25d0c3f30f | ||
|
|
c6ebbf7cb8 | ||
|
|
b73501c1e3 | ||
|
|
0e3f9bd527 | ||
|
|
452a71bfcf | ||
|
|
dac07fafb6 | ||
|
|
c7ac493b53 | ||
|
|
7b7b8b9260 | ||
|
|
f285d7c954 | ||
|
|
c8e129b95c | ||
|
|
6d64d91122 | ||
|
|
491f132b8e | ||
|
|
255b9cb8a0 | ||
|
|
03efdce8d6 | ||
|
|
0e84b80138 | ||
|
|
2aa94cd2c1 | ||
|
|
1cc23f7d9d | ||
|
|
43d0c166d4 | ||
|
|
2ed509a0f6 | ||
|
|
9eb3b8903a | ||
|
|
4ad3cbc6a6 | ||
|
|
754d1daebc | ||
|
|
da708c7216 | ||
|
|
9141d245a8 | ||
|
|
77ec351c1f | ||
|
|
416ec6938a | ||
|
|
ae1fdfb52a | ||
|
|
29a18ef5d7 | ||
|
|
c46cb29638 | ||
|
|
8ce73f3544 | ||
|
|
ff2684c217 | ||
|
|
61fce51212 | ||
|
|
0b6e59ba46 | ||
|
|
cba648b166 | ||
|
|
dad667988c | ||
|
|
db7501f7b4 | ||
|
|
82930ee86d | ||
|
|
79773fe0ae | ||
|
|
2c1c2a0f61 | ||
|
|
412b60bf59 | ||
|
|
d860e44b8a | ||
|
|
e316f6cd0e | ||
|
|
e9b02dd833 | ||
|
|
32af34f226 | ||
|
|
664510a1b4 | ||
|
|
5e5828ba12 | ||
|
|
dd72da7294 | ||
|
|
3c932e33bc | ||
|
|
a86fafed14 | ||
|
|
a99846b817 | ||
|
|
d45b16e791 | ||
|
|
15ac489573 | ||
|
|
ccbb604fc2 | ||
|
|
1f327efd03 | ||
|
|
c6dbc05bb6 | ||
|
|
33fe1d0337 | ||
|
|
25dd621132 | ||
|
|
d9c394bb44 | ||
|
|
91f7190b07 | ||
|
|
0a4f9419d8 | ||
|
|
eba8f15683 | ||
|
|
2310c56f4c | ||
|
|
06ea451f1f | ||
|
|
b2c55a6f79 | ||
|
|
3d366ce5c5 | ||
|
|
f7e366600f | ||
|
|
55bc840edb | ||
|
|
22c3175552 | ||
|
|
c1815d2f8c | ||
|
|
34654244a7 | ||
|
|
2924c03aa3 | ||
|
|
d1ec9db5f2 | ||
|
|
cb7e10dbc3 | ||
|
|
511e7162a6 | ||
|
|
32b0d9c600 | ||
|
|
967a136834 | ||
|
|
1aadd30f46 | ||
|
|
ae76b521d6 | ||
|
|
38c365215a | ||
|
|
80b8c86c68 | ||
|
|
8756ed3d68 | ||
|
|
d616861698 | ||
|
|
c64312fded | ||
|
|
304e6bf5bb | ||
|
|
161198379a | ||
|
|
661104d981 | ||
|
|
f71554ca96 | ||
|
|
a986e7e907 | ||
|
|
920f430d4d | ||
|
|
cd0462fbcb | ||
|
|
b9910a400a | ||
|
|
6d88e3cd3c | ||
|
|
fbf1ab7a81 | ||
|
|
868bbcd8b9 | ||
|
|
4e783852b3 | ||
|
|
07a84ae224 | ||
|
|
7bc72671b6 | ||
|
|
a560c3466a | ||
|
|
f10c917b71 | ||
|
|
208d8ca184 | ||
|
|
32aeb7a8a3 | ||
|
|
a185e63281 | ||
|
|
0b395d1f59 | ||
|
|
f396dc1ddf | ||
|
|
aafdac62a0 | ||
|
|
e8944675cb | ||
|
|
4d4f3f82de | ||
|
|
994f8c4e42 | ||
|
|
58ee3bf2f3 | ||
|
|
5deabef549 | ||
|
|
9a47a6ce70 | ||
|
|
eee8968d82 | ||
|
|
35357053b1 | ||
|
|
ceea86d6ed | ||
|
|
452f19461c | ||
|
|
7792c43b93 | ||
|
|
363ab79b5f | ||
|
|
9efb84fc22 | ||
|
|
1b90339d8b | ||
|
|
a25bfa86ed | ||
|
|
d5e1cd18fc | ||
|
|
9b073748cb | ||
|
|
0444f9c89e | ||
|
|
1b9bb76d07 | ||
|
|
c817014c7d | ||
|
|
accb1dc014 | ||
|
|
1755537a77 | ||
|
|
7b81d0571d | ||
|
|
7afed825be | ||
|
|
a1f17e2271 | ||
|
|
ecba3bafde | ||
|
|
daa2054000 | ||
|
|
151a67b752 | ||
|
|
60772c39e1 | ||
|
|
7d1fa9a216 | ||
|
|
758cd2d8b1 | ||
|
|
fdc5f34219 | ||
|
|
d5f434ce75 | ||
|
|
4a27d263c5 | ||
|
|
cc7e829804 | ||
|
|
4c4c13bf84 | ||
|
|
d6097a10ac | ||
|
|
584015bc77 | ||
|
|
61d25beae0 | ||
|
|
ddf3301d49 | ||
|
|
3d1632693c | ||
|
|
c437ac97d0 | ||
|
|
b7f458f3fe | ||
|
|
75d5d64ec7 | ||
|
|
f3e6c54b3e |
@@ -1 +1,3 @@
|
||||
**/node_modules/**
|
||||
**/node_modules/**
|
||||
*.d.ts
|
||||
*.js
|
||||
@@ -11,7 +11,6 @@
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
|
||||
@@ -15,7 +15,7 @@ scalar Date
|
||||
|
||||
type Query {
|
||||
recipe(id: String!): Recipe!
|
||||
recipes(skip: Int = 0, take: Int = 25): [Recipe!]!
|
||||
recipes(skip: Int! = 0, take: Int! = 25): [Recipe!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
||||
@@ -1,39 +1,134 @@
|
||||
import { HttpStatus, INestApplication } from '@nestjs/common';
|
||||
import { HttpServer, HttpStatus, INestApplication } from '@nestjs/common';
|
||||
import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { expect } from 'chai';
|
||||
import * as request from 'supertest';
|
||||
import { ErrorsController } from '../src/errors/errors.controller';
|
||||
|
||||
describe('Error messages', () => {
|
||||
let server;
|
||||
let app: INestApplication;
|
||||
let server: HttpServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [ErrorsController],
|
||||
}).compile();
|
||||
describe('Express', () => {
|
||||
let app: INestApplication;
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [ErrorsController],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
server = app.getHttpServer();
|
||||
await app.init();
|
||||
});
|
||||
app = module.createNestApplication();
|
||||
server = app.getHttpServer();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`/GET`, () => {
|
||||
return request(server).get('/sync').expect(HttpStatus.BAD_REQUEST).expect({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Integration test',
|
||||
it(`/GET`, () => {
|
||||
return request(server)
|
||||
.get('/sync')
|
||||
.expect(HttpStatus.BAD_REQUEST)
|
||||
.expect({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Integration test',
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET (Promise/async)`, () => {
|
||||
return request(server)
|
||||
.get('/async')
|
||||
.expect(HttpStatus.BAD_REQUEST)
|
||||
.expect({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Integration test',
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET (InternalServerError despite custom content-type)`, async () => {
|
||||
return request(server)
|
||||
.get('/unexpected-error')
|
||||
.expect(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.expect({
|
||||
statusCode: 500,
|
||||
message: 'Internal server error',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET (Promise/async)`, () => {
|
||||
return request(server).get('/async').expect(HttpStatus.BAD_REQUEST).expect({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Integration test',
|
||||
});
|
||||
});
|
||||
describe('Fastify', () => {
|
||||
let app: NestFastifyApplication;
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [ErrorsController],
|
||||
}).compile();
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
app = module.createNestApplication<NestFastifyApplication>(
|
||||
new FastifyAdapter(),
|
||||
);
|
||||
server = app.getHttpServer();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it(`/GET`, async () => {
|
||||
return app
|
||||
.inject({
|
||||
method: 'GET',
|
||||
url: '/sync',
|
||||
})
|
||||
.then(({ payload, statusCode }) => {
|
||||
expect(statusCode).to.equal(HttpStatus.BAD_REQUEST);
|
||||
expect(payload).to.equal(
|
||||
JSON.stringify({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Integration test',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET (Promise/async)`, async () => {
|
||||
return app
|
||||
.inject({
|
||||
method: 'GET',
|
||||
url: '/sync',
|
||||
})
|
||||
.then(({ payload, statusCode }) => {
|
||||
expect(statusCode).to.equal(HttpStatus.BAD_REQUEST);
|
||||
expect(payload).to.equal(
|
||||
JSON.stringify({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'Integration test',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it(`/GET (InternalServerError despite custom content-type)`, async () => {
|
||||
return app
|
||||
.inject({
|
||||
method: 'GET',
|
||||
url: '/unexpected-error',
|
||||
})
|
||||
.then(({ payload, statusCode }) => {
|
||||
expect(statusCode).to.equal(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
expect(payload).to.equal(
|
||||
JSON.stringify({
|
||||
statusCode: 500,
|
||||
message: 'Internal server error',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,6 +77,26 @@ describe('Middleware', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using default URI versioning with the global prefix', () => {
|
||||
beforeEach(async () => {
|
||||
app = await createAppWithVersioning(
|
||||
{
|
||||
type: VersioningType.URI,
|
||||
defaultVersion: VERSION_NEUTRAL,
|
||||
},
|
||||
async (app: INestApplication) => {
|
||||
app.setGlobalPrefix('api');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it(`forRoutes({ path: '/versioned', version: '1', method: RequestMethod.ALL })`, () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/api/v1/versioned')
|
||||
.expect(200, VERSIONED_VALUE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using HEADER versioning', () => {
|
||||
beforeEach(async () => {
|
||||
app = await createAppWithVersioning({
|
||||
@@ -133,6 +153,7 @@ describe('Middleware', () => {
|
||||
|
||||
async function createAppWithVersioning(
|
||||
versioningOptions: VersioningOptions,
|
||||
beforeInit?: (app: INestApplication) => Promise<void>,
|
||||
): Promise<INestApplication> {
|
||||
const app = (
|
||||
await Test.createTestingModule({
|
||||
@@ -141,6 +162,9 @@ async function createAppWithVersioning(
|
||||
).createNestApplication();
|
||||
|
||||
app.enableVersioning(versioningOptions);
|
||||
if (beforeInit) {
|
||||
await beforeInit(app);
|
||||
}
|
||||
await app.init();
|
||||
|
||||
return app;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { AppModule } from '../src/app.module';
|
||||
const RETURN_VALUE = 'test';
|
||||
const SCOPED_VALUE = 'test_scoped';
|
||||
const WILDCARD_VALUE = 'test_wildcard';
|
||||
const EXCLUDE_VALUE = 'test_exclude';
|
||||
|
||||
@Controller()
|
||||
class TestController {
|
||||
@@ -24,6 +25,11 @@ class TestController {
|
||||
wildcard_nested() {
|
||||
return RETURN_VALUE;
|
||||
}
|
||||
|
||||
@Get('exclude')
|
||||
exclude() {
|
||||
return EXCLUDE_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
@@ -36,8 +42,10 @@ class TestModule {
|
||||
.apply((req, res, next) => res.send(WILDCARD_VALUE))
|
||||
.forRoutes('tests/*')
|
||||
.apply((req, res, next) => res.send(SCOPED_VALUE))
|
||||
.exclude('exclude')
|
||||
.forRoutes(TestController)
|
||||
.apply((req, res, next) => res.send(RETURN_VALUE))
|
||||
.exclude('exclude')
|
||||
.forRoutes('*');
|
||||
}
|
||||
}
|
||||
@@ -45,25 +53,34 @@ class TestModule {
|
||||
describe('Middleware', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = (
|
||||
await Test.createTestingModule({
|
||||
imports: [TestModule],
|
||||
}).compile()
|
||||
).createNestApplication();
|
||||
|
||||
await app.init();
|
||||
it(`forRoutes(*)`, async () => {
|
||||
app = await createApp();
|
||||
await request(app.getHttpServer()).get('/hello').expect(200, RETURN_VALUE);
|
||||
await request(app.getHttpServer())
|
||||
.get('/exclude')
|
||||
.expect(200, EXCLUDE_VALUE);
|
||||
});
|
||||
|
||||
it(`forRoutes(*)`, () => {
|
||||
return request(app.getHttpServer()).get('/hello').expect(200, RETURN_VALUE);
|
||||
it(`forRoutes(*) with global prefix`, async () => {
|
||||
app = await createApp(app => app.setGlobalPrefix('api'));
|
||||
await request(app.getHttpServer())
|
||||
.get('/api/hello')
|
||||
.expect(200, RETURN_VALUE);
|
||||
await request(app.getHttpServer())
|
||||
.get('/api/exclude')
|
||||
.expect(200, EXCLUDE_VALUE);
|
||||
});
|
||||
|
||||
it(`forRoutes(TestController)`, () => {
|
||||
return request(app.getHttpServer()).get('/test').expect(200, SCOPED_VALUE);
|
||||
it(`forRoutes(TestController)`, async () => {
|
||||
app = await createApp();
|
||||
await request(app.getHttpServer()).get('/test').expect(200, SCOPED_VALUE);
|
||||
await request(app.getHttpServer())
|
||||
.get('/exclude')
|
||||
.expect(200, EXCLUDE_VALUE);
|
||||
});
|
||||
|
||||
it(`forRoutes(tests/*)`, () => {
|
||||
it(`forRoutes(tests/*)`, async () => {
|
||||
app = await createApp();
|
||||
return request(app.getHttpServer())
|
||||
.get('/tests/wildcard')
|
||||
.expect(200, WILDCARD_VALUE);
|
||||
@@ -73,3 +90,20 @@ describe('Middleware', () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
async function createApp(
|
||||
beforeInit?: (app: INestApplication) => void,
|
||||
): Promise<INestApplication> {
|
||||
const app = (
|
||||
await Test.createTestingModule({
|
||||
imports: [TestModule],
|
||||
}).compile()
|
||||
).createNestApplication();
|
||||
|
||||
if (beforeInit) {
|
||||
beforeInit(app);
|
||||
}
|
||||
await app.init();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Controller, Get } from '@nestjs/common';
|
||||
import { BadRequestException, Controller, Get, Header } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class ErrorsController {
|
||||
@@ -12,6 +12,12 @@ export class ErrorsController {
|
||||
this.throwError();
|
||||
}
|
||||
|
||||
@Get('unexpected-error')
|
||||
@Header('Content-Type', 'application/pdf')
|
||||
unexpectedError() {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
throwError() {
|
||||
throw new BadRequestException({
|
||||
statusCode: 400,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -119,6 +119,17 @@ describe('Global prefix', () => {
|
||||
await request(server).get('/api/v1/middleware/foo').expect(404);
|
||||
});
|
||||
|
||||
it(`should get the params in the global prefix`, async () => {
|
||||
app.setGlobalPrefix('/api/:tenantId');
|
||||
|
||||
server = app.getHttpServer();
|
||||
await app.init();
|
||||
|
||||
await request(server)
|
||||
.get('/api/test/params')
|
||||
.expect(200, { '0': 'params', tenantId: 'test' });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,11 @@ export class AppController {
|
||||
return 'Hello: ' + req.extras?.data;
|
||||
}
|
||||
|
||||
@Get('params')
|
||||
getParams(@Req() req): any {
|
||||
return req.middlewareParams;
|
||||
}
|
||||
|
||||
@Get('health')
|
||||
getHealth(): string {
|
||||
return 'up';
|
||||
|
||||
@@ -22,6 +22,11 @@ export class AppModule {
|
||||
req.extras = { data: 'Data attached in middleware' };
|
||||
next();
|
||||
})
|
||||
.forRoutes({ path: '*', method: RequestMethod.GET })
|
||||
.apply((req, res, next) => {
|
||||
req.middlewareParams = req.params;
|
||||
next();
|
||||
})
|
||||
.forRoutes({ path: '*', method: RequestMethod.GET });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
106
integration/nest-application/use-body-parser/e2e/fastify.spec.ts
Normal file
106
integration/nest-application/use-body-parser/e2e/fastify.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
23
integration/nest-application/use-body-parser/tsconfig.json
Normal file
23
integration/nest-application/use-body-parser/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -3,5 +3,5 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "9.3.0-beta.3"
|
||||
"version": "9.3.3"
|
||||
}
|
||||
|
||||
1129
package-lock.json
generated
1129
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/core",
|
||||
"version": "9.1.2",
|
||||
"version": "9.3.0",
|
||||
"description": "Modern, fast, powerful node.js web framework",
|
||||
"homepage": "https://nestjs.com",
|
||||
"repository": {
|
||||
@@ -58,6 +58,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@node-rs/xxhash": "^1.3.0",
|
||||
"@nuxtjs/opencollective": "0.3.2",
|
||||
"class-transformer": "0.5.1",
|
||||
"class-validator": "0.14.0",
|
||||
@@ -73,7 +74,7 @@
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "7.8.0",
|
||||
"socket.io": "4.5.4",
|
||||
"tslib": "2.4.1",
|
||||
"tslib": "2.5.0",
|
||||
"uid": "2.0.1",
|
||||
"uuid": "9.0.0"
|
||||
},
|
||||
@@ -98,11 +99,11 @@
|
||||
"@types/chai": "4.3.4",
|
||||
"@types/chai-as-promised": "7.1.5",
|
||||
"@types/cors": "2.8.13",
|
||||
"@types/express": "4.17.16",
|
||||
"@types/express": "4.17.17",
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/http-errors": "2.0.1",
|
||||
"@types/mocha": "10.0.1",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node": "18.11.19",
|
||||
"@types/sinon": "10.0.13",
|
||||
"@types/supertest": "2.0.12",
|
||||
"@types/ws": "8.5.4",
|
||||
@@ -115,7 +116,7 @@
|
||||
"artillery": "1.7.9",
|
||||
"body-parser": "1.20.1",
|
||||
"bytes": "3.1.2",
|
||||
"cache-manager": "5.1.4",
|
||||
"cache-manager": "5.1.5",
|
||||
"cache-manager-redis-store": "3.0.1",
|
||||
"chai": "4.3.7",
|
||||
"chai-as-promised": "7.1.1",
|
||||
@@ -123,10 +124,10 @@
|
||||
"commitlint-circle": "1.0.0",
|
||||
"concurrently": "7.6.0",
|
||||
"conventional-changelog": "3.1.25",
|
||||
"core-js": "3.27.1",
|
||||
"core-js": "3.27.2",
|
||||
"coveralls": "3.1.1",
|
||||
"delete-empty": "3.0.0",
|
||||
"engine.io-client": "6.3.0",
|
||||
"engine.io-client": "6.3.1",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-prettier": "8.6.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
@@ -134,7 +135,7 @@
|
||||
"fancy-log": "2.0.0",
|
||||
"fastify": "4.12.0",
|
||||
"graphql": "15.8.0",
|
||||
"graphql-tools": "8.3.16",
|
||||
"graphql-tools": "8.3.17",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-clang-format": "1.0.27",
|
||||
"gulp-clean": "0.4.0",
|
||||
@@ -154,7 +155,7 @@
|
||||
"markdown-table": "2.0.0",
|
||||
"merge-graphql-schemas": "1.7.8",
|
||||
"mocha": "10.2.0",
|
||||
"mongoose": "6.8.4",
|
||||
"mongoose": "6.9.0",
|
||||
"mqtt": "4.3.7",
|
||||
"multer": "1.4.4",
|
||||
"mysql2": "3.1.0",
|
||||
@@ -162,7 +163,7 @@
|
||||
"nodemon": "2.0.20",
|
||||
"nyc": "15.1.0",
|
||||
"prettier": "2.8.3",
|
||||
"redis": "4.5.1",
|
||||
"redis": "4.6.4",
|
||||
"rxjs-compat": "6.6.7",
|
||||
"sinon": "15.0.1",
|
||||
"sinon-chai": "3.7.0",
|
||||
@@ -172,7 +173,7 @@
|
||||
"ts-morph": "17.0.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typeorm": "0.3.11",
|
||||
"typescript": "4.9.4",
|
||||
"typescript": "4.9.5",
|
||||
"wrk": "1.2.1",
|
||||
"ws": "8.12.0"
|
||||
},
|
||||
|
||||
@@ -47,15 +47,17 @@ export interface CacheStoreSetOptions<T> {
|
||||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export interface CacheStoreFactory {
|
||||
/**
|
||||
* Return a configured cache store.
|
||||
*
|
||||
* @param args Cache manager options received from `CacheModule.register()`
|
||||
* or `CacheModule.registerAsync()`
|
||||
*/
|
||||
create(args: LiteralObject): CacheStore;
|
||||
}
|
||||
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.
|
||||
|
||||
@@ -35,7 +35,7 @@ export const ENHANCER_KEY_TO_SUBTYPE_MAP = {
|
||||
} as const;
|
||||
|
||||
export type EnhancerSubtype =
|
||||
typeof ENHANCER_KEY_TO_SUBTYPE_MAP[keyof typeof ENHANCER_KEY_TO_SUBTYPE_MAP];
|
||||
(typeof ENHANCER_KEY_TO_SUBTYPE_MAP)[keyof typeof ENHANCER_KEY_TO_SUBTYPE_MAP];
|
||||
|
||||
export const RENDER_METADATA = '__renderTemplate__';
|
||||
export const HTTP_CODE_METADATA = '__httpCode__';
|
||||
|
||||
@@ -99,9 +99,9 @@ export class HttpException extends Error {
|
||||
) {
|
||||
this.message = (this.response as Record<string, any>).message;
|
||||
} else if (this.constructor) {
|
||||
this.message = this.constructor.name
|
||||
.match(/[A-Z][a-z]+|[0-9]+/g)
|
||||
.join(' ');
|
||||
this.message =
|
||||
this.constructor.name.match(/[A-Z][a-z]+|[0-9]+/g)?.join(' ') ??
|
||||
'Error';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Nest @common
|
||||
* Copyright(c) 2017 - 2022 Kamil Mysliwiec
|
||||
* Copyright(c) 2017 - 2023 Kamil Mysliwiec
|
||||
* https://nestjs.com
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/common",
|
||||
"version": "9.3.0-beta.3",
|
||||
"version": "9.3.3",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"homepage": "https://nestjs.com",
|
||||
@@ -18,7 +18,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"tslib": "2.4.1",
|
||||
"tslib": "2.5.0",
|
||||
"uid": "2.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isUndefined } from '../../utils/shared.utils';
|
||||
import { Injectable, Optional } from '../../decorators/core';
|
||||
import { HttpStatus } from '../../enums';
|
||||
import { PipeTransform } from '../../interfaces/features/pipe-transform.interface';
|
||||
import { HttpErrorByCode } from '../../utils/http-error-by-code.util';
|
||||
import { isEmpty, isObject, isUndefined } from '../../utils/shared.utils';
|
||||
import { FileValidator } from './file-validator.interface';
|
||||
import { ParseFileOptions } from './parse-file-options.interface';
|
||||
|
||||
@@ -39,20 +39,34 @@ export class ParseFilePipe implements PipeTransform<any> {
|
||||
}
|
||||
|
||||
async transform(value: any): Promise<any> {
|
||||
if (isUndefined(value)) {
|
||||
if (this.thereAreNoFilesIn(value)) {
|
||||
if (this.fileIsRequired) {
|
||||
throw this.exceptionFactory('File is required');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
if (this.validators.length) {
|
||||
await this.validate(value);
|
||||
if (Array.isArray(value)) {
|
||||
await this.validateFiles(value);
|
||||
} else {
|
||||
await this.validate(value);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private validateFiles(files: any[]): Promise<any[]> {
|
||||
return Promise.all(files.map(f => this.validate(f)));
|
||||
}
|
||||
|
||||
private thereAreNoFilesIn(value: any): boolean {
|
||||
const isEmptyArray = Array.isArray(value) && isEmpty(value);
|
||||
const isEmptyObject = isObject(value) && isEmpty(Object.keys(value));
|
||||
return isUndefined(value) || isEmptyArray || isEmptyObject;
|
||||
}
|
||||
|
||||
protected async validate(file: any): Promise<any> {
|
||||
for (const validator of this.validators) {
|
||||
await this.validateOrThrow(file, validator);
|
||||
|
||||
@@ -66,8 +66,10 @@ export class ValidationPipe implements PipeTransform<any> {
|
||||
...validatorOptions
|
||||
} = options;
|
||||
|
||||
// @see https://github.com/nestjs/nest/issues/10683#issuecomment-1413690508
|
||||
this.validatorOptions = { forbidUnknownValues: false, ...validatorOptions };
|
||||
|
||||
this.isTransformEnabled = !!transform;
|
||||
this.validatorOptions = validatorOptions;
|
||||
this.transformOptions = transformOptions;
|
||||
this.isDetailedOutputDisabled = disableErrorMessages;
|
||||
this.validateCustomDecorators = validateCustomDecorators || false;
|
||||
|
||||
@@ -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';
|
||||
@@ -126,7 +125,9 @@ export class Logger implements LoggerService {
|
||||
@Logger.WrapBuffer
|
||||
error(message: any, ...optionalParams: any[]) {
|
||||
optionalParams = this.context
|
||||
? optionalParams.concat(this.context)
|
||||
? (optionalParams.length ? optionalParams : [undefined]).concat(
|
||||
this.context,
|
||||
)
|
||||
: optionalParams;
|
||||
|
||||
this.localInstance?.error(message, ...optionalParams);
|
||||
|
||||
@@ -384,7 +384,7 @@ describe('ValidationPipe', () => {
|
||||
});
|
||||
describe('otherwise', () => {
|
||||
it('should not reject', async () => {
|
||||
target = new ValidationPipe({ forbidUnknownValues: false });
|
||||
target = new ValidationPipe();
|
||||
const testObj = [
|
||||
{ prop1: 'value1', prop2: 'value2', prop3: 'value3' },
|
||||
];
|
||||
@@ -447,7 +447,7 @@ describe('ValidationPipe', () => {
|
||||
target = new ValidationPipe({ expectedType: TestModel });
|
||||
const testObj = { prop1: 'value1', prop2: 'value2' };
|
||||
|
||||
expect(await target.transform(testObj, m)).to.equal(testObj);
|
||||
expect(await target.transform(testObj, m)).to.deep.equal(testObj);
|
||||
});
|
||||
|
||||
it('should validate against the expected type if presented and metatype is primitive type', async () => {
|
||||
@@ -460,7 +460,7 @@ describe('ValidationPipe', () => {
|
||||
target = new ValidationPipe({ expectedType: TestModel });
|
||||
const testObj = { prop1: 'value1', prop2: 'value2' };
|
||||
|
||||
expect(await target.transform(testObj, m)).to.equal(testObj);
|
||||
expect(await target.transform(testObj, m)).to.deep.equal(testObj);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -480,43 +480,87 @@ describe('Logger', () => {
|
||||
warn(message: any, context?: string) {}
|
||||
}
|
||||
|
||||
const customLogger = new CustomLogger();
|
||||
const originalLogger = new Logger();
|
||||
describe('with global context', () => {
|
||||
const customLogger = new CustomLogger();
|
||||
const globalContext = 'RandomContext';
|
||||
const originalLogger = new Logger(globalContext);
|
||||
|
||||
let previousLoggerRef: LoggerService;
|
||||
let previousLoggerRef: LoggerService;
|
||||
|
||||
beforeEach(() => {
|
||||
previousLoggerRef =
|
||||
Logger['localInstanceRef'] || Logger['staticInstanceRef'];
|
||||
Logger.overrideLogger(customLogger);
|
||||
beforeEach(() => {
|
||||
previousLoggerRef =
|
||||
Logger['localInstanceRef'] || Logger['staticInstanceRef'];
|
||||
Logger.overrideLogger(customLogger);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Logger.overrideLogger(previousLoggerRef);
|
||||
});
|
||||
|
||||
it('should call custom logger "#log()" method with context as second argument', () => {
|
||||
const message = 'random log message with global context';
|
||||
|
||||
const customLoggerLogSpy = sinon.spy(customLogger, 'log');
|
||||
|
||||
originalLogger.log(message);
|
||||
|
||||
expect(customLoggerLogSpy.called).to.be.true;
|
||||
expect(customLoggerLogSpy.calledWith(message, globalContext)).to.be
|
||||
.true;
|
||||
});
|
||||
it('should call custom logger "#error()" method with context as third argument', () => {
|
||||
const message = 'random error message with global context';
|
||||
|
||||
const customLoggerErrorSpy = sinon.spy(customLogger, 'error');
|
||||
|
||||
originalLogger.error(message);
|
||||
|
||||
expect(customLoggerErrorSpy.called).to.be.true;
|
||||
expect(
|
||||
customLoggerErrorSpy.calledWith(message, undefined, globalContext),
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('without global context', () => {
|
||||
const customLogger = new CustomLogger();
|
||||
const originalLogger = new Logger();
|
||||
|
||||
afterEach(() => {
|
||||
Logger.overrideLogger(previousLoggerRef);
|
||||
});
|
||||
let previousLoggerRef: LoggerService;
|
||||
|
||||
it('should call custom logger "#log()" method', () => {
|
||||
const message = 'random message';
|
||||
const context = 'RandomContext';
|
||||
beforeEach(() => {
|
||||
previousLoggerRef =
|
||||
Logger['localInstanceRef'] || Logger['staticInstanceRef'];
|
||||
Logger.overrideLogger(customLogger);
|
||||
});
|
||||
|
||||
const customLoggerLogSpy = sinon.spy(customLogger, 'log');
|
||||
afterEach(() => {
|
||||
Logger.overrideLogger(previousLoggerRef);
|
||||
});
|
||||
|
||||
originalLogger.log(message, context);
|
||||
it('should call custom logger "#log()" method', () => {
|
||||
const message = 'random message';
|
||||
const context = 'RandomContext';
|
||||
|
||||
expect(customLoggerLogSpy.called).to.be.true;
|
||||
expect(customLoggerLogSpy.calledWith(message, context)).to.be.true;
|
||||
});
|
||||
const customLoggerLogSpy = sinon.spy(customLogger, 'log');
|
||||
|
||||
it('should call custom logger "#error()" method', () => {
|
||||
const message = 'random message';
|
||||
const context = 'RandomContext';
|
||||
originalLogger.log(message, context);
|
||||
|
||||
const customLoggerErrorSpy = sinon.spy(customLogger, 'error');
|
||||
expect(customLoggerLogSpy.called).to.be.true;
|
||||
expect(customLoggerLogSpy.calledWith(message, context)).to.be.true;
|
||||
});
|
||||
|
||||
originalLogger.error(message, context);
|
||||
it('should call custom logger "#error()" method', () => {
|
||||
const message = 'random message';
|
||||
const context = 'RandomContext';
|
||||
|
||||
expect(customLoggerErrorSpy.called).to.be.true;
|
||||
expect(customLoggerErrorSpy.calledWith(message, context)).to.be.true;
|
||||
const customLoggerErrorSpy = sinon.spy(customLogger, 'error');
|
||||
|
||||
originalLogger.error(message, undefined, context);
|
||||
|
||||
expect(customLoggerErrorSpy.called).to.be.true;
|
||||
expect(customLoggerErrorSpy.calledWith(message, undefined, context))
|
||||
.to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Nest @core
|
||||
* Copyright(c) 2017 - 2022 Kamil Mysliwiec
|
||||
* Copyright(c) 2017 - 2023 Kamil Mysliwiec
|
||||
* https://nestjs.com
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { DynamicModule, Type } from '@nestjs/common/interfaces';
|
||||
import {
|
||||
DynamicModule,
|
||||
ForwardReference,
|
||||
Type,
|
||||
} from '@nestjs/common/interfaces';
|
||||
import { ModuleTokenFactory } from './module-token-factory';
|
||||
|
||||
export interface ModuleFactory {
|
||||
@@ -18,19 +22,25 @@ export class ModuleCompiler {
|
||||
return { type, dynamicMetadata, token };
|
||||
}
|
||||
|
||||
public extractMetadata(metatype: Type<any> | DynamicModule): {
|
||||
public extractMetadata(
|
||||
metatype: Type<any> | ForwardReference | DynamicModule,
|
||||
): {
|
||||
type: Type<any>;
|
||||
dynamicMetadata?: Partial<DynamicModule> | undefined;
|
||||
} {
|
||||
if (!this.isDynamicModule(metatype)) {
|
||||
return { type: metatype };
|
||||
return {
|
||||
type: (metatype as ForwardReference)?.forwardRef
|
||||
? (metatype as ForwardReference).forwardRef()
|
||||
: metatype,
|
||||
};
|
||||
}
|
||||
const { module: type, ...dynamicMetadata } = metatype;
|
||||
return { type, dynamicMetadata };
|
||||
}
|
||||
|
||||
public isDynamicModule(
|
||||
module: Type<any> | DynamicModule,
|
||||
module: Type<any> | DynamicModule | ForwardReference,
|
||||
): module is DynamicModule {
|
||||
return !!(module as DynamicModule).module;
|
||||
}
|
||||
|
||||
@@ -353,7 +353,9 @@ export class Injector {
|
||||
}
|
||||
|
||||
public reflectConstructorParams<T>(type: Type<T>): any[] {
|
||||
const paramtypes = [...(Reflect.getMetadata(PARAMTYPES_METADATA, type) || [])];
|
||||
const paramtypes = [
|
||||
...(Reflect.getMetadata(PARAMTYPES_METADATA, type) || []),
|
||||
];
|
||||
const selfParams = this.reflectSelfParams<T>(type);
|
||||
|
||||
selfParams.forEach(({ index, param }) => (paramtypes[index] = param));
|
||||
|
||||
@@ -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,
|
||||
@@ -203,15 +203,18 @@ export class InstanceWrapper<T = any> {
|
||||
if (!isUndefined(this.isTreeDurable)) {
|
||||
return this.isTreeDurable;
|
||||
}
|
||||
if (this.durable === true) {
|
||||
this.isTreeDurable = true;
|
||||
this.printIntrospectedAsDurable();
|
||||
if (this.scope === Scope.REQUEST) {
|
||||
this.isTreeDurable = this.durable === undefined ? false : this.durable;
|
||||
if (this.isTreeDurable) {
|
||||
this.printIntrospectedAsDurable();
|
||||
}
|
||||
return this.isTreeDurable;
|
||||
}
|
||||
const isStatic = this.isDependencyTreeStatic();
|
||||
if (isStatic) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isTreeNonDurable = this.introspectDepsAttribute(
|
||||
(collection, registry) =>
|
||||
collection.some(
|
||||
@@ -221,7 +224,7 @@ export class InstanceWrapper<T = any> {
|
||||
),
|
||||
lookupRegistry,
|
||||
);
|
||||
this.isTreeDurable = !isTreeNonDurable && this.durable !== false;
|
||||
this.isTreeDurable = !isTreeNonDurable;
|
||||
if (this.isTreeDurable) {
|
||||
this.printIntrospectedAsDurable();
|
||||
}
|
||||
@@ -236,32 +239,30 @@ export class InstanceWrapper<T = any> {
|
||||
lookupRegistry: string[] = [],
|
||||
): boolean {
|
||||
if (lookupRegistry.includes(this[INSTANCE_ID_SYMBOL])) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
lookupRegistry = lookupRegistry.concat(this[INSTANCE_ID_SYMBOL]);
|
||||
|
||||
const { dependencies, properties, enhancers } =
|
||||
this[INSTANCE_METADATA_SYMBOL];
|
||||
|
||||
let introspectionResult =
|
||||
(dependencies && callback(dependencies, lookupRegistry)) || !dependencies;
|
||||
let introspectionResult = dependencies
|
||||
? callback(dependencies, lookupRegistry)
|
||||
: false;
|
||||
|
||||
if (!introspectionResult || !(properties || enhancers)) {
|
||||
if (introspectionResult || !(properties || enhancers)) {
|
||||
return introspectionResult;
|
||||
}
|
||||
const propertiesHosts = (properties || []).map(item => item.wrapper);
|
||||
introspectionResult =
|
||||
introspectionResult &&
|
||||
((properties &&
|
||||
callback(
|
||||
introspectionResult = properties
|
||||
? callback(
|
||||
properties.map(item => item.wrapper),
|
||||
lookupRegistry,
|
||||
)) ||
|
||||
!properties);
|
||||
if (!introspectionResult || !enhancers) {
|
||||
)
|
||||
: false;
|
||||
if (introspectionResult || !enhancers) {
|
||||
return introspectionResult;
|
||||
}
|
||||
return callback(enhancers, lookupRegistry);
|
||||
return enhancers ? callback(enhancers, lookupRegistry) : false;
|
||||
}
|
||||
|
||||
public isDependencyTreeStatic(lookupRegistry: string[] = []): boolean {
|
||||
@@ -273,10 +274,10 @@ export class InstanceWrapper<T = any> {
|
||||
this.printIntrospectedAsRequestScoped();
|
||||
return this.isTreeStatic;
|
||||
}
|
||||
this.isTreeStatic = this.introspectDepsAttribute(
|
||||
this.isTreeStatic = !this.introspectDepsAttribute(
|
||||
(collection, registry) =>
|
||||
collection.every((item: InstanceWrapper) =>
|
||||
item.isDependencyTreeStatic(registry),
|
||||
collection.some(
|
||||
(item: InstanceWrapper) => !item.isDependencyTreeStatic(registry),
|
||||
),
|
||||
lookupRegistry,
|
||||
);
|
||||
@@ -469,6 +470,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,14 @@ import { DynamicModule } from '@nestjs/common';
|
||||
import { Type } from '@nestjs/common/interfaces/type.interface';
|
||||
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
|
||||
import { isFunction, isSymbol } from '@nestjs/common/utils/shared.utils';
|
||||
import { xxh32 } from '@node-rs/xxhash';
|
||||
import stringify from 'fast-safe-stringify';
|
||||
import * as hash from 'object-hash';
|
||||
|
||||
const CLASS_STR = 'class ';
|
||||
const CLASS_STR_LEN = CLASS_STR.length;
|
||||
|
||||
export class ModuleTokenFactory {
|
||||
private readonly moduleTokenCache = new Map<string, string>();
|
||||
private readonly moduleIdsCache = new WeakMap<Type<unknown>, string>();
|
||||
|
||||
public create(
|
||||
@@ -13,23 +17,36 @@ export class ModuleTokenFactory {
|
||||
dynamicModuleMetadata?: Partial<DynamicModule> | undefined,
|
||||
): string {
|
||||
const moduleId = this.getModuleId(metatype);
|
||||
|
||||
if (!dynamicModuleMetadata) {
|
||||
return this.getStaticModuleToken(moduleId, this.getModuleName(metatype));
|
||||
}
|
||||
const opaqueToken = {
|
||||
id: moduleId,
|
||||
module: this.getModuleName(metatype),
|
||||
dynamic: this.getDynamicMetadataToken(dynamicModuleMetadata),
|
||||
dynamic: dynamicModuleMetadata,
|
||||
};
|
||||
return hash(opaqueToken, { ignoreUnknown: true });
|
||||
const opaqueTokenString = this.getStringifiedOpaqueToken(opaqueToken);
|
||||
|
||||
return this.hashString(opaqueTokenString);
|
||||
}
|
||||
|
||||
public getDynamicMetadataToken(
|
||||
dynamicModuleMetadata: Partial<DynamicModule> | undefined,
|
||||
): string {
|
||||
public getStaticModuleToken(moduleId: string, moduleName: string): string {
|
||||
const key = `${moduleId}_${moduleName}`;
|
||||
if (this.moduleTokenCache.has(key)) {
|
||||
return this.moduleTokenCache.get(key);
|
||||
}
|
||||
|
||||
const hash = this.hashString(key);
|
||||
this.moduleTokenCache.set(key, hash);
|
||||
return hash;
|
||||
}
|
||||
|
||||
public getStringifiedOpaqueToken(opaqueToken: object | undefined): string {
|
||||
// Uses safeStringify instead of JSON.stringify to support circular dynamic modules
|
||||
// The replacer function is also required in order to obtain real class names
|
||||
// instead of the unified "Function" key
|
||||
return dynamicModuleMetadata
|
||||
? stringify(dynamicModuleMetadata, this.replacer)
|
||||
: '';
|
||||
return opaqueToken ? stringify(opaqueToken, this.replacer) : '';
|
||||
}
|
||||
|
||||
public getModuleId(metatype: Type<unknown>): string {
|
||||
@@ -46,14 +63,18 @@ export class ModuleTokenFactory {
|
||||
return metatype.name;
|
||||
}
|
||||
|
||||
private hashString(value: string): string {
|
||||
return xxh32(value).toString();
|
||||
}
|
||||
|
||||
private replacer(key: string, value: any) {
|
||||
if (isFunction(value)) {
|
||||
const funcAsString = value.toString();
|
||||
const isClass = /^class\s/.test(funcAsString);
|
||||
const isClass = funcAsString.slice(0, CLASS_STR_LEN) === CLASS_STR;
|
||||
if (isClass) {
|
||||
return value.name;
|
||||
}
|
||||
return hash(funcAsString, { ignoreUnknown: true });
|
||||
return funcAsString;
|
||||
}
|
||||
if (isSymbol(value)) {
|
||||
return value.toString();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { uid } from 'uid';
|
||||
import { Module } from './module';
|
||||
|
||||
export class ModulesContainer extends Map<string, Module> {
|
||||
private readonly _applicationId = uuid();
|
||||
private readonly _applicationId = uid(21);
|
||||
|
||||
get applicationId(): string {
|
||||
return this._applicationId;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { v5 as uuid } from 'uuid';
|
||||
import { xxh32 } from '@node-rs/xxhash';
|
||||
|
||||
const DEFAULT_UUID_NAMESPACE = 'efa0df42-88af-474f-9cad-4206a2319f07';
|
||||
|
||||
@@ -6,12 +6,16 @@ export class DeterministicUuidRegistry {
|
||||
private static readonly registry = new Set<string>();
|
||||
|
||||
static get(str: string, namespace: string = DEFAULT_UUID_NAMESPACE, inc = 0) {
|
||||
const id = inc ? uuid(str + `${inc}`, namespace) : uuid(str, namespace);
|
||||
if (this.registry.has(id)) {
|
||||
const id = inc
|
||||
? xxh32(`${namespace}_${str}_${inc}`)
|
||||
: xxh32(`${namespace}_${str}`);
|
||||
const idAsString = `${id}`;
|
||||
|
||||
if (this.registry.has(idAsString)) {
|
||||
return this.get(str, namespace, inc + 1);
|
||||
}
|
||||
this.registry.add(id);
|
||||
return id;
|
||||
this.registry.add(idAsString);
|
||||
return idAsString;
|
||||
}
|
||||
|
||||
static clear() {
|
||||
|
||||
21
packages/core/inspector/uuid-factory.ts
Normal file
21
packages/core/inspector/uuid-factory.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { randomStringGenerator } from '@nestjs/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.Random;
|
||||
|
||||
static set mode(value: UuidFactoryMode) {
|
||||
this._mode = value;
|
||||
}
|
||||
|
||||
static get(key = '', namespace?: string) {
|
||||
return this._mode === UuidFactoryMode.Deterministic
|
||||
? DeterministicUuidRegistry.get(key, namespace)
|
||||
: randomStringGenerator();
|
||||
}
|
||||
}
|
||||
@@ -4,36 +4,111 @@ import {
|
||||
isFunction,
|
||||
isNil,
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import { iterate } from 'iterare';
|
||||
|
||||
export class MetadataScanner {
|
||||
private readonly cachedScannedPrototypes: Map<object, string[]> = new Map();
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link getAllMethodNames} instead.
|
||||
*/
|
||||
public scanFromPrototype<T extends Injectable, R = any>(
|
||||
instance: T,
|
||||
prototype: object,
|
||||
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);
|
||||
|
||||
// reason: https://github.com/nestjs/nest/pull/10821#issuecomment-1411916533
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link getAllMethodNames} instead.
|
||||
*/
|
||||
public *getAllFilteredMethodNames(
|
||||
prototype: object,
|
||||
): IterableIterator<string> {
|
||||
yield* this.getAllMethodNames(prototype);
|
||||
}
|
||||
|
||||
public getAllMethodNames(prototype: object | null): string[] {
|
||||
if (!prototype) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.cachedScannedPrototypes.has(prototype)) {
|
||||
return this.cachedScannedPrototypes.get(prototype);
|
||||
}
|
||||
|
||||
const visitedNames = new Map<string, boolean>();
|
||||
const result: string[] = [];
|
||||
|
||||
this.cachedScannedPrototypes.set(prototype, result);
|
||||
|
||||
do {
|
||||
for (const property of Object.getOwnPropertyNames(prototype)) {
|
||||
if (visitedNames.has(property)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visitedNames.set(property, true);
|
||||
|
||||
// reason: https://github.com/nestjs/nest/pull/10821#issuecomment-1411916533
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
} from '@nestjs/common/interfaces';
|
||||
import {
|
||||
MiddlewareConfigProxy,
|
||||
RouteInfo,
|
||||
MiddlewareConfiguration,
|
||||
RouteInfo,
|
||||
} from '@nestjs/common/interfaces/middleware';
|
||||
import { iterate } from 'iterare';
|
||||
import { RouteInfoPathExtractor } from './route-info-path-extractor';
|
||||
import { RoutesMapper } from './routes-mapper';
|
||||
import { filterMiddleware } from './utils';
|
||||
|
||||
@@ -19,12 +20,17 @@ export class MiddlewareBuilder implements MiddlewareConsumer {
|
||||
constructor(
|
||||
private readonly routesMapper: RoutesMapper,
|
||||
private readonly httpAdapter: HttpServer,
|
||||
private readonly routeInfoPathExtractor: RouteInfoPathExtractor,
|
||||
) {}
|
||||
|
||||
public apply(
|
||||
...middleware: Array<Type<any> | Function | any>
|
||||
): MiddlewareConfigProxy {
|
||||
return new MiddlewareBuilder.ConfigProxy(this, flatten(middleware));
|
||||
return new MiddlewareBuilder.ConfigProxy(
|
||||
this,
|
||||
flatten(middleware),
|
||||
this.routeInfoPathExtractor,
|
||||
);
|
||||
}
|
||||
|
||||
public build(): MiddlewareConfiguration[] {
|
||||
@@ -41,6 +47,7 @@ export class MiddlewareBuilder implements MiddlewareConsumer {
|
||||
constructor(
|
||||
private readonly builder: MiddlewareBuilder,
|
||||
private readonly middleware: Array<Type<any> | Function | any>,
|
||||
private routeInfoPathExtractor: RouteInfoPathExtractor,
|
||||
) {}
|
||||
|
||||
public getExcludedRoutes(): RouteInfo[] {
|
||||
@@ -50,7 +57,10 @@ export class MiddlewareBuilder implements MiddlewareConsumer {
|
||||
public exclude(
|
||||
...routes: Array<string | RouteInfo>
|
||||
): MiddlewareConfigProxy {
|
||||
this.excludedRoutes = this.getRoutesFlatList(routes);
|
||||
this.excludedRoutes = this.getRoutesFlatList(routes).map(route => ({
|
||||
...route,
|
||||
path: this.routeInfoPathExtractor.extractPathFrom(route),
|
||||
}));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpServer, Logger, VersioningType } from '@nestjs/common';
|
||||
import { HttpServer, Logger } from '@nestjs/common';
|
||||
import { RequestMethod } from '@nestjs/common/enums/request-method.enum';
|
||||
import {
|
||||
MiddlewareConfiguration,
|
||||
@@ -6,10 +6,7 @@ import {
|
||||
RouteInfo,
|
||||
} from '@nestjs/common/interfaces/middleware';
|
||||
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
|
||||
import {
|
||||
addLeadingSlash,
|
||||
isUndefined,
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import { isUndefined } from '@nestjs/common/utils/shared.utils';
|
||||
import { ApplicationConfig } from '../application-config';
|
||||
import { InvalidMiddlewareException } from '../errors/exceptions/invalid-middleware.exception';
|
||||
import { RuntimeException } from '../errors/exceptions/runtime.exception';
|
||||
@@ -18,7 +15,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 {
|
||||
@@ -26,13 +23,13 @@ import {
|
||||
MiddlewareEntrypointMetadata,
|
||||
} from '../inspector/interfaces/entrypoint.interface';
|
||||
import { REQUEST_CONTEXT_ID } from '../router/request/request-constants';
|
||||
import { RoutePathFactory } from '../router/route-path-factory';
|
||||
import { RouterExceptionFilters } from '../router/router-exception-filters';
|
||||
import { RouterProxy } from '../router/router-proxy';
|
||||
import { isRequestMethodAll, isRouteExcluded } from '../router/utils';
|
||||
import { isRequestMethodAll } from '../router/utils';
|
||||
import { MiddlewareBuilder } from './builder';
|
||||
import { MiddlewareContainer } from './container';
|
||||
import { MiddlewareResolver } from './resolver';
|
||||
import { RouteInfoPathExtractor } from './route-info-path-extractor';
|
||||
import { RoutesMapper } from './routes-mapper';
|
||||
|
||||
export class MiddlewareModule<
|
||||
@@ -46,13 +43,11 @@ export class MiddlewareModule<
|
||||
private routerExceptionFilter: RouterExceptionFilters;
|
||||
private routesMapper: RoutesMapper;
|
||||
private resolver: MiddlewareResolver;
|
||||
private config: ApplicationConfig;
|
||||
private container: NestContainer;
|
||||
private httpAdapter: HttpServer;
|
||||
private graphInspector: GraphInspector;
|
||||
private appOptions: TAppOptions;
|
||||
|
||||
constructor(private readonly routePathFactory: RoutePathFactory) {}
|
||||
private routeInfoPathExtractor: RouteInfoPathExtractor;
|
||||
|
||||
public async register(
|
||||
middlewareContainer: MiddlewareContainer,
|
||||
@@ -73,8 +68,7 @@ export class MiddlewareModule<
|
||||
);
|
||||
this.routesMapper = new RoutesMapper(container);
|
||||
this.resolver = new MiddlewareResolver(middlewareContainer, injector);
|
||||
|
||||
this.config = config;
|
||||
this.routeInfoPathExtractor = new RouteInfoPathExtractor(config);
|
||||
this.injector = injector;
|
||||
this.container = container;
|
||||
this.httpAdapter = httpAdapter;
|
||||
@@ -111,6 +105,7 @@ export class MiddlewareModule<
|
||||
const middlewareBuilder = new MiddlewareBuilder(
|
||||
this.routesMapper,
|
||||
this.httpAdapter,
|
||||
this.routeInfoPathExtractor,
|
||||
);
|
||||
try {
|
||||
await instance.configure(middlewareBuilder);
|
||||
@@ -250,6 +245,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 +257,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,
|
||||
@@ -316,56 +302,46 @@ export class MiddlewareModule<
|
||||
|
||||
private async registerHandler(
|
||||
applicationRef: HttpServer,
|
||||
{ path, method, version }: RouteInfo,
|
||||
routeInfo: RouteInfo,
|
||||
proxy: <TRequest, TResponse>(
|
||||
req: TRequest,
|
||||
res: TResponse,
|
||||
next: () => void,
|
||||
) => void,
|
||||
) {
|
||||
const prefix = this.config.getGlobalPrefix();
|
||||
const excludedRoutes = this.config.getGlobalPrefixOptions().exclude;
|
||||
if (
|
||||
(Array.isArray(excludedRoutes) &&
|
||||
isRouteExcluded(excludedRoutes, path, method)) ||
|
||||
['*', '/*', '(.*)', '/(.*)'].includes(path)
|
||||
) {
|
||||
path = addLeadingSlash(path);
|
||||
} else {
|
||||
const basePath = addLeadingSlash(prefix);
|
||||
if (basePath?.endsWith('/') && path?.startsWith('/')) {
|
||||
// strip slash when a wildcard is being used
|
||||
// and global prefix has been set
|
||||
path = path?.slice(1);
|
||||
}
|
||||
path = basePath + path;
|
||||
}
|
||||
|
||||
const applicationVersioningConfig = this.config.getVersioning();
|
||||
if (version && applicationVersioningConfig.type === VersioningType.URI) {
|
||||
const versionPrefix = this.routePathFactory.getVersionPrefix(
|
||||
applicationVersioningConfig,
|
||||
);
|
||||
path = `/${versionPrefix}${version.toString()}${path}`;
|
||||
}
|
||||
|
||||
const { method } = routeInfo;
|
||||
const paths = this.routeInfoPathExtractor.extractPathsFrom(routeInfo);
|
||||
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();
|
||||
};
|
||||
paths.forEach(path => 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;
|
||||
}
|
||||
}
|
||||
|
||||
87
packages/core/middleware/route-info-path-extractor.ts
Normal file
87
packages/core/middleware/route-info-path-extractor.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { VersioningType } from '@nestjs/common';
|
||||
import {
|
||||
RouteInfo,
|
||||
VersioningOptions,
|
||||
VersionValue,
|
||||
} from '@nestjs/common/interfaces';
|
||||
import {
|
||||
addLeadingSlash,
|
||||
stripEndSlash,
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import { ApplicationConfig } from '../application-config';
|
||||
import { ExcludeRouteMetadata } from '../router/interfaces/exclude-route-metadata.interface';
|
||||
import { isRouteExcluded } from '../router/utils';
|
||||
import { RoutePathFactory } from './../router/route-path-factory';
|
||||
|
||||
export class RouteInfoPathExtractor {
|
||||
private routePathFactory: RoutePathFactory;
|
||||
private readonly prefixPath: string;
|
||||
private readonly excludedGlobalPrefixRoutes: ExcludeRouteMetadata[];
|
||||
private readonly versioningConfig?: VersioningOptions;
|
||||
|
||||
constructor(private readonly applicationConfig: ApplicationConfig) {
|
||||
this.routePathFactory = new RoutePathFactory(applicationConfig);
|
||||
this.prefixPath = stripEndSlash(
|
||||
addLeadingSlash(this.applicationConfig.getGlobalPrefix()),
|
||||
);
|
||||
this.excludedGlobalPrefixRoutes =
|
||||
this.applicationConfig.getGlobalPrefixOptions().exclude;
|
||||
this.versioningConfig = this.applicationConfig.getVersioning();
|
||||
}
|
||||
|
||||
public extractPathsFrom({ path, method, version }: RouteInfo): string[] {
|
||||
const versionPath = this.extractVersionPathFrom(version);
|
||||
|
||||
if (this.isAWildcard(path)) {
|
||||
return Array.isArray(this.excludedGlobalPrefixRoutes)
|
||||
? [
|
||||
this.prefixPath + versionPath + addLeadingSlash(path),
|
||||
...this.excludedGlobalPrefixRoutes.map(
|
||||
route => versionPath + addLeadingSlash(route.path),
|
||||
),
|
||||
]
|
||||
: [this.prefixPath + versionPath + addLeadingSlash(path)];
|
||||
}
|
||||
|
||||
return [this.extractNonWildcardPathFrom({ path, method, version })];
|
||||
}
|
||||
|
||||
public extractPathFrom(route: RouteInfo): string {
|
||||
if (this.isAWildcard(route.path) && !route.version) {
|
||||
return addLeadingSlash(route.path);
|
||||
}
|
||||
|
||||
return this.extractNonWildcardPathFrom(route);
|
||||
}
|
||||
|
||||
private isAWildcard(path: string): boolean {
|
||||
return ['*', '/*', '/*/', '(.*)', '/(.*)'].includes(path);
|
||||
}
|
||||
|
||||
private extractNonWildcardPathFrom({
|
||||
path,
|
||||
method,
|
||||
version,
|
||||
}: RouteInfo): string {
|
||||
const versionPath = this.extractVersionPathFrom(version);
|
||||
|
||||
if (
|
||||
Array.isArray(this.excludedGlobalPrefixRoutes) &&
|
||||
isRouteExcluded(this.excludedGlobalPrefixRoutes, path, method)
|
||||
) {
|
||||
return versionPath + addLeadingSlash(path);
|
||||
}
|
||||
|
||||
return this.prefixPath + versionPath + addLeadingSlash(path);
|
||||
}
|
||||
|
||||
private extractVersionPathFrom(version?: VersionValue): string {
|
||||
if (!version || this.versioningConfig?.type !== VersioningType.URI)
|
||||
return '';
|
||||
|
||||
const versionPrefix = this.routePathFactory.getVersionPrefix(
|
||||
this.versioningConfig,
|
||||
);
|
||||
return addLeadingSlash(versionPrefix + version.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,33 @@
|
||||
import { RequestMethod } from '@nestjs/common';
|
||||
import { HttpServer, RouteInfo, Type } from '@nestjs/common/interfaces';
|
||||
import { isFunction } from '@nestjs/common/utils/shared.utils';
|
||||
import {
|
||||
addLeadingSlash,
|
||||
isFunction,
|
||||
isString,
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import { iterate } from 'iterare';
|
||||
import * as pathToRegexp from 'path-to-regexp';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { uid } from 'uid';
|
||||
import { ExcludeRouteMetadata } from '../router/interfaces/exclude-route-metadata.interface';
|
||||
import { isRouteExcluded } from '../router/utils';
|
||||
|
||||
export const mapToExcludeRoute = (
|
||||
routes: RouteInfo[],
|
||||
routes: (string | RouteInfo)[],
|
||||
): ExcludeRouteMetadata[] => {
|
||||
return routes.map(({ path, method }) => ({
|
||||
pathRegex: pathToRegexp(path),
|
||||
requestMethod: method,
|
||||
}));
|
||||
return routes.map(route => {
|
||||
if (isString(route)) {
|
||||
return {
|
||||
path: route,
|
||||
requestMethod: RequestMethod.ALL,
|
||||
pathRegex: pathToRegexp(addLeadingSlash(route)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
path: route.path,
|
||||
requestMethod: route.method,
|
||||
pathRegex: pathToRegexp(addLeadingSlash(route.path)),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const filterMiddleware = <T extends Function | Type<any> = any>(
|
||||
@@ -85,7 +99,7 @@ export function isMiddlewareClass(middleware: any): middleware is Type<any> {
|
||||
);
|
||||
}
|
||||
|
||||
export function assignToken(metatype: Type<any>, token = uuid()): Type<any> {
|
||||
export function assignToken(metatype: Type<any>, token = uid(21)): Type<any> {
|
||||
Object.defineProperty(metatype, 'name', { value: token });
|
||||
return metatype;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
NestHybridApplicationOptions,
|
||||
NestInterceptor,
|
||||
PipeTransform,
|
||||
RequestMethod,
|
||||
VersioningOptions,
|
||||
VersioningType,
|
||||
WebSocketAdapter,
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
import {
|
||||
GlobalPrefixOptions,
|
||||
NestApplicationOptions,
|
||||
RouteInfo,
|
||||
} from '@nestjs/common/interfaces';
|
||||
import {
|
||||
CorsOptions,
|
||||
@@ -31,7 +29,6 @@ import {
|
||||
} from '@nestjs/common/utils/shared.utils';
|
||||
import { iterate } from 'iterare';
|
||||
import { platform } from 'os';
|
||||
import * as pathToRegexp from 'path-to-regexp';
|
||||
import { AbstractHttpAdapter } from './adapters';
|
||||
import { ApplicationConfig } from './application-config';
|
||||
import { MESSAGES } from './constants';
|
||||
@@ -41,10 +38,9 @@ import { Injector } from './injector/injector';
|
||||
import { GraphInspector } from './inspector/graph-inspector';
|
||||
import { MiddlewareContainer } from './middleware/container';
|
||||
import { MiddlewareModule } from './middleware/middleware-module';
|
||||
import { mapToExcludeRoute } from './middleware/utils';
|
||||
import { NestApplicationContext } from './nest-application-context';
|
||||
import { ExcludeRouteMetadata } from './router/interfaces/exclude-route-metadata.interface';
|
||||
import { Resolver } from './router/interfaces/resolver.interface';
|
||||
import { RoutePathFactory } from './router/route-path-factory';
|
||||
import { RoutesResolver } from './router/routes-resolver';
|
||||
|
||||
const { SocketModule } = optionalRequire(
|
||||
@@ -89,9 +85,8 @@ export class NestApplication
|
||||
|
||||
this.selectContextModule();
|
||||
this.registerHttpServer();
|
||||
|
||||
this.injector = new Injector({ preview: this.appOptions.preview });
|
||||
this.middlewareModule = new MiddlewareModule(new RoutePathFactory(config));
|
||||
this.middlewareModule = new MiddlewareModule();
|
||||
this.routesResolver = new RoutesResolver(
|
||||
this.container,
|
||||
this.config,
|
||||
@@ -266,6 +261,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);
|
||||
}
|
||||
@@ -358,20 +367,9 @@ export class NestApplication
|
||||
public setGlobalPrefix(prefix: string, options?: GlobalPrefixOptions): this {
|
||||
this.config.setGlobalPrefix(prefix);
|
||||
if (options) {
|
||||
const exclude = options?.exclude.map(
|
||||
(route: string | RouteInfo): ExcludeRouteMetadata => {
|
||||
if (isString(route)) {
|
||||
return {
|
||||
requestMethod: RequestMethod.ALL,
|
||||
pathRegex: pathToRegexp(addLeadingSlash(route)),
|
||||
};
|
||||
}
|
||||
return {
|
||||
requestMethod: route.method,
|
||||
pathRegex: pathToRegexp(addLeadingSlash(route.path)),
|
||||
};
|
||||
},
|
||||
);
|
||||
const exclude = options?.exclude
|
||||
? mapToExcludeRoute(options.exclude)
|
||||
: [];
|
||||
this.config.setGlobalPrefixOptions({
|
||||
...options,
|
||||
exclude,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/core",
|
||||
"version": "9.3.0-beta.3",
|
||||
"version": "9.3.3",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -27,16 +27,15 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@node-rs/xxhash": "^1.3.0",
|
||||
"@nuxtjs/opencollective": "0.3.2",
|
||||
"fast-safe-stringify": "2.1.1",
|
||||
"iterare": "1.2.1",
|
||||
"object-hash": "3.0.0",
|
||||
"path-to-regexp": "3.2.0",
|
||||
"tslib": "2.4.1",
|
||||
"uuid": "9.0.0"
|
||||
"tslib": "2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "^9.3.0-beta.3"
|
||||
"@nestjs/common": "9.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^9.0.0",
|
||||
|
||||
@@ -20,9 +20,7 @@ export class MethodsReplFn extends ReplFunction {
|
||||
? Object.getPrototypeOf(this.ctx.app.get(token))
|
||||
: token?.prototype;
|
||||
|
||||
const methods = new Set(
|
||||
this.metadataScanner.getAllFilteredMethodNames(proto),
|
||||
);
|
||||
const methods = this.metadataScanner.getAllMethodNames(proto);
|
||||
|
||||
this.ctx.writeToStdout('\n');
|
||||
this.ctx.writeToStdout(`${clc.green('Methods')}:\n`);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { RequestMethod } from '@nestjs/common';
|
||||
|
||||
export interface ExcludeRouteMetadata {
|
||||
/**
|
||||
* Route path.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* Regular expression representing the route path.
|
||||
*/
|
||||
|
||||
@@ -33,18 +33,28 @@ export class PathsExplorer {
|
||||
? Object.getPrototypeOf(instance)
|
||||
: prototype;
|
||||
|
||||
return this.metadataScanner.scanFromPrototype<Controller, RouteDefinition>(
|
||||
instance,
|
||||
instancePrototype,
|
||||
method => this.exploreMethodMetadata(instance, instancePrototype, method),
|
||||
);
|
||||
return this.metadataScanner
|
||||
.getAllMethodNames(instancePrototype)
|
||||
.reduce((acc, method) => {
|
||||
const route = this.exploreMethodMetadata(
|
||||
instance,
|
||||
instancePrototype,
|
||||
method,
|
||||
);
|
||||
|
||||
if (route) {
|
||||
acc.push(route);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
public exploreMethodMetadata(
|
||||
instance: Controller,
|
||||
prototype: object,
|
||||
methodName: string,
|
||||
): RouteDefinition {
|
||||
): RouteDefinition | null {
|
||||
const instanceCallback = instance[methodName];
|
||||
const prototypeCallback = prototype[methodName];
|
||||
const routePath = Reflect.getMetadata(PATH_METADATA, prototypeCallback);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RequestMethod } from '@nestjs/common';
|
||||
import { addLeadingSlash } from '@nestjs/common/utils/shared.utils';
|
||||
import { ExcludeRouteMetadata } from '../interfaces/exclude-route-metadata.interface';
|
||||
|
||||
export const isRequestMethodAll = (method: RequestMethod) => {
|
||||
@@ -15,7 +16,7 @@ export function isRouteExcluded(
|
||||
isRequestMethodAll(route.requestMethod) ||
|
||||
route.requestMethod === requestMethod
|
||||
) {
|
||||
return route.pathRegex.exec(path);
|
||||
return route.pathRegex.exec(addLeadingSlash(path));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -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,9 +51,10 @@ 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';
|
||||
import { ParamsMetadata } from './helpers/interfaces/params-metadata.interface';
|
||||
|
||||
interface ApplicationProviderWrapper {
|
||||
moduleKey: string;
|
||||
@@ -249,14 +251,21 @@ export class DependenciesScanner {
|
||||
metadataKey,
|
||||
component,
|
||||
);
|
||||
const methodInjectables = this.metadataScanner.scanFromPrototype<
|
||||
Type,
|
||||
{ methodKey: string; metadata: Type<Injectable>[] }
|
||||
>(
|
||||
null,
|
||||
component.prototype,
|
||||
this.reflectKeyMetadata.bind(this, component, metadataKey),
|
||||
);
|
||||
const methodInjectables = this.metadataScanner
|
||||
.getAllMethodNames(component.prototype)
|
||||
.reduce((acc, method) => {
|
||||
const methodInjectable = this.reflectKeyMetadata(
|
||||
component,
|
||||
metadataKey,
|
||||
method,
|
||||
);
|
||||
|
||||
if (methodInjectable) {
|
||||
acc.push(methodInjectable);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
controllerInjectables.forEach(injectable =>
|
||||
this.insertInjectable(
|
||||
@@ -266,17 +275,17 @@ export class DependenciesScanner {
|
||||
ENHANCER_KEY_TO_SUBTYPE_MAP[metadataKey],
|
||||
),
|
||||
);
|
||||
methodInjectables.forEach(({ methodKey, metadata }) =>
|
||||
metadata.forEach(injectable =>
|
||||
methodInjectables.forEach(methodInjectable => {
|
||||
methodInjectable.metadata.forEach(injectable =>
|
||||
this.insertInjectable(
|
||||
injectable,
|
||||
token,
|
||||
component,
|
||||
ENHANCER_KEY_TO_SUBTYPE_MAP[metadataKey],
|
||||
methodKey,
|
||||
methodInjectable.methodKey,
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public reflectParamInjectables(
|
||||
@@ -284,28 +293,24 @@ export class DependenciesScanner {
|
||||
token: string,
|
||||
metadataKey: string,
|
||||
) {
|
||||
const paramsMetadata = this.metadataScanner.scanFromPrototype<
|
||||
Type,
|
||||
{
|
||||
methodKey: string;
|
||||
metadata: Record<
|
||||
string,
|
||||
{
|
||||
index: number;
|
||||
data: unknown;
|
||||
pipes: Array<Type<PipeTransform> | PipeTransform>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
>(null, component.prototype, methodKey => {
|
||||
const metadata = Reflect.getMetadata(metadataKey, component, methodKey);
|
||||
const paramsMethods = this.metadataScanner.getAllMethodNames(
|
||||
component.prototype,
|
||||
);
|
||||
|
||||
paramsMethods.forEach(methodKey => {
|
||||
const metadata: Record<
|
||||
string,
|
||||
{
|
||||
index: number;
|
||||
data: unknown;
|
||||
pipes: Array<Type<PipeTransform> | PipeTransform>;
|
||||
}
|
||||
> = Reflect.getMetadata(metadataKey, component, methodKey);
|
||||
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
return { methodKey, metadata };
|
||||
});
|
||||
|
||||
paramsMetadata.forEach(({ methodKey, metadata }) => {
|
||||
const params = Object.values(metadata);
|
||||
params
|
||||
.map(item => item.pipes)
|
||||
@@ -326,7 +331,7 @@ export class DependenciesScanner {
|
||||
component: Type<Injectable>,
|
||||
key: string,
|
||||
methodKey: string,
|
||||
) {
|
||||
): { methodKey: string; metadata: any } | undefined {
|
||||
let prototype = component.prototype;
|
||||
do {
|
||||
const descriptor = Reflect.getOwnPropertyDescriptor(prototype, methodKey);
|
||||
@@ -410,7 +415,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;
|
||||
|
||||
@@ -23,7 +23,11 @@ describe('ApplicationConfig', () => {
|
||||
it('should set global path options', () => {
|
||||
const options: GlobalPrefixOptions<ExcludeRouteMetadata> = {
|
||||
exclude: [
|
||||
{ pathRegex: new RegExp(/health/), requestMethod: RequestMethod.GET },
|
||||
{
|
||||
path: '/health',
|
||||
pathRegex: new RegExp(/health/),
|
||||
requestMethod: RequestMethod.GET,
|
||||
},
|
||||
],
|
||||
};
|
||||
appConfig.setGlobalPrefixOptions(options);
|
||||
|
||||
@@ -39,7 +39,68 @@ describe('InstanceWrapper', () => {
|
||||
expect(wrapper.isDependencyTreeStatic()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when request scoped durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
expect(wrapper.isDependencyTreeStatic()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when request scoped explicit non durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: false,
|
||||
});
|
||||
expect(wrapper.isDependencyTreeStatic()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when default', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({});
|
||||
expect(wrapper.isDependencyTreeStatic()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when statically scoped', () => {
|
||||
describe('dependencies, properties, enhancers', () => {
|
||||
describe('dependecies non static, properties static, enhancers static', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
wrapper.addCtorMetadata(
|
||||
0,
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
wrapper.addPropertiesMetadata('key1', new InstanceWrapper());
|
||||
wrapper.addEnhancerMetadata(new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeStatic()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('dependecies static, properties non static, enhancers static', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
wrapper.addCtorMetadata(0, new InstanceWrapper());
|
||||
wrapper.addPropertiesMetadata(
|
||||
'key1',
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
wrapper.addEnhancerMetadata(new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeStatic()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('dependecies static, properties static, enhancers non static', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
wrapper.addCtorMetadata(0, new InstanceWrapper());
|
||||
wrapper.addPropertiesMetadata('key1', new InstanceWrapper());
|
||||
wrapper.addEnhancerMetadata(
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeStatic()).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('dependencies', () => {
|
||||
describe('when each is static', () => {
|
||||
it('should return true', () => {
|
||||
@@ -116,8 +177,157 @@ describe('InstanceWrapper', () => {
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when request scoped and non durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
});
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when request scoped and explicit non durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: false,
|
||||
});
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when default scope', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when statically scoped', () => {
|
||||
describe('dependencies, properties, enhancers', () => {
|
||||
describe('dependecies non durable, properties non durable, enhancers durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
wrapper.addCtorMetadata(
|
||||
0,
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
wrapper.addCtorMetadata(1, new InstanceWrapper());
|
||||
wrapper.addPropertiesMetadata('key1', new InstanceWrapper());
|
||||
wrapper.addEnhancerMetadata(new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('dependecies non durable, properties durable, enhancers durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
wrapper.addCtorMetadata(
|
||||
0,
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
wrapper.addCtorMetadata(1, new InstanceWrapper());
|
||||
wrapper.addPropertiesMetadata(
|
||||
'key1',
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
wrapper.addEnhancerMetadata(new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('dependecies non durable, properties durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
wrapper.addCtorMetadata(
|
||||
0,
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
wrapper.addCtorMetadata(1, new InstanceWrapper());
|
||||
wrapper.addPropertiesMetadata('key1', new InstanceWrapper());
|
||||
wrapper.addPropertiesMetadata(
|
||||
'key2',
|
||||
new InstanceWrapper({ scope: Scope.REQUEST, durable: true }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('properties durable, enhancers non durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
wrapper.addPropertiesMetadata('key1', new InstanceWrapper());
|
||||
wrapper.addPropertiesMetadata(
|
||||
'key2',
|
||||
new InstanceWrapper({ scope: Scope.REQUEST, durable: true }),
|
||||
);
|
||||
wrapper.addEnhancerMetadata(
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('dependencies durable, enhancers non durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
wrapper.addCtorMetadata(
|
||||
0,
|
||||
new InstanceWrapper({ scope: Scope.REQUEST, durable: true }),
|
||||
);
|
||||
wrapper.addEnhancerMetadata(
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('dependencies', () => {
|
||||
describe('when wrapper is non durable and dependecy is static', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({ scope: Scope.REQUEST });
|
||||
wrapper.addCtorMetadata(0, new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is durable and dependecy is static', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
wrapper.addCtorMetadata(0, new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is non durable and dependecy is durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
});
|
||||
wrapper.addCtorMetadata(
|
||||
0,
|
||||
new InstanceWrapper({ scope: Scope.REQUEST, durable: true }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is durable and dependecy is static', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
wrapper.addCtorMetadata(0, new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is durable and dependecy is non durable', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
wrapper.addCtorMetadata(
|
||||
0,
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when each is static', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
@@ -174,6 +384,58 @@ describe('InstanceWrapper', () => {
|
||||
});
|
||||
});
|
||||
describe('properties', () => {
|
||||
describe('when wrapper is non durable and dependecy is static', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({ scope: Scope.REQUEST });
|
||||
wrapper.addPropertiesMetadata('key1', new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is durable and dependecy is static', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
wrapper.addPropertiesMetadata('key1', new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is non durable and dependecy is durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
});
|
||||
wrapper.addPropertiesMetadata(
|
||||
'key1',
|
||||
new InstanceWrapper({ scope: Scope.REQUEST, durable: true }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is durable and dependecy is static', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
wrapper.addPropertiesMetadata('key1', new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is durable and dependecy is non durable', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
wrapper.addPropertiesMetadata(
|
||||
'key1',
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when each is static', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
@@ -221,6 +483,56 @@ describe('InstanceWrapper', () => {
|
||||
});
|
||||
});
|
||||
describe('enhancers', () => {
|
||||
describe('when wrapper is non durable and dependecy is static', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({ scope: Scope.REQUEST });
|
||||
wrapper.addEnhancerMetadata(new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is durable and dependecy is static', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
wrapper.addEnhancerMetadata(new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is non durable and dependecy is durable', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
});
|
||||
wrapper.addEnhancerMetadata(
|
||||
new InstanceWrapper({ scope: Scope.REQUEST, durable: true }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.false;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is durable and dependecy is static', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
wrapper.addEnhancerMetadata(new InstanceWrapper());
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when wrapper is durable and dependecy is non durable', () => {
|
||||
it('should return true', () => {
|
||||
const wrapper = new InstanceWrapper({
|
||||
scope: Scope.REQUEST,
|
||||
durable: true,
|
||||
});
|
||||
wrapper.addEnhancerMetadata(
|
||||
new InstanceWrapper({ scope: Scope.REQUEST }),
|
||||
);
|
||||
expect(wrapper.isDependencyTreeDurable()).to.be.true;
|
||||
});
|
||||
});
|
||||
describe('when each is static', () => {
|
||||
it('should return false', () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { expect } from 'chai';
|
||||
import stringify from 'fast-safe-stringify';
|
||||
import * as hash from 'object-hash';
|
||||
import * as sinon from 'sinon';
|
||||
import { ModuleTokenFactory } from '../../injector/module-token-factory';
|
||||
|
||||
@@ -16,30 +14,20 @@ describe('ModuleTokenFactory', () => {
|
||||
class Module {}
|
||||
it('should return expected token', () => {
|
||||
const type = Module;
|
||||
const token = factory.create(type, undefined);
|
||||
expect(token).to.be.deep.eq(
|
||||
hash({
|
||||
id: moduleId,
|
||||
module: Module.name,
|
||||
dynamic: '',
|
||||
}),
|
||||
);
|
||||
const token1 = factory.create(type, undefined);
|
||||
const token2 = factory.create(type, undefined);
|
||||
expect(token1).to.be.deep.eq(token2);
|
||||
});
|
||||
it('should include dynamic metadata', () => {
|
||||
const type = Module;
|
||||
const token = factory.create(type, {
|
||||
const token1 = factory.create(type, {
|
||||
providers: [{}],
|
||||
} as any);
|
||||
const token2 = factory.create(type, {
|
||||
providers: [{}],
|
||||
} as any);
|
||||
|
||||
expect(token).to.be.deep.eq(
|
||||
hash({
|
||||
id: moduleId,
|
||||
module: Module.name,
|
||||
dynamic: stringify({
|
||||
providers: [{}],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(token1).to.be.deep.eq(token2);
|
||||
});
|
||||
});
|
||||
describe('getModuleName', () => {
|
||||
@@ -48,18 +36,18 @@ describe('ModuleTokenFactory', () => {
|
||||
expect(factory.getModuleName(metatype as any)).to.be.eql(metatype.name);
|
||||
});
|
||||
});
|
||||
describe('getDynamicMetadataToken', () => {
|
||||
describe('getStringifiedOpaqueToken', () => {
|
||||
describe('when metadata exists', () => {
|
||||
it('should return hash', () => {
|
||||
const metadata = { providers: ['', {}] };
|
||||
expect(factory.getDynamicMetadataToken(metadata as any)).to.be.eql(
|
||||
expect(factory.getStringifiedOpaqueToken(metadata as any)).to.be.eql(
|
||||
JSON.stringify(metadata),
|
||||
);
|
||||
});
|
||||
it('should return hash with class', () => {
|
||||
class Provider {}
|
||||
const metadata = { providers: [Provider], exports: [Provider] };
|
||||
expect(factory.getDynamicMetadataToken(metadata)).to.be.eql(
|
||||
expect(factory.getStringifiedOpaqueToken(metadata)).to.be.eql(
|
||||
'{"providers":["Provider"],"exports":["Provider"]}',
|
||||
);
|
||||
});
|
||||
@@ -69,10 +57,8 @@ describe('ModuleTokenFactory', () => {
|
||||
useValue: function Provider() {},
|
||||
};
|
||||
const metadata = { providers: [provider] };
|
||||
expect(factory.getDynamicMetadataToken(metadata)).to.be.eql(
|
||||
`{"providers":[{"provide":"ProvideValue","useValue":"${hash(
|
||||
provider.useValue.toString(),
|
||||
)}"}]}`,
|
||||
expect(factory.getStringifiedOpaqueToken(metadata)).to.be.eql(
|
||||
`{"providers":[{"provide":"ProvideValue","useValue":"${provider.useValue.toString()}"}]}`,
|
||||
);
|
||||
});
|
||||
it('should serialize symbols in a dynamic metadata object', () => {
|
||||
@@ -89,14 +75,14 @@ describe('ModuleTokenFactory', () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(factory.getDynamicMetadataToken(metadata)).to.be.eql(
|
||||
expect(factory.getStringifiedOpaqueToken(metadata)).to.be.eql(
|
||||
'{"providers":[{"provide":"Symbol(a)","useValue":"a"},{"provide":"Symbol(b)","useValue":"b"}]}',
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('when metadata does not exist', () => {
|
||||
it('should return empty string', () => {
|
||||
expect(factory.getDynamicMetadataToken(undefined)).to.be.eql('');
|
||||
expect(factory.getStringifiedOpaqueToken(undefined)).to.be.eql('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,12 +30,32 @@ describe('MetadataScanner', () => {
|
||||
}
|
||||
|
||||
it('should return only methods', () => {
|
||||
const methods = scanner.scanFromPrototype(
|
||||
const methods = scanner.getAllMethodNames(Test.prototype);
|
||||
expect(methods).to.eql(['test', 'test2', 'testParent', 'testParent2']);
|
||||
});
|
||||
|
||||
it('should return the same instance for the same prototype', () => {
|
||||
const methods1 = scanner.getAllMethodNames(Test.prototype);
|
||||
const methods2 = scanner.getAllMethodNames(Test.prototype);
|
||||
expect(methods1 === methods2).to.eql(true);
|
||||
});
|
||||
|
||||
it('should keep compatibility with older methods', () => {
|
||||
const methods1 = scanner.getAllMethodNames(Test.prototype).map(m => m[0]);
|
||||
const methods2 = scanner.scanFromPrototype(
|
||||
new Test(),
|
||||
Test.prototype,
|
||||
a => a,
|
||||
r => r[0],
|
||||
);
|
||||
expect(methods).to.eql(['test', 'test2', 'testParent', 'testParent2']);
|
||||
|
||||
expect(methods1).to.eql(methods2);
|
||||
|
||||
const methods3 = scanner.getAllMethodNames(Test.prototype);
|
||||
const methods4 = [
|
||||
...new Set(scanner.getAllFilteredMethodNames(Test.prototype)),
|
||||
];
|
||||
|
||||
expect(methods3).to.eql(methods4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { expect } from 'chai';
|
||||
import { Controller, Get, RequestMethod, Version } from '../../../common';
|
||||
import { ApplicationConfig } from '../../application-config';
|
||||
import { NestContainer } from '../../injector/container';
|
||||
import { MiddlewareBuilder } from '../../middleware/builder';
|
||||
import { RouteInfoPathExtractor } from '../../middleware/route-info-path-extractor';
|
||||
import { RoutesMapper } from '../../middleware/routes-mapper';
|
||||
import { NoopHttpAdapter } from './../utils/noop-adapter.spec';
|
||||
|
||||
@@ -10,9 +12,11 @@ describe('MiddlewareBuilder', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const container = new NestContainer();
|
||||
const appConfig = new ApplicationConfig();
|
||||
builder = new MiddlewareBuilder(
|
||||
new RoutesMapper(container),
|
||||
new NoopHttpAdapter({}),
|
||||
new RouteInfoPathExtractor(appConfig),
|
||||
);
|
||||
});
|
||||
describe('apply', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { RoutePathFactory } from '@nestjs/core/router/route-path-factory';
|
||||
import { RouteInfoPathExtractor } from '@nestjs/core/middleware/route-info-path-extractor';
|
||||
import * as chai from 'chai';
|
||||
import { expect } from 'chai';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
@@ -47,15 +47,21 @@ describe('MiddlewareModule', () => {
|
||||
beforeEach(() => {
|
||||
const container = new NestContainer();
|
||||
const appConfig = new ApplicationConfig();
|
||||
|
||||
graphInspector = new GraphInspector(container);
|
||||
middlewareModule = new MiddlewareModule(new RoutePathFactory(appConfig));
|
||||
middlewareModule = new MiddlewareModule();
|
||||
middlewareModule['routerExceptionFilter'] = new RouterExceptionFilters(
|
||||
new NestContainer(),
|
||||
appConfig,
|
||||
new NoopHttpAdapter({}),
|
||||
);
|
||||
middlewareModule['routeInfoPathExtractor'] = new RouteInfoPathExtractor(
|
||||
appConfig,
|
||||
);
|
||||
middlewareModule['routerExceptionFilter'] = new RouterExceptionFilters(
|
||||
container,
|
||||
appConfig,
|
||||
new NoopHttpAdapter({}),
|
||||
);
|
||||
middlewareModule['config'] = appConfig;
|
||||
middlewareModule['graphInspector'] = graphInspector;
|
||||
});
|
||||
|
||||
@@ -86,6 +92,7 @@ describe('MiddlewareModule', () => {
|
||||
new MiddlewareBuilder(
|
||||
(middlewareModule as any).routesMapper,
|
||||
undefined,
|
||||
new RouteInfoPathExtractor(new ApplicationConfig()),
|
||||
),
|
||||
),
|
||||
).to.be.true;
|
||||
|
||||
185
packages/core/test/middleware/route-info-path-extractor.spec.ts
Normal file
185
packages/core/test/middleware/route-info-path-extractor.spec.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { RequestMethod, VersioningType } from '@nestjs/common';
|
||||
import { ApplicationConfig } from '@nestjs/core';
|
||||
import { mapToExcludeRoute } from '@nestjs/core/middleware/utils';
|
||||
import { expect } from 'chai';
|
||||
import { RouteInfoPathExtractor } from './../../middleware/route-info-path-extractor';
|
||||
|
||||
describe('RouteInfoPathExtractor', () => {
|
||||
describe('extractPathsFrom', () => {
|
||||
let appConfig: ApplicationConfig;
|
||||
let routeInfoPathExtractor: RouteInfoPathExtractor;
|
||||
|
||||
beforeEach(() => {
|
||||
appConfig = new ApplicationConfig();
|
||||
appConfig.enableVersioning({
|
||||
type: VersioningType.URI,
|
||||
});
|
||||
routeInfoPathExtractor = new RouteInfoPathExtractor(appConfig);
|
||||
});
|
||||
|
||||
it(`should return correct paths`, () => {
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathsFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
}),
|
||||
).to.eql(['/*']);
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathsFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql(['/v1/*']);
|
||||
});
|
||||
|
||||
it(`should return correct paths when set global prefix`, () => {
|
||||
Reflect.set(routeInfoPathExtractor, 'prefixPath', '/api');
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathsFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
}),
|
||||
).to.eql(['/api/*']);
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathsFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql(['/api/v1/*']);
|
||||
});
|
||||
|
||||
it(`should return correct paths when set global prefix and global prefix options`, () => {
|
||||
Reflect.set(routeInfoPathExtractor, 'prefixPath', '/api');
|
||||
Reflect.set(
|
||||
routeInfoPathExtractor,
|
||||
'excludedGlobalPrefixRoutes',
|
||||
mapToExcludeRoute(['foo']),
|
||||
);
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathsFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
}),
|
||||
).to.eql(['/api/*', '/foo']);
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathsFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql(['/api/v1/*', '/v1/foo']);
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathsFrom({
|
||||
path: 'foo',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql(['/v1/foo']);
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathsFrom({
|
||||
path: 'bar',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql(['/api/v1/bar']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractPathFrom', () => {
|
||||
let appConfig: ApplicationConfig;
|
||||
let routeInfoPathExtractor: RouteInfoPathExtractor;
|
||||
|
||||
beforeEach(() => {
|
||||
appConfig = new ApplicationConfig();
|
||||
appConfig.enableVersioning({
|
||||
type: VersioningType.URI,
|
||||
});
|
||||
routeInfoPathExtractor = new RouteInfoPathExtractor(appConfig);
|
||||
});
|
||||
|
||||
it(`should return correct path`, () => {
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
}),
|
||||
).to.eql('/*');
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql('/v1/*');
|
||||
});
|
||||
|
||||
it(`should return correct path when set global prefix`, () => {
|
||||
Reflect.set(routeInfoPathExtractor, 'prefixPath', '/api');
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
}),
|
||||
).to.eql('/*');
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql('/api/v1/*');
|
||||
});
|
||||
|
||||
it(`should return correct path when set global prefix and global prefix options`, () => {
|
||||
Reflect.set(routeInfoPathExtractor, 'prefixPath', '/api');
|
||||
Reflect.set(
|
||||
routeInfoPathExtractor,
|
||||
'excludedGlobalPrefixRoutes',
|
||||
mapToExcludeRoute(['foo']),
|
||||
);
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
}),
|
||||
).to.eql('/*');
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathFrom({
|
||||
path: '*',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql('/api/v1/*');
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathFrom({
|
||||
path: 'foo',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql('/v1/foo');
|
||||
|
||||
expect(
|
||||
routeInfoPathExtractor.extractPathFrom({
|
||||
path: 'bar',
|
||||
method: RequestMethod.ALL,
|
||||
version: '1',
|
||||
}),
|
||||
).to.eql('/api/v1/bar');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RequestMethod, Type } from '@nestjs/common';
|
||||
import { addLeadingSlash } from '@nestjs/common/utils/shared.utils';
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
mapToExcludeRoute,
|
||||
} from '../../middleware/utils';
|
||||
import { NoopHttpAdapter } from '../utils/noop-adapter.spec';
|
||||
import * as pathToRegexp from 'path-to-regexp';
|
||||
|
||||
describe('middleware utils', () => {
|
||||
const noopAdapter = new NoopHttpAdapter({});
|
||||
@@ -17,6 +19,27 @@ describe('middleware utils', () => {
|
||||
class Test {}
|
||||
function fnMiddleware(req, res, next) {}
|
||||
|
||||
describe('mapToExcludeRoute', () => {
|
||||
it('should return exclude route metadata', () => {
|
||||
const stringRoute = 'foo';
|
||||
const routeInfo = {
|
||||
path: 'bar',
|
||||
method: RequestMethod.GET,
|
||||
};
|
||||
expect(mapToExcludeRoute([stringRoute, routeInfo])).to.eql([
|
||||
{
|
||||
path: stringRoute,
|
||||
requestMethod: RequestMethod.ALL,
|
||||
pathRegex: pathToRegexp(addLeadingSlash(stringRoute)),
|
||||
},
|
||||
{
|
||||
path: routeInfo.path,
|
||||
requestMethod: routeInfo.method,
|
||||
pathRegex: pathToRegexp(addLeadingSlash(routeInfo.path)),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('filterMiddleware', () => {
|
||||
let middleware: any[];
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { RequestMethod } from '@nestjs/common';
|
||||
import { expect } from 'chai';
|
||||
import { ApplicationConfig } from '../application-config';
|
||||
import { NestContainer } from '../injector/container';
|
||||
import { GraphInspector } from '../inspector/graph-inspector';
|
||||
import { NestApplication } from '../nest-application';
|
||||
import { mapToExcludeRoute } from './../middleware/utils';
|
||||
import { NoopHttpAdapter } from './utils/noop-adapter.spec';
|
||||
|
||||
describe('NestApplication', () => {
|
||||
@@ -54,4 +56,24 @@ describe('NestApplication', () => {
|
||||
).to.equal(1);
|
||||
});
|
||||
});
|
||||
describe('Global Prefix', () => {
|
||||
it('should get correct global prefix options', () => {
|
||||
const applicationConfig = new ApplicationConfig();
|
||||
const container = new NestContainer(applicationConfig);
|
||||
const instance = new NestApplication(
|
||||
container,
|
||||
new NoopHttpAdapter({}),
|
||||
applicationConfig,
|
||||
new GraphInspector(container),
|
||||
{},
|
||||
);
|
||||
const excludeRoute = ['foo', { path: 'bar', method: RequestMethod.GET }];
|
||||
instance.setGlobalPrefix('api', {
|
||||
exclude: excludeRoute,
|
||||
});
|
||||
expect(applicationConfig.getGlobalPrefixOptions()).to.eql({
|
||||
exclude: mapToExcludeRoute(excludeRoute),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -247,6 +247,7 @@ describe('RoutePathFactory', () => {
|
||||
sinon.stub(applicationConfig, 'getGlobalPrefixOptions').returns({
|
||||
exclude: [
|
||||
{
|
||||
path: '/random',
|
||||
pathRegex: pathToRegexp('/random'),
|
||||
requestMethod: RequestMethod.ALL,
|
||||
},
|
||||
@@ -265,6 +266,7 @@ describe('RoutePathFactory', () => {
|
||||
sinon.stub(applicationConfig, 'getGlobalPrefixOptions').returns({
|
||||
exclude: [
|
||||
{
|
||||
path: '/cats',
|
||||
pathRegex: pathToRegexp('/cats'),
|
||||
requestMethod: RequestMethod.ALL,
|
||||
},
|
||||
@@ -283,6 +285,7 @@ describe('RoutePathFactory', () => {
|
||||
sinon.stub(applicationConfig, 'getGlobalPrefixOptions').returns({
|
||||
exclude: [
|
||||
{
|
||||
path: '/cats',
|
||||
pathRegex: pathToRegexp('/cats'),
|
||||
requestMethod: RequestMethod.GET,
|
||||
},
|
||||
|
||||
@@ -42,13 +42,13 @@ let kafkaPackage: any = {};
|
||||
|
||||
export class ClientKafka extends ClientProxy {
|
||||
protected logger = new Logger(ClientKafka.name);
|
||||
protected client: Kafka = null;
|
||||
protected consumer: Consumer = null;
|
||||
protected producer: Producer = null;
|
||||
protected parser: KafkaParser = null;
|
||||
protected client: Kafka | null = null;
|
||||
protected consumer: Consumer | null = null;
|
||||
protected producer: Producer | null = null;
|
||||
protected parser: KafkaParser | null = null;
|
||||
protected initialized: Promise<void> | null = null;
|
||||
protected responsePatterns: string[] = [];
|
||||
protected consumerAssignments: { [key: string]: number } = {};
|
||||
|
||||
protected brokers: string[] | BrokersFunction;
|
||||
protected clientId: string;
|
||||
protected groupId: string;
|
||||
@@ -94,46 +94,56 @@ export class ClientKafka extends ClientProxy {
|
||||
this.consumer && (await this.consumer.disconnect());
|
||||
this.producer = null;
|
||||
this.consumer = null;
|
||||
this.initialized = null;
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
public async connect(): Promise<Producer> {
|
||||
if (this.client) {
|
||||
return this.producer;
|
||||
if (this.initialized) {
|
||||
return this.initialized.then(() => this.producer);
|
||||
}
|
||||
this.client = this.createClient();
|
||||
this.initialized = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
this.client = this.createClient();
|
||||
|
||||
if (!this.producerOnlyMode) {
|
||||
const partitionAssigners = [
|
||||
(
|
||||
config: ConstructorParameters<typeof KafkaReplyPartitionAssigner>[1],
|
||||
) => new KafkaReplyPartitionAssigner(this, config),
|
||||
] as any[];
|
||||
if (!this.producerOnlyMode) {
|
||||
const partitionAssigners = [
|
||||
(
|
||||
config: ConstructorParameters<
|
||||
typeof KafkaReplyPartitionAssigner
|
||||
>[1],
|
||||
) => new KafkaReplyPartitionAssigner(this, config),
|
||||
];
|
||||
|
||||
const consumerOptions = Object.assign(
|
||||
{
|
||||
partitionAssigners,
|
||||
},
|
||||
this.options.consumer || {},
|
||||
{
|
||||
groupId: this.groupId,
|
||||
},
|
||||
);
|
||||
const consumerOptions = Object.assign(
|
||||
{
|
||||
partitionAssigners,
|
||||
},
|
||||
this.options.consumer || {},
|
||||
{
|
||||
groupId: this.groupId,
|
||||
},
|
||||
);
|
||||
|
||||
this.consumer = this.client.consumer(consumerOptions);
|
||||
// set member assignments on join and rebalance
|
||||
this.consumer.on(
|
||||
this.consumer.events.GROUP_JOIN,
|
||||
this.setConsumerAssignments.bind(this),
|
||||
);
|
||||
await this.consumer.connect();
|
||||
await this.bindTopics();
|
||||
}
|
||||
this.consumer = this.client.consumer(consumerOptions);
|
||||
// set member assignments on join and rebalance
|
||||
this.consumer.on(
|
||||
this.consumer.events.GROUP_JOIN,
|
||||
this.setConsumerAssignments.bind(this),
|
||||
);
|
||||
await this.consumer.connect();
|
||||
await this.bindTopics();
|
||||
}
|
||||
|
||||
this.producer = this.client.producer(this.options.producer || {});
|
||||
await this.producer.connect();
|
||||
this.producer = this.client.producer(this.options.producer || {});
|
||||
await this.producer.connect();
|
||||
|
||||
return this.producer;
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
return this.initialized.then(() => this.producer);
|
||||
}
|
||||
|
||||
public async bindTopics(): Promise<void> {
|
||||
|
||||
@@ -3,21 +3,29 @@ import { loadPackage } from '@nestjs/common/utils/load-package.util';
|
||||
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
|
||||
import { isFunction } from '@nestjs/common/utils/shared.utils';
|
||||
import { EventEmitter } from 'events';
|
||||
import { EmptyError, fromEvent, lastValueFrom, merge, Observable } from 'rxjs';
|
||||
import { first, map, retryWhen, scan, share, switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
EmptyError,
|
||||
firstValueFrom,
|
||||
fromEvent,
|
||||
merge,
|
||||
Observable,
|
||||
ReplaySubject,
|
||||
} from 'rxjs';
|
||||
import { first, map, retryWhen, scan, skip, switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
CONNECT_EVENT,
|
||||
CONNECT_FAILED_EVENT,
|
||||
DISCONNECTED_RMQ_MESSAGE,
|
||||
DISCONNECT_EVENT,
|
||||
ERROR_EVENT,
|
||||
RQM_DEFAULT_IS_GLOBAL_PREFETCH_COUNT,
|
||||
RQM_DEFAULT_NOACK,
|
||||
RQM_DEFAULT_NO_ASSERT,
|
||||
RQM_DEFAULT_PERSISTENT,
|
||||
RQM_DEFAULT_PREFETCH_COUNT,
|
||||
RQM_DEFAULT_QUEUE,
|
||||
RQM_DEFAULT_QUEUE_OPTIONS,
|
||||
RQM_DEFAULT_URL,
|
||||
RQM_DEFAULT_NO_ASSERT,
|
||||
} from '../constants';
|
||||
import { RmqUrl } from '../external/rmq-url.interface';
|
||||
import { ReadPacket, RmqOptions, WritePacket } from '../interfaces';
|
||||
@@ -25,18 +33,30 @@ import { RmqRecord } from '../record-builders';
|
||||
import { RmqRecordSerializer } from '../serializers/rmq-record.serializer';
|
||||
import { ClientProxy } from './client-proxy';
|
||||
|
||||
// import type {
|
||||
// AmqpConnectionManager,
|
||||
// ChannelWrapper,
|
||||
// } from 'amqp-connection-manager';
|
||||
// import type { Channel, ConsumeMessage } from 'amqplib';
|
||||
|
||||
type Channel = any;
|
||||
type ChannelWrapper = any;
|
||||
type ConsumeMessage = any;
|
||||
type AmqpConnectionManager = any;
|
||||
|
||||
let rqmPackage: any = {};
|
||||
|
||||
const REPLY_QUEUE = 'amq.rabbitmq.reply-to';
|
||||
|
||||
export class ClientRMQ extends ClientProxy {
|
||||
protected readonly logger = new Logger(ClientProxy.name);
|
||||
protected connection$: ReplaySubject<any>;
|
||||
protected connection: Promise<any>;
|
||||
protected client: any = null;
|
||||
protected channel: any = null;
|
||||
protected client: AmqpConnectionManager = null;
|
||||
protected channel: ChannelWrapper = null;
|
||||
protected urls: string[] | RmqUrl[];
|
||||
protected queue: string;
|
||||
protected queueOptions: any;
|
||||
protected queueOptions: Record<string, any>;
|
||||
protected responseEmitter: EventEmitter;
|
||||
protected replyQueue: string;
|
||||
protected persistent: boolean;
|
||||
@@ -75,42 +95,44 @@ export class ClientRMQ extends ClientProxy {
|
||||
|
||||
public connect(): Promise<any> {
|
||||
if (this.client) {
|
||||
return this.connection;
|
||||
return this.convertConnectionToPromise();
|
||||
}
|
||||
this.client = this.createClient();
|
||||
this.handleError(this.client);
|
||||
this.handleDisconnectError(this.client);
|
||||
|
||||
const connect$ = this.connect$(this.client);
|
||||
this.connection = lastValueFrom(
|
||||
this.mergeDisconnectEvent(this.client, connect$).pipe(
|
||||
switchMap(() => this.createChannel()),
|
||||
share(),
|
||||
),
|
||||
).catch(err => {
|
||||
if (err instanceof EmptyError) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
this.responseEmitter = new EventEmitter();
|
||||
this.responseEmitter.setMaxListeners(0);
|
||||
|
||||
return this.connection;
|
||||
const connect$ = this.connect$(this.client);
|
||||
const withDisconnect$ = this.mergeDisconnectEvent(
|
||||
this.client,
|
||||
connect$,
|
||||
).pipe(switchMap(() => this.createChannel()));
|
||||
|
||||
const withReconnect$ = fromEvent(this.client, CONNECT_EVENT).pipe(skip(1));
|
||||
const source$ = merge(withDisconnect$, withReconnect$);
|
||||
|
||||
this.connection$ = new ReplaySubject(1);
|
||||
source$.subscribe(this.connection$);
|
||||
|
||||
return this.convertConnectionToPromise();
|
||||
}
|
||||
|
||||
public createChannel(): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
this.channel = this.client.createChannel({
|
||||
json: false,
|
||||
setup: (channel: any) => this.setupChannel(channel, resolve),
|
||||
setup: (channel: Channel) => this.setupChannel(channel, resolve),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public createClient<T = any>(): T {
|
||||
public createClient(): AmqpConnectionManager {
|
||||
const socketOptions = this.getOptionsProp(this.options, 'socketOptions');
|
||||
return rqmPackage.connect(this.urls, {
|
||||
connectionOptions: socketOptions,
|
||||
}) as T;
|
||||
});
|
||||
}
|
||||
|
||||
public mergeDisconnectEvent<T = any>(
|
||||
@@ -119,7 +141,7 @@ export class ClientRMQ extends ClientProxy {
|
||||
): Observable<T> {
|
||||
const eventToError = (eventType: string) =>
|
||||
fromEvent(instance, eventType).pipe(
|
||||
map((err: any) => {
|
||||
map((err: unknown) => {
|
||||
throw err;
|
||||
}),
|
||||
);
|
||||
@@ -138,10 +160,23 @@ export class ClientRMQ extends ClientProxy {
|
||||
),
|
||||
),
|
||||
);
|
||||
// If we ever decide to propagate all disconnect errors & re-emit them through
|
||||
// the "connection" stream then comment out "first()" operator.
|
||||
return merge(source$, disconnect$, connectFailed$).pipe(first());
|
||||
}
|
||||
|
||||
public async setupChannel(channel: any, resolve: Function) {
|
||||
public async convertConnectionToPromise() {
|
||||
try {
|
||||
return await firstValueFrom(this.connection$);
|
||||
} catch (err) {
|
||||
if (err instanceof EmptyError) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async setupChannel(channel: Channel, resolve: Function) {
|
||||
const prefetchCount =
|
||||
this.getOptionsProp(this.options, 'prefetchCount') ||
|
||||
RQM_DEFAULT_PREFETCH_COUNT;
|
||||
@@ -153,18 +188,15 @@ export class ClientRMQ extends ClientProxy {
|
||||
await channel.assertQueue(this.queue, this.queueOptions);
|
||||
}
|
||||
await channel.prefetch(prefetchCount, isGlobalPrefetchCount);
|
||||
|
||||
this.responseEmitter = new EventEmitter();
|
||||
this.responseEmitter.setMaxListeners(0);
|
||||
await this.consumeChannel(channel);
|
||||
resolve();
|
||||
}
|
||||
|
||||
public async consumeChannel(channel: any) {
|
||||
public async consumeChannel(channel: Channel) {
|
||||
const noAck = this.getOptionsProp(this.options, 'noAck', RQM_DEFAULT_NOACK);
|
||||
await channel.consume(
|
||||
this.replyQueue,
|
||||
(msg: any) =>
|
||||
(msg: ConsumeMessage) =>
|
||||
this.responseEmitter.emit(msg.properties.correlationId, msg),
|
||||
{
|
||||
noAck,
|
||||
@@ -172,16 +204,14 @@ export class ClientRMQ extends ClientProxy {
|
||||
);
|
||||
}
|
||||
|
||||
public handleError(client: any): void {
|
||||
public handleError(client: AmqpConnectionManager): void {
|
||||
client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err));
|
||||
}
|
||||
|
||||
public handleDisconnectError(client: any): void {
|
||||
public handleDisconnectError(client: AmqpConnectionManager): void {
|
||||
client.addListener(DISCONNECT_EVENT, (err: any) => {
|
||||
this.logger.error(DISCONNECTED_RMQ_MESSAGE);
|
||||
this.logger.error(err);
|
||||
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,7 +261,7 @@ export class ClientRMQ extends ClientProxy {
|
||||
content,
|
||||
options,
|
||||
}: {
|
||||
content: any;
|
||||
content: Buffer;
|
||||
options: Record<string, unknown>;
|
||||
}) =>
|
||||
this.handleMessage(JSON.parse(content.toString()), options, callback);
|
||||
@@ -244,17 +274,19 @@ export class ClientRMQ extends ClientProxy {
|
||||
delete serializedPacket.options;
|
||||
|
||||
this.responseEmitter.on(correlationId, listener);
|
||||
this.channel.sendToQueue(
|
||||
this.queue,
|
||||
Buffer.from(JSON.stringify(serializedPacket)),
|
||||
{
|
||||
replyTo: this.replyQueue,
|
||||
persistent: this.persistent,
|
||||
...options,
|
||||
headers: this.mergeHeaders(options?.headers),
|
||||
correlationId,
|
||||
},
|
||||
);
|
||||
this.channel
|
||||
.sendToQueue(
|
||||
this.queue,
|
||||
Buffer.from(JSON.stringify(serializedPacket)),
|
||||
{
|
||||
replyTo: this.replyQueue,
|
||||
persistent: this.persistent,
|
||||
...options,
|
||||
headers: this.mergeHeaders(options?.headers),
|
||||
correlationId,
|
||||
},
|
||||
)
|
||||
.catch(err => callback({ err }));
|
||||
return () => this.responseEmitter.removeListener(correlationId, listener);
|
||||
} catch (err) {
|
||||
callback({ err });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Nest @microservices
|
||||
* Copyright(c) 2017 - 2022 Kamil Mysliwiec
|
||||
* Copyright(c) 2017 - 2023 Kamil Mysliwiec
|
||||
* https://nestjs.com
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
@@ -4,10 +4,10 @@ import { MetadataScanner } from '@nestjs/core/metadata-scanner';
|
||||
import {
|
||||
CLIENT_CONFIGURATION_METADATA,
|
||||
CLIENT_METADATA,
|
||||
PATTERN_EXTRAS_METADATA,
|
||||
PATTERN_HANDLER_METADATA,
|
||||
PATTERN_METADATA,
|
||||
TRANSPORT_METADATA,
|
||||
PATTERN_EXTRAS_METADATA,
|
||||
} from './constants';
|
||||
import { Transport } from './enums';
|
||||
import { PatternHandler } from './enums/pattern-handler.enum';
|
||||
@@ -37,12 +37,10 @@ export class ListenerMetadataExplorer {
|
||||
|
||||
public explore(instance: Controller): EventOrMessageListenerDefinition[] {
|
||||
const instancePrototype = Object.getPrototypeOf(instance);
|
||||
return this.metadataScanner.scanFromPrototype<
|
||||
Controller,
|
||||
EventOrMessageListenerDefinition
|
||||
>(instance, instancePrototype, method =>
|
||||
this.exploreMethodMetadata(instancePrototype, method),
|
||||
);
|
||||
return this.metadataScanner
|
||||
.getAllMethodNames(instancePrototype)
|
||||
.map(method => this.exploreMethodMetadata(instancePrototype, method))
|
||||
.filter(metadata => metadata);
|
||||
}
|
||||
|
||||
public exploreMethodMetadata(
|
||||
|
||||
@@ -14,7 +14,15 @@ import { Module } from '@nestjs/core/injector/module';
|
||||
import { GraphInspector } from '@nestjs/core/inspector/graph-inspector';
|
||||
import { MetadataScanner } from '@nestjs/core/metadata-scanner';
|
||||
import { REQUEST_CONTEXT_ID } from '@nestjs/core/router/request/request-constants';
|
||||
import { connectable, Observable, Subject } from 'rxjs';
|
||||
import {
|
||||
forkJoin,
|
||||
from as fromPromise,
|
||||
isObservable,
|
||||
mergeMap,
|
||||
Observable,
|
||||
ObservedValueOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
import { IClientProxyFactory } from './client/client-proxy-factory';
|
||||
import { ClientsContainer } from './container';
|
||||
import { ExceptionFiltersContext } from './context/exception-filters-context';
|
||||
@@ -111,20 +119,18 @@ export class ListenersController {
|
||||
defaultCallMetadata,
|
||||
);
|
||||
if (isEventHandler) {
|
||||
const eventHandler: MessageHandler = (...args: unknown[]) => {
|
||||
const eventHandler: MessageHandler = async (...args: unknown[]) => {
|
||||
const originalArgs = args;
|
||||
const [dataOrContextHost] = originalArgs;
|
||||
if (dataOrContextHost instanceof RequestContextHost) {
|
||||
args = args.slice(1, args.length);
|
||||
}
|
||||
const originalReturnValue = proxy(...args);
|
||||
const returnedValueWrapper = eventHandler.next?.(
|
||||
...(originalArgs as Parameters<MessageHandler>),
|
||||
const returnValue = proxy(...args);
|
||||
return this.forkJoinHandlersIfAttached(
|
||||
returnValue,
|
||||
originalArgs,
|
||||
eventHandler,
|
||||
);
|
||||
returnedValueWrapper?.then(returnedValue =>
|
||||
this.connectIfStream(returnedValue as Observable<unknown>),
|
||||
);
|
||||
return originalReturnValue;
|
||||
};
|
||||
return server.addHandler(
|
||||
pattern,
|
||||
@@ -143,6 +149,7 @@ export class ListenersController {
|
||||
moduleKey,
|
||||
methodKey,
|
||||
defaultCallMetadata,
|
||||
isEventHandler,
|
||||
);
|
||||
server.addHandler(pattern, asyncHandler, isEventHandler, extras);
|
||||
});
|
||||
@@ -174,6 +181,23 @@ export class ListenersController {
|
||||
);
|
||||
}
|
||||
|
||||
public forkJoinHandlersIfAttached(
|
||||
currentReturnValue: Promise<unknown> | Observable<unknown>,
|
||||
originalArgs: unknown[],
|
||||
handlerRef: MessageHandler,
|
||||
) {
|
||||
if (handlerRef.next) {
|
||||
const returnedValueWrapper = handlerRef.next(
|
||||
...(originalArgs as Parameters<MessageHandler>),
|
||||
);
|
||||
return forkJoin({
|
||||
current: this.transformToObservable(currentReturnValue),
|
||||
next: this.transformToObservable(returnedValueWrapper),
|
||||
});
|
||||
}
|
||||
return currentReturnValue;
|
||||
}
|
||||
|
||||
public assignClientsToProperties(instance: Controller | Injectable) {
|
||||
for (const {
|
||||
property,
|
||||
@@ -201,17 +225,20 @@ export class ListenersController {
|
||||
moduleKey: string,
|
||||
methodKey: string,
|
||||
defaultCallMetadata: Record<string, any> = DEFAULT_CALLBACK_METADATA,
|
||||
isEventHandler = false,
|
||||
) {
|
||||
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,13 +247,10 @@ 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(
|
||||
instance,
|
||||
moduleRef,
|
||||
@@ -242,14 +266,16 @@ export class ListenersController {
|
||||
wrapper.id,
|
||||
defaultCallMetadata,
|
||||
);
|
||||
const returnedValueWrapper = requestScopedHandler.next?.(
|
||||
dataOrContextHost,
|
||||
...args,
|
||||
);
|
||||
returnedValueWrapper?.then(returnedValue =>
|
||||
this.connectIfStream(returnedValue as Observable<unknown>),
|
||||
);
|
||||
return proxy(...args);
|
||||
|
||||
const returnValue = proxy(...args);
|
||||
if (isEventHandler) {
|
||||
return this.forkJoinHandlersIfAttached(
|
||||
returnValue,
|
||||
[dataOrContextHost, ...args],
|
||||
requestScopedHandler,
|
||||
);
|
||||
}
|
||||
return returnValue;
|
||||
} catch (err) {
|
||||
let exceptionFilter = this.exceptionFiltersCache.get(
|
||||
instance[methodKey],
|
||||
@@ -270,7 +296,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,22 +308,32 @@ 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;
|
||||
}
|
||||
|
||||
private connectIfStream(source: Observable<unknown>) {
|
||||
if (!source) {
|
||||
return;
|
||||
public transformToObservable<T>(
|
||||
resultOrDeferred: Observable<T> | Promise<T>,
|
||||
): Observable<T>;
|
||||
public transformToObservable<T>(
|
||||
resultOrDeferred: T,
|
||||
): never extends Observable<ObservedValueOf<T>>
|
||||
? Observable<T>
|
||||
: Observable<ObservedValueOf<T>>;
|
||||
public transformToObservable(resultOrDeferred: any) {
|
||||
if (resultOrDeferred instanceof Promise) {
|
||||
return fromPromise(resultOrDeferred).pipe(
|
||||
mergeMap(val => (isObservable(val) ? val : of(val))),
|
||||
);
|
||||
}
|
||||
const connectableSource = connectable(source, {
|
||||
connector: () => new Subject(),
|
||||
resetOnDisconnect: false,
|
||||
});
|
||||
connectableSource.connect();
|
||||
|
||||
if (isObservable(resultOrDeferred)) {
|
||||
return resultOrDeferred;
|
||||
}
|
||||
|
||||
return of(resultOrDeferred);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class MicroservicesModule<
|
||||
if (!this.listenersController) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
if (this.appOptions.preview) {
|
||||
if (this.appOptions?.preview) {
|
||||
return;
|
||||
}
|
||||
const modules = container.getModules();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/microservices",
|
||||
"version": "9.3.0-beta.3",
|
||||
"version": "9.3.3",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@microservices)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -18,11 +18,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"tslib": "2.4.1"
|
||||
"tslib": "2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "^9.3.0-beta.3",
|
||||
"@nestjs/core": "^9.3.0-beta.3"
|
||||
"@nestjs/common": "9.3.3",
|
||||
"@nestjs/core": "9.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grpc/grpc-js": "*",
|
||||
|
||||
@@ -191,7 +191,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
}
|
||||
|
||||
const response$ = this.transformToObservable(
|
||||
await handler(packet.data, kafkaContext),
|
||||
handler(packet.data, kafkaContext),
|
||||
);
|
||||
|
||||
const replayStream$ = new ReplaySubject();
|
||||
@@ -218,6 +218,8 @@ export class ServerKafka extends Server implements CustomTransportStrategy {
|
||||
if (err instanceof KafkaRetriableException && !isPromiseResolved) {
|
||||
isPromiseResolved = true;
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
replayStream$.error(err);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { isUndefined, isObject } from '@nestjs/common/utils/shared.utils';
|
||||
import { Observable } from 'rxjs';
|
||||
import { NATS_DEFAULT_URL, NO_MESSAGE_HANDLER } from '../constants';
|
||||
import { NatsContext } from '../ctx-host/nats.context';
|
||||
import { NatsRequestJSONDeserializer } from '../deserializers/nats-request-json.deserializer';
|
||||
@@ -137,17 +136,28 @@ export class ServerNats extends Server implements CustomTransportStrategy {
|
||||
status.data && isObject(status.data)
|
||||
? JSON.stringify(status.data)
|
||||
: status.data;
|
||||
if (status.type === 'disconnect' || status.type === 'error') {
|
||||
this.logger.error(
|
||||
`NatsError: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
} else {
|
||||
const message = `NatsStatus: type: "${status.type}", data: "${data}".`;
|
||||
if (status.type === 'pingTimer') {
|
||||
this.logger.debug(message);
|
||||
} else {
|
||||
this.logger.log(message);
|
||||
}
|
||||
|
||||
switch (status.type) {
|
||||
case 'error':
|
||||
case 'disconnect':
|
||||
this.logger.error(
|
||||
`NatsError: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'pingTimer':
|
||||
if (this.options.debug) {
|
||||
this.logger.debug(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.log(
|
||||
`NatsStatus: type: "${status.type}", data: "${data}".`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Subject,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import { catchError, finalize } from 'rxjs/operators';
|
||||
import { catchError, finalize, mergeMap } from 'rxjs/operators';
|
||||
import { NO_EVENT_HANDLER } from '../constants';
|
||||
import { BaseRpcContext } from '../ctx-host/base-rpc.context';
|
||||
import { IncomingRequestDeserializer } from '../deserializers/incoming-request.deserializer';
|
||||
@@ -133,7 +133,9 @@ export abstract class Server {
|
||||
: Observable<ObservedValueOf<T>>;
|
||||
public transformToObservable(resultOrDeferred: any) {
|
||||
if (resultOrDeferred instanceof Promise) {
|
||||
return fromPromise(resultOrDeferred);
|
||||
return fromPromise(resultOrDeferred).pipe(
|
||||
mergeMap(val => (isObservable(val) ? val : of(val))),
|
||||
);
|
||||
}
|
||||
|
||||
if (isObservable(resultOrDeferred)) {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from '../../external/kafka.interface';
|
||||
|
||||
describe('ClientKafka', () => {
|
||||
// static
|
||||
const topic = 'test.topic';
|
||||
const partition = 0;
|
||||
const replyTopic = 'test.topic.reply';
|
||||
@@ -25,7 +24,6 @@ describe('ClientKafka', () => {
|
||||
const heartbeat = async () => {};
|
||||
const pause = () => () => {};
|
||||
|
||||
// message
|
||||
const message: KafkaMessage = {
|
||||
key: Buffer.from(key),
|
||||
offset,
|
||||
@@ -35,7 +33,6 @@ describe('ClientKafka', () => {
|
||||
attributes,
|
||||
};
|
||||
|
||||
// deserialized message
|
||||
const deserializedMessage: any = {
|
||||
key,
|
||||
offset,
|
||||
@@ -47,7 +44,6 @@ describe('ClientKafka', () => {
|
||||
partition,
|
||||
};
|
||||
|
||||
// payloads
|
||||
const payload: EachMessagePayload = {
|
||||
topic,
|
||||
partition,
|
||||
@@ -152,26 +148,6 @@ describe('ClientKafka', () => {
|
||||
pause,
|
||||
};
|
||||
|
||||
const deserializedPayloadError: EachMessagePayload = {
|
||||
topic,
|
||||
partition,
|
||||
message: Object.assign(
|
||||
{
|
||||
headers: {
|
||||
[KafkaHeaders.CORRELATION_ID]: correlationId,
|
||||
[KafkaHeaders.NEST_ERR]: NO_MESSAGE_HANDLER,
|
||||
},
|
||||
},
|
||||
deserializedMessage,
|
||||
{
|
||||
size: 0,
|
||||
value: null,
|
||||
},
|
||||
),
|
||||
heartbeat,
|
||||
pause,
|
||||
};
|
||||
|
||||
let client: ClientKafka;
|
||||
let callback: sinon.SinonSpy;
|
||||
let connect: sinon.SinonSpy;
|
||||
@@ -182,7 +158,7 @@ describe('ClientKafka', () => {
|
||||
let consumerStub: sinon.SinonStub;
|
||||
let producerStub: sinon.SinonStub;
|
||||
let createClientStub: sinon.SinonStub;
|
||||
let kafkaClient;
|
||||
let kafkaClient: any;
|
||||
|
||||
beforeEach(() => {
|
||||
client = new ClientKafka({});
|
||||
@@ -322,7 +298,8 @@ describe('ClientKafka', () => {
|
||||
});
|
||||
|
||||
it('should expect the connection to be reused', async () => {
|
||||
(client as any).client = kafkaClient;
|
||||
(client as any).initialized = Promise.resolve({});
|
||||
|
||||
await client.connect();
|
||||
|
||||
expect(createClientStub.calledOnce).to.be.false;
|
||||
@@ -368,7 +345,8 @@ describe('ClientKafka', () => {
|
||||
});
|
||||
|
||||
it('should expect the connection to be reused', async () => {
|
||||
(client as any).client = kafkaClient;
|
||||
(client as any).initialized = Promise.resolve({});
|
||||
|
||||
await client.connect();
|
||||
|
||||
expect(createClientStub.calledOnce).to.be.false;
|
||||
|
||||
@@ -15,7 +15,6 @@ describe('ClientRMQ', function () {
|
||||
let createClientStub: sinon.SinonStub;
|
||||
let handleErrorsSpy: sinon.SinonSpy;
|
||||
let connect$Stub: sinon.SinonStub;
|
||||
let mergeDisconnectEvent: sinon.SinonStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new ClientRMQ({});
|
||||
@@ -33,7 +32,7 @@ describe('ClientRMQ', function () {
|
||||
return this;
|
||||
},
|
||||
}));
|
||||
mergeDisconnectEvent = sinon
|
||||
sinon
|
||||
.stub(client, 'mergeDisconnectEvent')
|
||||
.callsFake((_, source) => source);
|
||||
});
|
||||
@@ -173,7 +172,7 @@ describe('ClientRMQ', function () {
|
||||
const pattern = 'test';
|
||||
let msg: ReadPacket;
|
||||
let connectSpy: sinon.SinonSpy,
|
||||
sendToQueueSpy: sinon.SinonSpy,
|
||||
sendToQueueStub: sinon.SinonStub,
|
||||
eventSpy: sinon.SinonSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -181,10 +180,10 @@ describe('ClientRMQ', function () {
|
||||
msg = { pattern, data: 'data' };
|
||||
connectSpy = sinon.spy(client, 'connect');
|
||||
eventSpy = sinon.spy();
|
||||
sendToQueueSpy = sinon.spy();
|
||||
sendToQueueStub = sinon.stub().callsFake(() => ({ catch: sinon.spy() }));
|
||||
|
||||
client['channel'] = {
|
||||
sendToQueue: sendToQueueSpy,
|
||||
sendToQueue: sendToQueueStub,
|
||||
};
|
||||
client['responseEmitter'] = new EventEmitter();
|
||||
client['responseEmitter'].on(pattern, eventSpy);
|
||||
@@ -196,15 +195,15 @@ describe('ClientRMQ', function () {
|
||||
|
||||
it('should send message to a proper queue', () => {
|
||||
client['publish'](msg, () => {
|
||||
expect(sendToQueueSpy.called).to.be.true;
|
||||
expect(sendToQueueSpy.getCall(0).args[0]).to.be.eql(client['queue']);
|
||||
expect(sendToQueueStub.called).to.be.true;
|
||||
expect(sendToQueueStub.getCall(0).args[0]).to.be.eql(client['queue']);
|
||||
});
|
||||
});
|
||||
|
||||
it('should send buffer from stringified message', () => {
|
||||
client['publish'](msg, () => {
|
||||
expect(sendToQueueSpy.called).to.be.true;
|
||||
expect(sendToQueueSpy.getCall(1).args[1]).to.be.eql(
|
||||
expect(sendToQueueStub.called).to.be.true;
|
||||
expect(sendToQueueStub.getCall(1).args[1]).to.be.eql(
|
||||
Buffer.from(JSON.stringify(msg)),
|
||||
);
|
||||
});
|
||||
@@ -231,7 +230,7 @@ describe('ClientRMQ', function () {
|
||||
describe('headers', () => {
|
||||
it('should not generate headers if none are configured', () => {
|
||||
client['publish'](msg, () => {
|
||||
expect(sendToQueueSpy.getCall(0).args[2].headers).to.be.undefined;
|
||||
expect(sendToQueueStub.getCall(0).args[2].headers).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -240,7 +239,7 @@ describe('ClientRMQ', function () {
|
||||
msg.data = new RmqRecord('data', { headers: requestHeaders });
|
||||
|
||||
client['publish'](msg, () => {
|
||||
expect(sendToQueueSpy.getCall(0).args[2].headers).to.eql(
|
||||
expect(sendToQueueStub.getCall(0).args[2].headers).to.eql(
|
||||
requestHeaders,
|
||||
);
|
||||
});
|
||||
@@ -254,7 +253,7 @@ describe('ClientRMQ', function () {
|
||||
msg.data = new RmqRecord('data', { headers: requestHeaders });
|
||||
|
||||
client['publish'](msg, () => {
|
||||
expect(sendToQueueSpy.getCall(0).args[2].headers).to.eql({
|
||||
expect(sendToQueueStub.getCall(0).args[2].headers).to.eql({
|
||||
...staticHeaders,
|
||||
...requestHeaders,
|
||||
});
|
||||
@@ -269,7 +268,7 @@ describe('ClientRMQ', function () {
|
||||
msg.data = new RmqRecord('data', { headers: requestHeaders });
|
||||
|
||||
client['publish'](msg, () => {
|
||||
expect(sendToQueueSpy.getCall(0).args[2].headers).to.eql(
|
||||
expect(sendToQueueStub.getCall(0).args[2].headers).to.eql(
|
||||
requestHeaders,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -52,17 +52,16 @@ describe('ListenerMetadataExplorer', () => {
|
||||
instance = new ListenerMetadataExplorer(scanner);
|
||||
});
|
||||
describe('explore', () => {
|
||||
let scanFromPrototype: sinon.SinonSpy;
|
||||
let getAllMethodNames: sinon.SinonSpy;
|
||||
beforeEach(() => {
|
||||
scanFromPrototype = sinon.spy(scanner, 'scanFromPrototype');
|
||||
getAllMethodNames = sinon.spy(scanner, 'getAllMethodNames');
|
||||
});
|
||||
it(`should call "scanFromPrototype" with expected arguments`, () => {
|
||||
const obj = new Test();
|
||||
instance.explore(obj);
|
||||
|
||||
const { args } = scanFromPrototype.getCall(0);
|
||||
expect(args[0]).to.be.eql(obj);
|
||||
expect(args[1]).to.be.eql(Object.getPrototypeOf(obj));
|
||||
const { args } = getAllMethodNames.getCall(0);
|
||||
expect(args[0]).to.be.eql(Object.getPrototypeOf(obj));
|
||||
});
|
||||
});
|
||||
describe('exploreMethodMetadata', () => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { Server } from 'net';
|
||||
import {
|
||||
HttpStatus,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
RequestMethod,
|
||||
StreamableFile,
|
||||
VersioningType,
|
||||
VersioningOptions,
|
||||
VersioningType,
|
||||
VERSION_NEUTRAL,
|
||||
} from '@nestjs/common';
|
||||
import { VersionValue } from '@nestjs/common/interfaces';
|
||||
@@ -29,11 +30,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';
|
||||
|
||||
@@ -46,6 +49,9 @@ type VersionedRoute = <
|
||||
next: () => void,
|
||||
) => any;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class ExpressAdapter extends AbstractHttpAdapter {
|
||||
private readonly routerMethodFactory = new RouterMethodFactory();
|
||||
private readonly logger = new Logger(ExpressAdapter.name);
|
||||
@@ -94,6 +100,16 @@ export class ExpressAdapter extends AbstractHttpAdapter {
|
||||
},
|
||||
);
|
||||
}
|
||||
if (
|
||||
response.getHeader('Content-Type') !== undefined &&
|
||||
response.getHeader('Content-Type') !== 'application/json' &&
|
||||
body?.statusCode >= HttpStatus.BAD_REQUEST
|
||||
) {
|
||||
this.logger.warn(
|
||||
"Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses",
|
||||
);
|
||||
response.setHeader('Content-Type', 'application/json');
|
||||
}
|
||||
return isObject(body) ? response.json(body) : response.send(String(body));
|
||||
}
|
||||
|
||||
@@ -235,6 +251,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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Nest @platform-express
|
||||
* Copyright(c) 2017 - 2022 Kamil Mysliwiec
|
||||
* Copyright(c) 2017 - 2023 Kamil Mysliwiec
|
||||
* https://nestjs.com
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './nest-express-application.interface';
|
||||
export * from './nest-express-body-parser-options.interface';
|
||||
|
||||
@@ -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).
|
||||
*
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Options } from 'body-parser';
|
||||
|
||||
export type NestExpressBodyParserOptions<T extends Options = Options> = Omit<
|
||||
T,
|
||||
'verify'
|
||||
>;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/platform-express",
|
||||
"version": "9.3.0-beta.3",
|
||||
"version": "9.3.3",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@platform-express)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -21,11 +21,11 @@
|
||||
"cors": "2.8.5",
|
||||
"express": "4.18.2",
|
||||
"multer": "1.4.4-lts.1",
|
||||
"tslib": "2.4.1"
|
||||
"tslib": "2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "^9.3.0-beta.3",
|
||||
"@nestjs/core": "^9.3.0-beta.3"
|
||||
"@nestjs/common": "9.3.3",
|
||||
"@nestjs/core": "9.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^9.0.0",
|
||||
|
||||
@@ -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,
|
||||
@@ -91,6 +93,9 @@ type VersionedRoute<TRequest, TResponse> = ((
|
||||
type FastifyRawRequest<TServer extends RawServerBase> =
|
||||
RawRequestDefaultExpression<TServer> & { originalUrl?: string };
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class FastifyAdapter<
|
||||
TServer extends RawServerBase = RawServerDefault,
|
||||
TRawRequest extends FastifyRawRequest<TServer> = FastifyRawRequest<TServer>,
|
||||
@@ -227,12 +232,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];
|
||||
}
|
||||
@@ -326,6 +346,17 @@ export class FastifyAdapter<
|
||||
}
|
||||
body = body.getStream();
|
||||
}
|
||||
if (
|
||||
fastifyReply.getHeader('Content-Type') !== undefined &&
|
||||
fastifyReply.getHeader('Content-Type') !== 'application/json' &&
|
||||
body?.statusCode >= HttpStatus.BAD_REQUEST
|
||||
) {
|
||||
Logger.warn(
|
||||
"Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses",
|
||||
FastifyAdapter.name,
|
||||
);
|
||||
fastifyReply.header('Content-Type', 'application/json');
|
||||
}
|
||||
return fastifyReply.send(body);
|
||||
}
|
||||
|
||||
@@ -465,6 +496,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 +578,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 +599,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()));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Nest @platform-fastify
|
||||
* Copyright(c) 2017 - 2022 Kamil Mysliwiec
|
||||
* Copyright(c) 2017 - 2023 Kamil Mysliwiec
|
||||
* https://nestjs.com
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* "fastify-static" interfaces
|
||||
* @see https://github.com/fastify/fastify-static/blob/master/types/index.d.ts
|
||||
* @publicApi
|
||||
*/
|
||||
import { Stats } from 'fs';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* "fastify/view" interfaces
|
||||
* @see https://github.com/fastify/point-of-view/blob/master/types/index.d.ts
|
||||
* @publicApi
|
||||
*/
|
||||
export interface FastifyViewOptions {
|
||||
engine: {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './nest-fastify-application.interface';
|
||||
export * from './nest-fastify-body-parser-options.interface';
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import {
|
||||
FastifyBodyParser,
|
||||
FastifyInstance,
|
||||
FastifyPluginAsync,
|
||||
FastifyPluginCallback,
|
||||
FastifyPluginOptions,
|
||||
FastifyRegisterOptions,
|
||||
RawServerBase,
|
||||
} from 'fastify';
|
||||
import {
|
||||
Chain as LightMyRequestChain,
|
||||
@@ -12,7 +14,11 @@ import {
|
||||
Response as LightMyRequestResponse,
|
||||
} from 'light-my-request';
|
||||
import { FastifyStaticOptions, FastifyViewOptions } from './external';
|
||||
import { NestFastifyBodyParserOptions } from './nest-fastify-body-parser-options.interface';
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export interface NestFastifyApplication extends INestApplication {
|
||||
/**
|
||||
* A wrapper function around native `fastify.register()` method.
|
||||
@@ -28,6 +34,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' })`
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { AddContentTypeParser } from 'fastify';
|
||||
|
||||
export type NestFastifyBodyParserOptions = Omit<
|
||||
Parameters<AddContentTypeParser>[1],
|
||||
'parseAs'
|
||||
>;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/platform-fastify",
|
||||
"version": "9.3.0-beta.3",
|
||||
"version": "9.3.3",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@platform-fastify)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -23,7 +23,7 @@
|
||||
"fastify": "4.12.0",
|
||||
"light-my-request": "5.8.0",
|
||||
"path-to-regexp": "3.2.0",
|
||||
"tslib": "2.4.1"
|
||||
"tslib": "2.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fastify/static": "^6.0.0",
|
||||
|
||||
@@ -8,6 +8,9 @@ import { fromEvent, Observable } from 'rxjs';
|
||||
import { filter, first, map, mergeMap, share, takeUntil } from 'rxjs/operators';
|
||||
import { Server, ServerOptions, Socket } from 'socket.io';
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class IoAdapter extends AbstractWsAdapter {
|
||||
public create(
|
||||
port: number,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Nest @platform-socket.io
|
||||
* Copyright(c) 2017 - 2022 Kamil Mysliwiec
|
||||
* Copyright(c) 2017 - 2023 Kamil Mysliwiec
|
||||
* https://nestjs.com
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/platform-socket.io",
|
||||
"version": "9.3.0-beta.3",
|
||||
"version": "9.3.3",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@platform-socket.io)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"socket.io": "4.5.4",
|
||||
"tslib": "2.4.1"
|
||||
"tslib": "2.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^9.0.0",
|
||||
|
||||
@@ -27,6 +27,9 @@ type WsServerRegistryEntry = any[];
|
||||
|
||||
const UNDERLYING_HTTP_SERVER_PORT = 0;
|
||||
|
||||
/**
|
||||
* @publicApi
|
||||
*/
|
||||
export class WsAdapter extends AbstractWsAdapter {
|
||||
protected readonly logger = new Logger(WsAdapter.name);
|
||||
protected readonly httpServersRegistry = new Map<
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Nest @platform-ws
|
||||
* Copyright(c) 2017 - 2022 Kamil Mysliwiec
|
||||
* Copyright(c) 2017 - 2023 Kamil Mysliwiec
|
||||
* https://nestjs.com
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user