Compare commits

..

2 Commits

Author SHA1 Message Date
Kamil Myśliwiec
f273041594 test: update unit tests 2024-11-07 12:07:59 +01:00
Kamil Myśliwiec
da8ebded25 fix(microservices): use instance ref to call handler #13473 2024-11-07 12:01:18 +01:00
198 changed files with 614918 additions and 16967 deletions

View File

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

View File

@@ -6,8 +6,8 @@ cd "$(dirname "$0")"
cd /tmp/
sudo apt-get install build-essential libssl-dev git -y
git clone --depth=1 https://github.com/wg/wrk.git wrk
git clone https://github.com/wg/wrk.git wrk
cd wrk
sudo make
# move the executable to somewhere in your PATH, ex:
sudo cp wrk /usr/local/bin
sudo cp wrk /usr/local/bin

View File

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

4
.gitignore vendored
View File

@@ -1,5 +1,4 @@
packages/*/package-lock.json
sample/*/package-lock.json
# dependencies
node_modules/
@@ -11,7 +10,7 @@ node_modules/
/.devcontainer
/.classpath
/.project
/.settings
/.settings
*.code-workspace
# Vim
@@ -51,4 +50,3 @@ build/config\.gypi
.npmrc
pnpm-lock.yaml
/.history

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,11 @@
import { ContextId, ContextIdStrategy, HostComponentInfo } from '@nestjs/core';
import { Request } from 'express';
export type TenantContext = {
tenantId: string;
forceError?: boolean;
};
const tenants = new Map<string, ContextId>();
export class DurableContextIdStrategy implements ContextIdStrategy {
attach(contextId: ContextId, request: Request) {
const tenantId = request.headers['x-tenant-id'] as string;
const forceError = request.headers['x-force-error'] === 'true';
let tenantSubTreeId: ContextId;
if (tenants.has(tenantId)) {
@@ -21,15 +14,10 @@ export class DurableContextIdStrategy implements ContextIdStrategy {
tenantSubTreeId = { id: +tenantId } as ContextId;
tenants.set(tenantId, tenantSubTreeId);
}
const payload: TenantContext = { tenantId };
if (forceError) {
payload.forceError = true;
}
return {
resolve: (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId,
payload,
payload: { tenantId },
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

29290
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -524,32 +524,6 @@ export function Body(
*/
export function RawBody(): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the `rawBody` Buffer
* property from the `req` object and populates the decorated parameter with that value.
* Also applies pipes to the bound rawBody parameter.
*
* For example:
* ```typescript
* async create(@RawBody(new ValidationPipe()) rawBody: Buffer)
* ```
*
* @param pipes one or more pipes - either instances or classes - to apply to
* the bound body parameter.
*
* @see [Request object](https://docs.nestjs.com/controllers#request-object)
* @see [Raw body](https://docs.nestjs.com/faq/raw-body)
* @see [Working with pipes](https://docs.nestjs.com/custom-decorators#working-with-pipes)
*
* @publicApi
*/
export function RawBody(
...pipes: (
| Type<PipeTransform<Buffer | undefined>>
| PipeTransform<Buffer | undefined>
)[]
): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the `rawBody` Buffer
* property from the `req` object and populates the decorated parameter with that value.

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/common",
"version": "10.4.16",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
"author": "Kamil Mysliwiec",
"homepage": "https://nestjs.com",
@@ -19,13 +19,12 @@
"license": "MIT",
"dependencies": {
"iterare": "1.2.1",
"tslib": "2.8.1",
"tslib": "2.7.0",
"uid": "2.0.2"
},
"peerDependencies": {
"class-transformer": "*",
"class-validator": "*",
"file-type": "^20.4.1",
"reflect-metadata": "^0.1.12 || ^0.2.0",
"rxjs": "^7.1.0"
},
@@ -35,9 +34,6 @@
},
"class-transformer": {
"optional": true
},
"file-type": {
"optional": true
}
}
}

View File

@@ -3,19 +3,14 @@ import { IFile } from './interfaces';
export type FileTypeValidatorOptions = {
fileType: string | RegExp;
/**
* If `true`, the validator will skip the magic numbers validation.
* This can be useful when you can't identify some files as there are no common magic numbers available for some file types.
* @default false
*/
skipMagicNumbersValidation?: boolean;
};
/**
* Defines the built-in FileTypeValidator. It validates incoming files by examining
* their magic numbers using the file-type package, providing more reliable file type validation
* than just checking the mimetype string.
* Defines the built-in FileType File Validator. It validates incoming files mime-type
* matching a string or a regular expression. Note that this validator uses a naive strategy
* to check the mime-type and could be fooled if the client provided a file with renamed extension.
* (for instance, renaming a 'malicious.bat' to 'malicious.jpeg'). To handle such security issues
* with more reliability, consider checking against the file's [magic-numbers](https://en.wikipedia.org/wiki/Magic_number_%28programming%29)
*
* @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators)
*
@@ -25,42 +20,19 @@ export class FileTypeValidator extends FileValidator<
FileTypeValidatorOptions,
IFile
> {
buildErrorMessage(file?: IFile): string {
if (file?.mimetype) {
return `Validation failed (current file type is ${file.mimetype}, expected type is ${this.validationOptions.fileType})`;
}
buildErrorMessage(): string {
return `Validation failed (expected type is ${this.validationOptions.fileType})`;
}
async isValid(file?: IFile): Promise<boolean> {
isValid(file?: IFile): boolean {
if (!this.validationOptions) {
return true;
}
const isFileValid = !!file && 'mimetype' in file;
if (this.validationOptions.skipMagicNumbersValidation) {
return (
isFileValid && !!file.mimetype.match(this.validationOptions.fileType)
);
}
if (!isFileValid || !file.buffer) {
return false;
}
try {
const { fileTypeFromBuffer } = (await eval(
'import ("file-type")',
)) as typeof import('file-type');
const fileType = await fileTypeFromBuffer(file.buffer);
return (
!!fileType && !!fileType.mime.match(this.validationOptions.fileType)
);
} catch {
return false;
}
return (
!!file &&
'mimetype' in file &&
!!file.mimetype.match(this.validationOptions.fileType)
);
}
}

View File

@@ -1,5 +1,4 @@
export interface IFile {
mimetype: string;
size: number;
buffer?: Buffer;
}

View File

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

View File

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

View File

@@ -1,195 +1,89 @@
import { expect } from 'chai';
import { IFile } from '../../../../common/pipes/file/interfaces';
import { FileTypeValidator } from '../../../pipes';
describe('FileTypeValidator', () => {
describe('isValid', () => {
it('should return true when the file buffer matches the specified type', async () => {
it('should return true when the file mimetype is the same as the specified', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
});
const jpegBuffer = Buffer.from([
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
]);
const requestFile = {
mimetype: 'image/jpeg',
buffer: jpegBuffer,
} as IFile;
} as any;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(true);
expect(fileTypeValidator.isValid(requestFile)).to.equal(true);
});
it('should return true when the file buffer matches the specified file extension', async () => {
it('should return true when the file mimetype ends with the specified option type', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'jpeg',
});
const jpegBuffer = Buffer.from([
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
]);
const requestFile = {
mimetype: 'image/jpeg',
buffer: jpegBuffer,
} as IFile;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(true);
} as any;
expect(fileTypeValidator.isValid(requestFile)).to.equal(true);
});
it('should return true when the file buffer matches the specified regexp', async () => {
it('should return true when the file mimetype matches the specified regexp', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: /^image\//,
fileType: /word/,
});
const jpegBuffer = Buffer.from([
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
]);
const requestFile = {
mimetype: 'image/jpeg',
buffer: jpegBuffer,
} as IFile;
mimetype: 'application/msword',
} as any;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(true);
expect(fileTypeValidator.isValid(requestFile)).to.equal(true);
});
it('should return false when the file buffer does not match the specified type', async () => {
it('should return false when the file mimetype is different from the specified', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
});
const pngBuffer = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
]);
const requestFile = {
mimetype: 'image/jpeg', // Spoofed mimetype
buffer: pngBuffer,
} as IFile;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
});
it('should return false when the file buffer does not match the specified file extension', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'jpeg',
});
const pngBuffer = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
]);
const requestFile = {
mimetype: 'image/png',
buffer: pngBuffer,
} as IFile;
} as any;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
expect(fileTypeValidator.isValid(requestFile)).to.equal(false);
});
it('should return false when no buffer is provided', async () => {
it('should return false when the file mimetype does not match the provided regexp', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: /mp4/,
});
const requestFile = {
mimetype: 'image/png',
} as any;
expect(fileTypeValidator.isValid(requestFile)).to.equal(false);
});
it('should return false when the file mimetype was not provided', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
});
const requestFile = {
mimetype: 'image/jpeg',
} as IFile;
const requestFile = {} as any;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
expect(fileTypeValidator.isValid(requestFile)).to.equal(false);
});
it('should return false when no file is provided', async () => {
it('should return false when no file provided', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
});
expect(await fileTypeValidator.isValid()).to.equal(false);
});
it('should return false when no buffer is provided', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
});
const requestFile = {
mimetype: 'image/jpeg',
} as IFile;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
});
it('should return true when the file buffer matches the specified regexp', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: /^image\//,
});
const jpegBuffer = Buffer.from([
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
]);
const requestFile = {
mimetype: 'image/jpeg',
buffer: jpegBuffer,
} as IFile;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(true);
});
it('should return true when no validation options are provided', async () => {
const fileTypeValidator = new FileTypeValidator({} as any);
const jpegBuffer = Buffer.from([
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
]);
const requestFile = {
mimetype: 'image/jpeg',
buffer: jpegBuffer,
} as IFile;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(true);
});
it('should skip magic numbers validation when the skipMagicNumbersValidation is true', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
skipMagicNumbersValidation: true,
});
const requestFile = {
mimetype: 'image/jpeg',
} as IFile;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(true);
});
it('should return false when the file buffer does not match any known type', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'unknown/type',
});
const unknownBuffer = Buffer.from([
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
]);
const requestFile = {
mimetype: 'unknown/type',
buffer: unknownBuffer,
} as IFile;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
});
it('should return false when the buffer is empty', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
});
const emptyBuffer = Buffer.from([]);
const requestFile = {
mimetype: 'image/jpeg',
buffer: emptyBuffer,
} as IFile;
expect(await fileTypeValidator.isValid(requestFile)).to.equal(false);
expect(fileTypeValidator.isValid()).to.equal(false);
});
});
describe('buildErrorMessage', () => {
it('should return a string with the format "Validation failed (expected type is #fileType)"', async () => {
it('should return a string with the format "Validation failed (expected type is #fileType)"', () => {
const fileType = 'image/jpeg';
const fileTypeValidator = new FileTypeValidator({
fileType,
@@ -199,85 +93,5 @@ describe('FileTypeValidator', () => {
`Validation failed (expected type is ${fileType})`,
);
});
it('should include the file type in the error message when a file is provided', async () => {
const currentFileType = 'image/png';
const fileType = 'image/jpeg';
const fileTypeValidator = new FileTypeValidator({
fileType,
});
const file = { mimetype: currentFileType } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).to.equal(
`Validation failed (current file type is ${currentFileType}, expected type is ${fileType})`,
);
});
it('should handle regexp file type in error message', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: /^image\//,
});
const file = { mimetype: 'application/pdf' } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).to.equal(
`Validation failed (current file type is application/pdf, expected type is /^image\\//)`,
);
});
it('should handle file extension in error message', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'jpeg',
});
const file = { mimetype: 'image/png' } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).to.equal(
'Validation failed (current file type is image/png, expected type is jpeg)',
);
});
it('should handle regexp file type in error message', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: /^image\//,
});
const file = { mimetype: 'application/pdf' } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).to.equal(
`Validation failed (current file type is application/pdf, expected type is /^image\\//)`,
);
});
it('should handle file extension in error message', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'jpeg',
});
const file = { mimetype: 'image/png' } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).to.equal(
'Validation failed (current file type is image/png, expected type is jpeg)',
);
});
it('should handle regexp file type in error message', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: /^image\//,
});
const file = { mimetype: 'application/pdf' } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).to.equal(
`Validation failed (current file type is application/pdf, expected type is /^image\\//)`,
);
});
it('should handle file extension in error message', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'jpeg',
});
const file = { mimetype: 'image/png' } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).to.equal(
'Validation failed (current file type is image/png, expected type is jpeg)',
);
});
});
});

View File

@@ -1,9 +1,9 @@
import { expect } from 'chai';
import {
FileTypeValidator,
FileValidator,
MaxFileSizeValidator,
ParseFilePipeBuilder,
FileTypeValidator,
} from '../../../pipes';
describe('ParseFilePipeBuilder', () => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ import {
InjectionToken,
NestModule,
Provider,
Scope,
Type,
ValueProvider,
} from '@nestjs/common/interfaces';
@@ -254,11 +253,6 @@ export class Module {
return this.addCustomProvider(provider, this._providers, enhancerSubtype);
}
const isAlreadyDeclared = this._providers.has(provider);
if (this.isTransientProvider(provider) && isAlreadyDeclared) {
return provider;
}
this._providers.set(
provider,
new InstanceWrapper({
@@ -662,8 +656,4 @@ export class Module {
const key = this.name?.toString() ?? this.token?.toString();
return key ? UuidFactory.get(`${prefix}_${key}`) : randomStringGenerator();
}
private isTransientProvider(provider: Type<any>): boolean {
return getClassScope(provider) === Scope.TRANSIENT;
}
}

View File

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

View File

@@ -54,7 +54,6 @@ export class NestApplicationContext<
private shutdownCleanupRef?: (...args: unknown[]) => unknown;
private _instanceLinksHost: InstanceLinksHost;
private _moduleRefsForHooksByDistance?: Array<Module>;
private initializationPromise?: Promise<void>;
protected get instanceLinksHost() {
if (!this._instanceLinksHost) {
@@ -235,16 +234,8 @@ export class NestApplicationContext<
if (this.isInitialized) {
return this;
}
this.initializationPromise = new Promise(async (resolve, reject) => {
try {
await this.callInitHook();
await this.callBootstrapHook();
resolve();
} catch (err) {
reject(err);
}
});
await this.initializationPromise;
await this.callInitHook();
await this.callBootstrapHook();
this.isInitialized = true;
return this;
@@ -255,7 +246,6 @@ export class NestApplicationContext<
* @returns {Promise<void>}
*/
public async close(signal?: string): Promise<void> {
await this.initializationPromise;
await this.callDestroyHook();
await this.callBeforeShutdownHook(signal);
await this.dispose();
@@ -343,7 +333,6 @@ export class NestApplicationContext<
return;
}
receivedSignal = true;
await this.initializationPromise;
await this.callDestroyHook();
await this.callBeforeShutdownHook(signal);
await this.dispose();

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/core",
"version": "10.4.16",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -32,11 +32,11 @@
"fast-safe-stringify": "2.1.1",
"iterare": "1.2.1",
"path-to-regexp": "3.3.0",
"tslib": "2.8.1",
"tslib": "2.7.0",
"uid": "2.0.2"
},
"devDependencies": {
"@nestjs/common": "10.4.16"
"@nestjs/common": "10.4.7"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { Logger } from '@nestjs/common/services/logger.service';
import { loadPackage } from '@nestjs/common/utils/load-package.util';
import { isNil, isUndefined } from '@nestjs/common/utils/shared.utils';
import { isUndefined } from '@nestjs/common/utils/shared.utils';
import {
KAFKA_DEFAULT_BROKER,
KAFKA_DEFAULT_CLIENT,
@@ -37,15 +37,6 @@ import {
KafkaRequestSerializer,
} from '../serializers/kafka-request.serializer';
import { ClientProxy } from './client-proxy';
import {
connectable,
defer,
Observable,
Subject,
throwError as _throw,
} from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { InvalidMessageException } from '../errors/invalid-message.exception';
let kafkaPackage: any = {};
@@ -232,58 +223,6 @@ export class ClientKafka extends ClientProxy {
return this.consumerAssignments;
}
public emitBatch<TResult = any, TInput = any>(
pattern: any,
data: { messages: TInput[] },
): Observable<TResult> {
if (isNil(pattern) || isNil(data)) {
return _throw(() => new InvalidMessageException());
}
const source = defer(async () => this.connect()).pipe(
mergeMap(() => this.dispatchBatchEvent({ pattern, data })),
);
const connectableSource = connectable(source, {
connector: () => new Subject(),
resetOnDisconnect: false,
});
connectableSource.connect();
return connectableSource;
}
public commitOffsets(
topicPartitions: TopicPartitionOffsetAndMetadata[],
): Promise<void> {
if (this.consumer) {
return this.consumer.commitOffsets(topicPartitions);
} else {
throw new Error('No consumer initialized');
}
}
protected async dispatchBatchEvent<TInput = any>(
packets: ReadPacket<{ messages: TInput[] }>,
): Promise<any> {
if (packets.data.messages.length === 0) {
return;
}
const pattern = this.normalizePattern(packets.pattern);
const outgoingEvents = await Promise.all(
packets.data.messages.map(message => {
return this.serializer.serialize(message as any, { pattern });
}),
);
const message = Object.assign(
{
topic: pattern,
messages: outgoingEvents,
},
this.options.send || {},
);
return this.producer.send(message);
}
protected async dispatchEvent(packet: OutgoingEvent): Promise<any> {
const pattern = this.normalizePattern(packet.pattern);
const outgoingEvent = await this.serializer.serialize(packet.data, {
@@ -381,4 +320,14 @@ export class ClientKafka extends ClientProxy {
this.deserializer =
(options && options.deserializer) || new KafkaResponseDeserializer();
}
public commitOffsets(
topicPartitions: TopicPartitionOffsetAndMetadata[],
): Promise<void> {
if (this.consumer) {
return this.consumer.commitOffsets(topicPartitions);
} else {
throw new Error('No consumer initialized');
}
}
}

View File

@@ -210,22 +210,15 @@ export class ClientMqtt extends ClientProxy {
return undefined;
}
// Cant just spread objects as MQTT won't deliver
// any message with empty object as "userProperties" field
// @url https://github.com/nestjs/nest/issues/14079
let options: MqttRecordOptions = {};
if (requestOptions) {
options = { ...requestOptions };
}
if (this.options?.userProperties) {
options.properties = {
...options.properties,
return {
...requestOptions,
properties: {
...requestOptions?.properties,
userProperties: {
...this.options?.userProperties,
...options.properties?.userProperties,
...requestOptions?.properties?.userProperties,
},
};
}
return options;
},
};
}
}

View File

@@ -135,7 +135,9 @@ export class ClientRMQ extends ClientProxy {
public createClient(): AmqpConnectionManager {
const socketOptions = this.getOptionsProp(this.options, 'socketOptions');
return rmqPackage.connect(this.urls, socketOptions);
return rmqPackage.connect(this.urls, {
connectionOptions: socketOptions?.connectionOptions,
});
}
public mergeDisconnectEvent<T = any>(

View File

@@ -1,23 +1,23 @@
import {
isNil,
isNumber,
isObject,
isNumber,
isNil,
isSymbol,
} from '@nestjs/common/utils/shared.utils';
/* eslint-disable @typescript-eslint/no-use-before-define */
import {
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';
import { PatternMetadata } from '../interfaces/pattern-metadata.interface';
import { Transport } from '../enums';
import {
InvalidGrpcDecoratorException,
RpcDecoratorMetadata,
} from '../errors/invalid-grpc-message-decorator.exception';
import { PatternMetadata } from '../interfaces/pattern-metadata.interface';
export enum GrpcMethodStreamingType {
NO_STREAMING = 'no_stream',
@@ -141,35 +141,7 @@ export function GrpcStreamMethod(
method,
GrpcMethodStreamingType.RX_STREAMING,
);
MessagePattern(metadata, Transport.GRPC)(target, key, descriptor);
const originalMethod = descriptor.value;
// Override original method to call the "drainBuffer" method on the first parameter
// This is required to avoid premature message emission
descriptor.value = async function (
this: any,
observable: any,
...args: any[]
) {
const result = await Promise.resolve(
originalMethod.apply(this, [observable, ...args]),
);
// Drain buffer if "drainBuffer" method is available
if (observable && observable.drainBuffer) {
observable.drainBuffer();
}
return result;
};
// Copy all metadata from the original method to the new one
const metadataKeys = Reflect.getMetadataKeys(originalMethod);
metadataKeys.forEach(metadataKey => {
const metadataValue = Reflect.getMetadata(metadataKey, originalMethod);
Reflect.defineMetadata(metadataKey, metadataValue, descriptor.value);
});
return MessagePattern(metadata, Transport.GRPC)(target, key, descriptor);
};
}

View File

@@ -39,25 +39,36 @@ export class ListenerMetadataExplorer {
const instancePrototype = Object.getPrototypeOf(instance);
return this.metadataScanner
.getAllMethodNames(instancePrototype)
.map(method => this.exploreMethodMetadata(instancePrototype, method))
.map(method =>
this.exploreMethodMetadata(instance, instancePrototype, method),
)
.filter(metadata => metadata);
}
public exploreMethodMetadata(
instance: Controller,
instancePrototype: object,
methodKey: string,
): EventOrMessageListenerDefinition {
const targetCallback = instancePrototype[methodKey];
const prototypeCallback = instancePrototype[methodKey];
const handlerType = Reflect.getMetadata(
PATTERN_HANDLER_METADATA,
targetCallback,
prototypeCallback,
);
if (isUndefined(handlerType)) {
return;
}
const patterns = Reflect.getMetadata(PATTERN_METADATA, targetCallback);
const transport = Reflect.getMetadata(TRANSPORT_METADATA, targetCallback);
const extras = Reflect.getMetadata(PATTERN_EXTRAS_METADATA, targetCallback);
const patterns = Reflect.getMetadata(PATTERN_METADATA, prototypeCallback);
const transport = Reflect.getMetadata(
TRANSPORT_METADATA,
prototypeCallback,
);
const extras = Reflect.getMetadata(
PATTERN_EXTRAS_METADATA,
prototypeCallback,
);
const targetCallback = instance[methodKey];
return {
methodKey,
targetCallback,

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/microservices",
"version": "10.4.16",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@microservices)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -19,11 +19,11 @@
},
"dependencies": {
"iterare": "1.2.1",
"tslib": "2.8.1"
"tslib": "2.7.0"
},
"devDependencies": {
"@nestjs/common": "10.4.16",
"@nestjs/core": "10.4.16"
"@nestjs/common": "10.4.7",
"@nestjs/core": "10.4.7"
},
"peerDependencies": {
"@grpc/grpc-js": "*",

View File

@@ -6,7 +6,6 @@ import {
import {
EMPTY,
Observable,
ReplaySubject,
Subject,
Subscription,
defaultIfEmpty,
@@ -166,7 +165,7 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
if (!methodHandler) {
continue;
}
service[methodName] = this.createServiceMethod(
service[methodName] = await this.createServiceMethod(
methodHandler,
grpcService.prototype[methodName],
streamingType,
@@ -175,7 +174,7 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
return service;
}
public getMessageHandler(
getMessageHandler(
serviceName: string,
methodName: string,
streaming: GrpcMethodStreamingType,
@@ -279,7 +278,7 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
source: Observable<T>,
call: GrpcCall<T>,
): Promise<void> {
// This promise should **not** reject, as we're handling errors in the observable for the Call
// this promise should **not** reject, as we're handling errors in the observable for the Call
// the promise is only needed to signal when writing/draining has been completed
return new Promise((resolve, _doNotUse) => {
const valuesWaitingToBeDrained: T[] = [];
@@ -381,11 +380,8 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
call: GrpcCall,
callback: (err: unknown, value: unknown) => void,
) => {
// Needs to be a Proxy in order to buffer messages that come before handler is executed
// This could happen if handler has any async guards or interceptors registered that would delay
// the execution.
const { subject, next, error, complete } = this.bufferUntilDrained();
call.on('data', (m: any) => next(m));
const req = new Subject<any>();
call.on('data', (m: any) => req.next(m));
call.on('error', (e: any) => {
// Check if error means that stream ended on other end
const isCancelledError = String(e).toLowerCase().indexOf('cancelled');
@@ -395,15 +391,11 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
return;
}
// If another error then just pass it along
error(e);
req.error(e);
});
call.on('end', () => complete());
call.on('end', () => req.complete());
const handler = methodHandler(
subject.asObservable(),
call.metadata,
call,
);
const handler = methodHandler(req.asObservable(), call.metadata, call);
const res = this.transformToObservable(await handler);
if (isResponseStream) {
await this.writeObservableToGrpc(res, call);
@@ -434,15 +426,11 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
call: GrpcCall,
callback: (err: unknown, value: unknown) => void,
) => {
let handlerStream: Observable<any>;
if (isResponseStream) {
handlerStream = this.transformToObservable(await methodHandler(call));
methodHandler(call);
} else {
handlerStream = this.transformToObservable(
await methodHandler(call, callback),
);
methodHandler(call, callback);
}
await lastValueFrom(handlerStream);
};
}
@@ -481,7 +469,7 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
this.messageHandlers.set(route, callback);
}
public async createClient() {
public async createClient(): Promise<any> {
const channelOptions: ChannelOptions =
this.options && this.options.channelOptions
? this.options.channelOptions
@@ -624,71 +612,4 @@ export class ServerGrpc extends Server implements CustomTransportStrategy {
);
}
}
private bufferUntilDrained<T>() {
type DrainableSubject<T> = Subject<T> & { drainBuffer: () => void };
const subject = new Subject<T>();
let replayBuffer = new ReplaySubject<T>();
let hasDrained = false;
function drainBuffer(this: DrainableSubject<T>) {
if (hasDrained) {
return;
}
hasDrained = true;
// Replay buffered values to the new subscriber
setImmediate(() => {
const subcription = replayBuffer.subscribe(subject);
subcription.unsubscribe();
replayBuffer = null;
});
}
return {
subject: new Proxy<DrainableSubject<T>>(subject as DrainableSubject<T>, {
get(target, prop, receiver) {
if (prop === 'asObservable') {
return () => {
const stream = subject.asObservable();
// "drainBuffer" will be called before the evaluation of the handler
// but after any enhancers have been applied (e.g., `interceptors`)
Object.defineProperty(stream, drainBuffer.name, {
value: drainBuffer,
});
return stream;
};
}
if (hasDrained) {
return Reflect.get(target, prop, receiver);
}
return Reflect.get(replayBuffer, prop, receiver);
},
}),
next: (value: T) => {
if (!hasDrained) {
replayBuffer.next(value);
}
subject.next(value);
},
error: (err: any) => {
if (!hasDrained) {
replayBuffer.error(err);
}
subject.error(err);
},
complete: () => {
if (!hasDrained) {
replayBuffer.complete();
// Replay buffer is no longer needed
// Return early to allow subject to complete later, after the replay buffer
// has been drained
return;
}
subject.complete();
},
};
}
}

View File

@@ -9,8 +9,6 @@ import {
EachMessagePayload,
KafkaMessage,
} from '../../external/kafka.interface';
import { Observable } from 'rxjs';
import { Producer } from 'kafkajs';
describe('ClientKafka', () => {
const topic = 'test.topic';
@@ -551,77 +549,6 @@ describe('ClientKafka', () => {
});
});
describe('emitBatch', () => {
it(`should return an observable stream`, () => {
const stream$ = client.emitBatch(
{},
{
messages: [],
},
);
expect(stream$ instanceof Observable).to.be.true;
});
it(`should call "connect" immediately`, () => {
const connectSpy = sinon.spy(client, 'connect');
client.emitBatch(
{},
{
messages: [],
},
);
expect(connectSpy.calledOnce).to.be.true;
});
describe('when "connect" throws', () => {
it('should return Observable with error', () => {
sinon.stub(client, 'connect').callsFake(() => {
throw new Error();
});
const stream$ = client.emitBatch(
{},
{
messages: [],
},
);
stream$.subscribe({
next: () => {},
error: err => {
expect(err).to.be.instanceof(Error);
},
});
});
});
describe('when is connected', () => {
beforeEach(() => {
sinon
.stub(client, 'connect')
.callsFake(() => Promise.resolve({} as Producer));
});
it(`should call dispatchBatchEvent`, () => {
const pattern = { test: 3 };
const data = { messages: [] };
const dispatchBatchEventSpy = sinon
.stub()
.callsFake(() => Promise.resolve(true));
const stream$ = client.emitBatch(pattern, data);
client['dispatchBatchEvent'] = dispatchBatchEventSpy;
stream$.subscribe(() => {
expect(dispatchBatchEventSpy.calledOnce).to.be.true;
});
});
});
it('should return Observable with error', () => {
const err$ = client.emitBatch(null, null);
expect(err$).to.be.instanceOf(Observable);
});
});
describe('dispatchEvent', () => {
const eventMessage = {
id: undefined,

View File

@@ -71,6 +71,7 @@ describe('ListenerMetadataExplorer', () => {
});
it(`should return undefined when "handlerType" metadata is undefined`, () => {
const metadata = instance.exploreMethodMetadata(
test,
Object.getPrototypeOf(test),
'noPattern',
);
@@ -80,6 +81,7 @@ describe('ListenerMetadataExplorer', () => {
describe('@MessagePattern', () => {
it(`should return pattern properties when "handlerType" metadata is not undefined`, () => {
const metadata = instance.exploreMethodMetadata(
test,
Object.getPrototypeOf(test),
'testMessage',
);
@@ -96,6 +98,7 @@ describe('ListenerMetadataExplorer', () => {
});
it(`should return multiple patterns when more than one is declared`, () => {
const metadata = instance.exploreMethodMetadata(
test,
Object.getPrototypeOf(test),
'testMultipleMessage',
);
@@ -116,6 +119,7 @@ describe('ListenerMetadataExplorer', () => {
describe('@EventPattern', () => {
it(`should return pattern properties when "handlerType" metadata is not undefined`, () => {
const metadata = instance.exploreMethodMetadata(
test,
Object.getPrototypeOf(test),
'testEvent',
);
@@ -132,6 +136,7 @@ describe('ListenerMetadataExplorer', () => {
});
it(`should return multiple patterns when more than one is declared`, () => {
const metadata = instance.exploreMethodMetadata(
test,
Object.getPrototypeOf(test),
'testMultipleEvent',
);

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/platform-express",
"version": "10.4.16",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@platform-express)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -20,13 +20,13 @@
"dependencies": {
"body-parser": "1.20.3",
"cors": "2.8.5",
"express": "4.21.2",
"express": "4.21.1",
"multer": "1.4.4-lts.1",
"tslib": "2.8.1"
"tslib": "2.7.0"
},
"devDependencies": {
"@nestjs/common": "10.4.16",
"@nestjs/core": "10.4.16"
"@nestjs/common": "10.4.7",
"@nestjs/core": "10.4.7"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/platform-fastify",
"version": "10.4.16",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@platform-fastify)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -22,9 +22,9 @@
"@fastify/formbody": "7.4.0",
"@fastify/middie": "8.3.3",
"fastify": "4.28.1",
"light-my-request": "6.3.0",
"light-my-request": "6.1.0",
"path-to-regexp": "3.3.0",
"tslib": "2.8.1"
"tslib": "2.7.0"
},
"peerDependencies": {
"@fastify/static": "^6.0.0 || ^7.0.0",

View File

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

View File

@@ -82,12 +82,4 @@ export class IoAdapter extends AbstractWsAdapter {
}
return { data: payload };
}
public close(server: Server) {
if (this.forceCloseConnections && server.httpServer === this.httpServer) {
return;
}
return super.close(server);
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/platform-socket.io",
"version": "10.4.16",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@platform-socket.io)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -18,8 +18,8 @@
"access": "public"
},
"dependencies": {
"socket.io": "4.8.1",
"tslib": "2.8.1"
"socket.io": "4.8.0",
"tslib": "2.7.0"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/platform-ws",
"version": "10.4.16",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@platform-ws)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -18,7 +18,7 @@
"access": "public"
},
"dependencies": {
"tslib": "2.8.1",
"tslib": "2.7.0",
"ws": "8.18.0"
},
"peerDependencies": {

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/testing",
"version": "10.4.16",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@testing)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -18,7 +18,7 @@
"access": "public"
},
"dependencies": {
"tslib": "2.8.1"
"tslib": "2.7.0"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0",

View File

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

View File

@@ -17,15 +17,6 @@ export abstract class AbstractWsAdapter<
> implements WebSocketAdapter<TServer, TClient, TOptions>
{
protected readonly httpServer: any;
private _forceCloseConnections: boolean;
public set forceCloseConnections(value: boolean) {
this._forceCloseConnections = value;
}
public get forceCloseConnections(): boolean {
return this._forceCloseConnections;
}
constructor(appOrHttpServer?: INestApplicationContext | any) {
if (appOrHttpServer && appOrHttpServer instanceof NestApplication) {

View File

@@ -1,6 +1,6 @@
{
"name": "@nestjs/websockets",
"version": "10.4.16",
"version": "10.4.7",
"description": "Nest - modern, fast, powerful node.js web framework (@websockets)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -15,11 +15,11 @@
"dependencies": {
"iterare": "1.2.1",
"object-hash": "3.0.0",
"tslib": "2.8.1"
"tslib": "2.7.0"
},
"devDependencies": {
"@nestjs/common": "10.4.16",
"@nestjs/core": "10.4.16"
"@nestjs/common": "10.4.7",
"@nestjs/core": "10.4.7"
},
"peerDependencies": {
"@nestjs/common": "^10.0.0",

View File

@@ -1,4 +1,3 @@
import { NestApplicationOptions } from '@nestjs/common';
import { InjectionToken } from '@nestjs/common/interfaces';
import { Injectable } from '@nestjs/common/interfaces/injectable.interface';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
@@ -114,12 +113,8 @@ export class SocketModule<
}
private initializeAdapter() {
const forceCloseConnections = (this.appOptions as NestApplicationOptions)
.forceCloseConnections;
const adapter = this.applicationConfig.getIoAdapter();
if (adapter) {
(adapter as AbstractWsAdapter).forceCloseConnections =
forceCloseConnections;
this.isAdapterInitialized = true;
return;
}
@@ -129,7 +124,6 @@ export class SocketModule<
() => require('@nestjs/platform-socket.io'),
);
const ioAdapter = new IoAdapter(this.httpServer);
ioAdapter.forceCloseConnections = forceCloseConnections;
this.applicationConfig.setIoAdapter(ioAdapter);
this.isAdapterInitialized = true;

16628
sample/01-cats-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,35 +19,35 @@
"test:e2e": "jest --config ./e2e/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "10.4.13",
"@nestjs/core": "10.4.13",
"@nestjs/platform-express": "10.4.13",
"@nestjs/common": "10.3.2",
"@nestjs/core": "10.3.2",
"@nestjs/platform-express": "10.3.8",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"reflect-metadata": "0.2.2",
"rimraf": "5.0.10",
"reflect-metadata": "0.2.1",
"rimraf": "5.0.5",
"rxjs": "7.8.1"
},
"devDependencies": {
"@nestjs/cli": "10.4.8",
"@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.13",
"@nestjs/cli": "10.3.2",
"@nestjs/schematics": "10.1.1",
"@nestjs/testing": "10.3.2",
"@types/express": "4.17.21",
"@types/jest": "29.5.14",
"@types/node": "22.10.0",
"@types/supertest": "6.0.2",
"@types/jest": "29.5.12",
"@types/node": "20.8.7",
"@types/supertest": "2.0.16",
"jest": "29.7.0",
"prettier": "3.4.2",
"supertest": "6.3.4",
"prettier": "3.2.5",
"supertest": "6.3.3",
"ts-jest": "29.1.2",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"eslint": "8.57.1",
"eslint": "8.42.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.29.1",
"typescript": "5.3.3"
},
"jest": {

View File

@@ -19,36 +19,16 @@ describe('CatsController', () => {
describe('findAll', () => {
it('should return an array of cats', async () => {
const cats: Cat[] = [
const result: Cat[] = [
{
age: 2,
breed: 'Bombay',
name: 'Pixel',
},
];
// @ts-ignore
catsService.cats = cats;
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(cats);
});
});
describe('create', () => {
it('should add a new cat', async () => {
const cat: Cat = {
age: 2,
breed: 'Bombay',
name: 'Pixel',
};
const expectedCatArray = [cat];
// @ts-ignore
expect(catsService.cats).toStrictEqual([]);
await catsController.create(cat);
// @ts-ignore
expect(catsService.cats).toStrictEqual(expectedCatArray);
expect(await catsController.findAll()).toBe(result);
});
});
});

View File

@@ -1,48 +0,0 @@
import { Test } from '@nestjs/testing';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
describe('CatsService', () => {
let catsService: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [CatsService],
}).compile();
catsService = moduleRef.get<CatsService>(CatsService);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = [
{
name: 'Frajola',
age: 2,
breed: 'Stray',
},
];
//@ts-ignore
catsService.cats = result;
await expect(catsService.findAll()).resolves.toBe(result);
});
});
describe('create', () => {
it('should add a new cat', async () => {
const cat: Cat = {
name: 'Frajola',
age: 2,
breed: 'Stray',
};
const expectedCatArray = [cat];
//@ts-ignore
expect(catsService.cats).toStrictEqual([]);
await catsService.create(cat);
//@ts-ignore
expect(catsService.cats).toStrictEqual(expectedCatArray);
});
});
});

View File

@@ -9,7 +9,7 @@ export class CatsService {
this.cats.push(cat);
}
findAll(): Promise<Cat[]> {
return Promise.resolve(this.cats);
findAll(): Cat[] {
return this.cats;
}
}

16764
sample/02-gateways/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,37 +19,37 @@
"test:e2e": "jest --config ./e2e/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "10.4.13",
"@nestjs/core": "10.4.13",
"@nestjs/platform-express": "10.4.13",
"@nestjs/platform-socket.io": "10.4.13",
"@nestjs/websockets": "10.4.13",
"@nestjs/common": "10.3.2",
"@nestjs/core": "10.3.2",
"@nestjs/platform-express": "10.3.8",
"@nestjs/platform-socket.io": "10.3.2",
"@nestjs/websockets": "10.3.2",
"@socket.io/redis-adapter": "8.2.1",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"reflect-metadata": "0.2.2",
"rimraf": "5.0.10",
"reflect-metadata": "0.2.1",
"rimraf": "5.0.5",
"rxjs": "7.8.1",
"socket.io": "4.8.1"
"socket.io": "4.7.4"
},
"devDependencies": {
"@nestjs/cli": "10.4.8",
"@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.13",
"@nestjs/cli": "10.4.5",
"@nestjs/schematics": "10.1.1",
"@nestjs/testing": "10.3.2",
"@types/express": "4.17.21",
"@types/jest": "29.5.14",
"@types/node": "22.10.0",
"@types/supertest": "6.0.2",
"@types/jest": "29.5.12",
"@types/node": "20.8.7",
"@types/supertest": "2.0.16",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"eslint": "8.57.1",
"eslint": "8.42.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.29.1",
"jest": "29.7.0",
"prettier": "3.4.2",
"prettier": "3.2.5",
"redis": "4.6.13",
"supertest": "6.3.4",
"supertest": "6.3.3",
"ts-jest": "29.1.2",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",

16373
sample/03-microservices/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,36 +19,36 @@
"test:e2e": "echo 'No e2e tests implemented yet.'"
},
"dependencies": {
"@nestjs/common": "10.4.13",
"@nestjs/core": "10.4.13",
"@nestjs/microservices": "10.4.13",
"@nestjs/platform-express": "10.4.13",
"@nestjs/common": "10.3.2",
"@nestjs/core": "10.3.2",
"@nestjs/microservices": "10.3.2",
"@nestjs/platform-express": "10.3.8",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"reflect-metadata": "0.2.2",
"rimraf": "5.0.10",
"reflect-metadata": "0.2.1",
"rimraf": "5.0.5",
"rxjs": "7.8.1"
},
"devDependencies": {
"@nestjs/cli": "10.4.8",
"@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.13",
"@types/amqplib": "0.10.6",
"@nestjs/cli": "10.3.2",
"@nestjs/schematics": "10.1.1",
"@nestjs/testing": "10.3.2",
"@types/amqplib": "0.10.4",
"@types/express": "4.17.21",
"@types/node": "22.10.0",
"@types/supertest": "6.0.2",
"@types/node": "20.8.7",
"@types/supertest": "2.0.16",
"jest": "29.7.0",
"prettier": "3.4.2",
"supertest": "6.3.4",
"prettier": "3.2.5",
"supertest": "6.3.3",
"ts-jest": "29.1.2",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"eslint": "8.57.1",
"eslint": "8.42.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.29.1",
"typescript": "5.3.3"
}
}

16563
sample/04-grpc/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,32 +20,32 @@
},
"dependencies": {
"@grpc/grpc-js": "1.10.9",
"@grpc/reflection": "1.0.4",
"@nestjs/common": "10.4.13",
"@nestjs/core": "10.4.13",
"@nestjs/microservices": "10.4.13",
"@nestjs/platform-express": "10.4.13",
"@grpc/reflection": "1.0.3",
"@nestjs/common": "10.3.2",
"@nestjs/core": "10.3.2",
"@nestjs/microservices": "10.3.2",
"@nestjs/platform-express": "10.3.8",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"reflect-metadata": "0.2.2",
"rimraf": "5.0.10",
"reflect-metadata": "0.2.1",
"rimraf": "5.0.5",
"rxjs": "7.8.1"
},
"devDependencies": {
"@nestjs/cli": "10.4.8",
"@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.13",
"@nestjs/cli": "10.3.2",
"@nestjs/schematics": "10.1.1",
"@nestjs/testing": "10.3.2",
"@types/express": "4.17.21",
"@types/node": "22.10.0",
"@types/supertest": "6.0.2",
"@types/node": "20.8.7",
"@types/supertest": "2.0.16",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"eslint": "8.57.1",
"eslint": "8.42.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.29.1",
"jest": "29.7.0",
"prettier": "3.4.2",
"supertest": "6.3.4",
"prettier": "3.2.5",
"supertest": "6.3.3",
"ts-jest": "29.1.2",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",

View File

@@ -1,8 +1,8 @@
import { ReflectionService } from '@grpc/reflection';
import { GrpcOptions, Transport } from '@nestjs/microservices';
import { ClientOptions, Transport } from '@nestjs/microservices';
import { join } from 'path';
export const grpcClientOptions: GrpcOptions = {
export const grpcClientOptions: ClientOptions = {
transport: Transport.GRPC,
options: {
package: 'hero', // ['hero', 'hero2']

View File

@@ -2,7 +2,7 @@ version: "3"
services:
mysql:
image: mysql:9
image: mysql:8
restart: always
environment:
MYSQL_ROOT_PASSWORD: root
@@ -10,7 +10,7 @@ services:
ports:
- "3306:3306"
db-test:
image: mysql:9
image: mysql:8
restart: always
environment:
MYSQL_ROOT_PASSWORD: root

17140
sample/05-sql-typeorm/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,32 +19,32 @@
"test:e2e": "echo 'No e2e tests implemented yet.'"
},
"dependencies": {
"@nestjs/common": "10.4.13",
"@nestjs/core": "10.4.13",
"@nestjs/platform-express": "10.4.13",
"@nestjs/typeorm": "10.0.2",
"@nestjs/common": "10.3.2",
"@nestjs/core": "10.3.2",
"@nestjs/platform-express": "10.3.8",
"@nestjs/typeorm": "10.0.1",
"mysql2": "3.9.8",
"reflect-metadata": "0.2.2",
"rimraf": "5.0.10",
"rimraf": "5.0.5",
"rxjs": "7.8.1",
"typeorm": "0.3.20"
},
"devDependencies": {
"@nestjs/cli": "10.4.8",
"@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.13",
"@nestjs/cli": "10.3.2",
"@nestjs/schematics": "10.1.1",
"@nestjs/testing": "10.3.2",
"@types/express": "4.17.21",
"@types/jest": "29.5.14",
"@types/node": "20.17.9",
"@types/supertest": "6.0.2",
"@types/jest": "29.5.12",
"@types/node": "20.8.7",
"@types/supertest": "2.0.16",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"eslint": "8.57.1",
"eslint": "8.42.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.29.1",
"jest": "29.7.0",
"prettier": "3.4.2",
"supertest": "6.3.4",
"prettier": "3.2.5",
"supertest": "6.3.3",
"ts-jest": "29.1.2",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",

16620
sample/06-mongoose/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,31 +19,31 @@
"test:e2e": "echo 'No e2e tests implemented yet.'"
},
"dependencies": {
"@nestjs/common": "10.4.13",
"@nestjs/core": "10.4.13",
"@nestjs/mongoose": "10.0.3",
"@nestjs/platform-express": "10.4.13",
"mongoose": "8.8.4",
"reflect-metadata": "0.2.2",
"rimraf": "5.0.10",
"@nestjs/common": "10.3.2",
"@nestjs/core": "10.3.2",
"@nestjs/mongoose": "10.0.2",
"@nestjs/platform-express": "10.3.8",
"mongoose": "8.0.1",
"reflect-metadata": "0.2.1",
"rimraf": "5.0.5",
"rxjs": "7.8.1"
},
"devDependencies": {
"@nestjs/cli": "10.4.8",
"@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.13",
"@nestjs/cli": "10.3.2",
"@nestjs/schematics": "10.1.1",
"@nestjs/testing": "10.3.2",
"@types/express": "4.17.21",
"@types/jest": "29.5.14",
"@types/node": "22.10.0",
"@types/supertest": "6.0.2",
"@types/jest": "29.5.12",
"@types/node": "20.8.7",
"@types/supertest": "2.0.16",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"eslint": "8.57.1",
"eslint": "8.42.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.29.1",
"jest": "29.7.0",
"prettier": "3.4.2",
"supertest": "6.3.4",
"prettier": "3.2.5",
"supertest": "6.3.3",
"ts-jest": "29.1.2",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",

View File

@@ -1,20 +1,23 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Types } from 'mongoose';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
const catsServiceMock = {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
describe('CatsController', () => {
describe('Cats Controller', () => {
let controller: CatsController;
let service: jest.Mocked<CatsService>;
let service: CatsService;
const createCatDto: CreateCatDto = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
const mockCat = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
_id: 'a id',
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -22,124 +25,65 @@ describe('CatsController', () => {
providers: [
{
provide: CatsService,
useValue: catsServiceMock,
useValue: {
findAll: jest.fn().mockResolvedValue([
{
name: 'Cat #1',
breed: 'Bread #1',
age: 4,
},
{
name: 'Cat #2',
breed: 'Breed #2',
age: 3,
},
{
name: 'Cat #3',
breed: 'Breed #3',
age: 2,
},
]),
create: jest.fn().mockResolvedValue(createCatDto),
},
},
],
}).compile();
controller = module.get(CatsController);
service = module.get(CatsService);
controller = module.get<CatsController>(CatsController);
service = module.get<CatsService>(CatsService);
});
describe('create', () => {
describe('create()', () => {
it('should create a new cat', async () => {
const mockedCat = {
_id: new Types.ObjectId(),
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
service.create.mockResolvedValueOnce(mockedCat);
const createSpy = jest
.spyOn(service, 'create')
.mockResolvedValueOnce(mockCat);
const createCatDto: CreateCatDto = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
const result = await controller.create(createCatDto);
expect(result).toEqual(mockedCat);
expect(service.create).toHaveBeenCalledWith(createCatDto);
await controller.create(createCatDto);
expect(createSpy).toHaveBeenCalledWith(createCatDto);
});
});
describe('findAll', () => {
describe('findAll()', () => {
it('should return an array of cats', async () => {
const mockedCats = [
expect(controller.findAll()).resolves.toEqual([
{
_id: new Types.ObjectId(),
name: 'Cat #1',
breed: 'Bread #1',
age: 4,
},
{
_id: new Types.ObjectId(),
name: 'Cat #2',
breed: 'Breed #2',
age: 3,
},
{
_id: new Types.ObjectId(),
name: 'Cat #3',
breed: 'Breed #3',
age: 2,
},
];
service.findAll.mockResolvedValueOnce(mockedCats);
const result = await controller.findAll();
expect(result).toEqual(mockedCats);
]);
expect(service.findAll).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should return a single cat', async () => {
const mockedCat = {
_id: new Types.ObjectId(),
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
service.findOne.mockResolvedValueOnce(mockedCat);
const id = new Types.ObjectId().toString();
const result = await controller.findOne(id);
expect(result).toEqual(mockedCat);
expect(service.findOne).toHaveBeenCalledWith(id);
});
});
describe('update', () => {
it('should update a single cat', async () => {
const mockedCat = {
_id: new Types.ObjectId(),
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
service.update.mockResolvedValueOnce(mockedCat);
const id = new Types.ObjectId().toString();
const updateCatDto: CreateCatDto = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
const result = await controller.update(id, updateCatDto);
expect(result).toEqual(mockedCat);
expect(service.update).toHaveBeenCalledWith(id, updateCatDto);
});
});
describe('delete', () => {
it('should delete a single cat', async () => {
const mockedCat = {
_id: new Types.ObjectId(),
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
service.delete.mockResolvedValueOnce(mockedCat);
const id = new Types.ObjectId().toString();
const result = await controller.delete(id);
expect(result).toEqual(mockedCat);
expect(service.delete).toHaveBeenCalledWith(id);
});
});
});

View File

@@ -1,7 +1,6 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './schemas/cat.schema';
@Controller('cats')
@@ -10,7 +9,7 @@ export class CatsController {
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
await this.catsService.create(createCatDto);
}
@Get()
@@ -23,11 +22,6 @@ export class CatsController {
return this.catsService.findOne(id);
}
@Post(':id')
async update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
return this.catsService.update(id, updateCatDto);
}
@Delete(':id')
async delete(@Param('id') id: string) {
return this.catsService.delete(id);

View File

@@ -1,21 +1,31 @@
import { getModelToken } from '@nestjs/mongoose';
import { Test, TestingModule } from '@nestjs/testing';
import { Model, Types } from 'mongoose';
import { Model } from 'mongoose';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './schemas/cat.schema';
const catModelMock = {
create: jest.fn(),
find: jest.fn(),
findOne: jest.fn(),
findByIdAndUpdate: jest.fn(),
findByIdAndDelete: jest.fn(),
const mockCat = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
describe('CatsService', () => {
let service: CatsService;
let model: jest.Mocked<Model<Cat>>;
let model: Model<Cat>;
const catsArray = [
{
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
},
{
name: 'Cat #2',
breed: 'Breed #2',
age: 2,
},
];
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
@@ -23,128 +33,46 @@ describe('CatsService', () => {
CatsService,
{
provide: getModelToken('Cat'),
useValue: catModelMock,
useValue: {
new: jest.fn().mockResolvedValue(mockCat),
constructor: jest.fn().mockResolvedValue(mockCat),
find: jest.fn(),
create: jest.fn(),
exec: jest.fn(),
},
},
],
}).compile();
service = module.get(CatsService);
model = module.get(getModelToken('Cat'));
service = module.get<CatsService>(CatsService);
model = module.get<Model<Cat>>(getModelToken('Cat'));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should insert a new cat', async () => {
const mockedCat: CreateCatDto = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
model.create.mockResolvedValueOnce(mockedCat as any);
const createCatDto = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
const result = await service.create(createCatDto);
expect(result).toEqual(mockedCat);
expect(model.create).toHaveBeenCalledWith(createCatDto);
});
it('should return all cats', async () => {
jest.spyOn(model, 'find').mockReturnValue({
exec: jest.fn().mockResolvedValueOnce(catsArray),
} as any);
const cats = await service.findAll();
expect(cats).toEqual(catsArray);
});
describe('findAll', () => {
it('should return all cats', async () => {
const mockedCats = [
{
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
},
{
name: 'Cat #2',
breed: 'Breed #2',
age: 2,
},
];
model.find.mockReturnValueOnce({
exec: jest.fn().mockResolvedValueOnce(mockedCats),
} as any);
const result = await service.findAll();
expect(result).toEqual(mockedCats);
expect(model.find).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should return one cat', async () => {
const mockedCat = {
it('should insert a new cat', async () => {
jest.spyOn(model, 'create').mockImplementationOnce(() =>
Promise.resolve({
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
model.findOne.mockReturnValueOnce({
exec: jest.fn().mockResolvedValueOnce(mockedCat),
} as any);
const id = new Types.ObjectId().toString();
const result = await service.findOne(id);
expect(result).toEqual(mockedCat);
expect(model.findOne).toHaveBeenCalledWith({ _id: id });
});
});
describe('update', () => {
it('should update a cat', async () => {
const mockedCat = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
model.findByIdAndUpdate.mockReturnValueOnce({
exec: jest.fn().mockResolvedValueOnce(mockedCat),
} as any);
const id = new Types.ObjectId().toString();
const updateCatDto = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
const result = await service.update(id, updateCatDto);
expect(result).toEqual(mockedCat);
expect(model.findByIdAndUpdate).toHaveBeenCalledWith(
{ _id: id },
updateCatDto,
{ new: true },
);
});
});
describe('delete', () => {
it('should delete a cat', async () => {
const mockedCat = {
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
};
model.findByIdAndDelete.mockReturnValueOnce({
exec: jest.fn().mockResolvedValueOnce(mockedCat),
} as any);
const id = new Types.ObjectId().toString();
const result = await service.delete(id);
expect(result).toEqual(mockedCat);
expect(model.findByIdAndDelete).toHaveBeenCalledWith({ _id: id });
} as any),
);
const newCat = await service.create({
name: 'Cat #1',
breed: 'Breed #1',
age: 4,
});
expect(newCat).toEqual(mockCat);
});
});

View File

@@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './schemas/cat.schema';
@Injectable()
@@ -22,15 +21,9 @@ export class CatsService {
return this.catModel.findOne({ _id: id }).exec();
}
async update(id: string, updateCatDto: UpdateCatDto): Promise<Cat> {
return this.catModel
.findByIdAndUpdate({ _id: id }, updateCatDto, { new: true })
.exec();
}
async delete(id: string): Promise<Cat> {
async delete(id: string) {
const deletedCat = await this.catModel
.findByIdAndDelete({ _id: id })
.findByIdAndRemove({ _id: id })
.exec();
return deletedCat;
}

View File

@@ -1,5 +0,0 @@
export class UpdateCatDto {
readonly name?: string;
readonly age?: number;
readonly breed?: string;
}

View File

@@ -2,7 +2,7 @@ version: "3"
services:
mysql:
image: mysql:9
image: mysql:8
restart: always
environment:
MYSQL_ROOT_PASSWORD: root

16698
sample/07-sequelize/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,34 +19,34 @@
"test:e2e": "echo 'No e2e tests implemented yet.'"
},
"dependencies": {
"@nestjs/common": "10.4.13",
"@nestjs/core": "10.4.13",
"@nestjs/platform-express": "10.4.13",
"@nestjs/sequelize": "10.0.1",
"mysql2": "3.9.8",
"reflect-metadata": "0.2.2",
"rimraf": "5.0.10",
"@nestjs/common": "10.3.2",
"@nestjs/core": "10.3.2",
"@nestjs/platform-express": "10.3.8",
"@nestjs/sequelize": "10.0.0",
"mysql2": "3.9.7",
"reflect-metadata": "0.2.1",
"rimraf": "5.0.5",
"rxjs": "7.8.1",
"sequelize": "6.37.5",
"sequelize": "6.36.0",
"sequelize-typescript": "2.1.6",
"typescript": "5.3.3"
},
"devDependencies": {
"@nestjs/cli": "10.4.8",
"@nestjs/schematics": "10.2.3",
"@nestjs/testing": "10.4.13",
"@nestjs/cli": "10.3.2",
"@nestjs/schematics": "10.1.1",
"@nestjs/testing": "10.3.2",
"@types/express": "4.17.21",
"@types/jest": "29.5.14",
"@types/node": "20.17.9",
"@types/supertest": "6.0.2",
"@types/jest": "29.5.12",
"@types/node": "20.8.7",
"@types/supertest": "2.0.16",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"eslint": "8.57.1",
"eslint": "8.42.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.29.1",
"jest": "29.7.0",
"prettier": "3.4.2",
"supertest": "6.3.4",
"prettier": "3.2.5",
"supertest": "6.3.3",
"ts-jest": "29.1.2",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",

11807
sample/08-webpack/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,26 +12,26 @@
"test:e2e": "echo 'No e2e tests implemented yet.'"
},
"dependencies": {
"@nestjs/common": "10.4.13",
"@nestjs/core": "10.4.13",
"@nestjs/platform-express": "10.4.13",
"reflect-metadata": "0.2.2",
"@nestjs/common": "10.3.2",
"@nestjs/core": "10.3.2",
"@nestjs/platform-express": "10.3.8",
"reflect-metadata": "0.2.1",
"rxjs": "7.8.1",
"typescript": "5.3.3"
},
"devDependencies": {
"@nestjs/cli": "10.4.8",
"@nestjs/schematics": "10.2.3",
"@types/node": "22.10.0",
"@nestjs/cli": "10.3.2",
"@nestjs/schematics": "10.1.1",
"@types/node": "20.8.7",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"eslint": "8.57.1",
"eslint": "8.42.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.29.1",
"start-server-webpack-plugin": "2.2.5",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
"webpack": "5.96.1",
"webpack": "5.90.3",
"webpack-cli": "5.1.4",
"webpack-node-externals": "3.0.0"
}

15213
sample/09-babel-example/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

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