chore: merge master

This commit is contained in:
Kamil Myśliwiec committed 2026-05-13 14:24:58 +02:00
commit f0c95c8d8a
102 files changed
+2902 -522

No files matched your search

+1 -3
View File
@@ -124,9 +124,7 @@ jobs:
- *install-deps
- run:
name: Prepare tests
command: |
bash ./scripts/prepare.sh
sleep 10
command: bash ./scripts/prepare.sh
- run:
name: List containers
command: docker ps
+2 -2
View File
@@ -56,7 +56,7 @@ Please consider what kind of change it is:
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like:
We want to fix all the issues as soon as possible, but before fixing a bug, we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using a repository or [Gist](https://gist.github.com/). Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
- version of NestJS used
- 3rd-party libraries and their versions
@@ -202,7 +202,7 @@ To ensure consistency throughout the source code, keep these rules in mind as yo
// We're working on auto-documentation.
* All public API methods **must be documented**. (Details TBC). -->
- All features or bug fixes **must be tested** by one or more specs (unit-tests).
- All features or bug fixes **must be tested** by one or more specs (unit tests).
- We follow [Google's JavaScript Style Guide][js-style-guide], but wrap all code at
**100 characters**. An automated formatter is available (`npm run format`).
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
@@ -0,0 +1,69 @@
import * as path from 'path';
type ResolveFilename = (
request: string,
parent: NodeModule | null | undefined,
isMain: boolean,
options?: Record<string, unknown>,
) => string;
const Module = require('module') as {
_resolveFilename: ResolveFilename;
};
const workspaceRoot = path.join(__dirname, '..', '..');
const packageRoots = new Map<string, string>([
['@nestjs/common', path.join(workspaceRoot, 'packages/common')],
['@nestjs/core', path.join(workspaceRoot, 'packages/core')],
['@nestjs/microservices', path.join(workspaceRoot, 'packages/microservices')],
[
'@nestjs/platform-express',
path.join(workspaceRoot, 'packages/platform-express'),
],
[
'@nestjs/platform-fastify',
path.join(workspaceRoot, 'packages/platform-fastify'),
],
[
'@nestjs/platform-socket.io',
path.join(workspaceRoot, 'packages/platform-socket.io'),
],
['@nestjs/platform-ws', path.join(workspaceRoot, 'packages/platform-ws')],
['@nestjs/testing', path.join(workspaceRoot, 'packages/testing')],
['@nestjs/websockets', path.join(workspaceRoot, 'packages/websockets')],
]);
const originalResolveFilename = Module._resolveFilename;
Module._resolveFilename = function resolveFilename(
request,
parent,
isMain,
options,
) {
const directMatch = packageRoots.get(request);
if (directMatch) {
return originalResolveFilename.call(
this,
directMatch,
parent,
isMain,
options,
);
}
for (const [packageName, packageRoot] of packageRoots) {
if (request.startsWith(`${packageName}/`)) {
const relativePath = request.slice(packageName.length + 1);
return originalResolveFilename.call(
this,
path.join(packageRoot, relativePath),
parent,
isMain,
options,
);
}
}
return originalResolveFilename.call(this, request, parent, isMain, options);
};
+3 -3
View File
@@ -25,7 +25,7 @@ services:
- "9001:9001"
restart: always
mysql:
image: mysql:9.6.0
image: mysql:9.7.0
environment:
MYSQL_ROOT_HOST: '%'
MYSQL_ROOT_PASSWORD: root
@@ -51,7 +51,7 @@ services:
zookeeper:
container_name: test-zookeeper
hostname: zookeeper
image: confluentinc/cp-zookeeper:7.9.5
image: confluentinc/cp-zookeeper:7.9.6
ports:
- "2181:2181"
environment:
@@ -60,7 +60,7 @@ services:
kafka:
container_name: test-kafka
hostname: kafka
image: confluentinc/cp-kafka:8.1.1
image: confluentinc/cp-kafka:8.2.0
depends_on:
- zookeeper
ports:
@@ -611,4 +611,185 @@ describe('Middleware (FastifyAdapter)', () => {
await app.close();
});
});
describe('should respect fastify routing options', () => {
const MIDDLEWARE_RETURN_VALUE = 'middleware_return';
@Controller()
class TestController {
@Get('abc/def')
included() {
return 'whatnot';
}
}
@Module({
imports: [AppModule],
controllers: [TestController],
})
class TestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res, next) => res.end(MIDDLEWARE_RETURN_VALUE))
.forRoutes({ path: 'abc/def', method: RequestMethod.GET });
}
}
describe('[ignoreTrailingSlash] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
ignoreTrailingSlash: true,
// routerOptions: {
// ignoreTrailingSlash: true,
// },
}),
);
await app.init();
});
it(`GET forRoutes(GET /abc/def/)`, () => {
return app
.inject({
method: 'GET',
url: '/abc/def/', // trailing slash
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('[ignoreDuplicateSlashes] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: {
ignoreDuplicateSlashes: true,
},
}),
);
await app.init();
});
it(`GET forRoutes(GET /abc//def)`, () => {
return app
.inject({
method: 'GET',
url: '/abc//def', // duplicate slashes
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('[caseSensitive] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: {
caseSensitive: true,
},
}),
);
await app.init();
});
it(`GET forRoutes(GET /ABC/DEF)`, () => {
return app
.inject({
method: 'GET',
url: '/ABC/DEF', // different case
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('[useSemicolonDelimiter] attribute', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(
new FastifyAdapter({
routerOptions: { useSemicolonDelimiter: true } as any,
}),
);
await app.init();
});
it(`GET forRoutes(GET /abc/def;foo=bar)`, () => {
return app
.inject({
method: 'GET',
url: '/abc/def;foo=bar', // semicolon delimiter
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
describe('HEAD auto-forwarding to GET', () => {
beforeEach(async () => {
app = (
await Test.createTestingModule({
imports: [TestModule],
}).compile()
).createNestApplication<NestFastifyApplication>(new FastifyAdapter());
await app.init();
});
it(`GET forRoutes(HEAD /abc/def)`, () => {
return app
.inject({
method: 'HEAD',
url: '/abc/def',
})
.then(({ payload }) =>
expect(payload).to.be.eql(MIDDLEWARE_RETURN_VALUE),
);
});
afterEach(async () => {
await app.close();
});
});
});
});
@@ -0,0 +1,115 @@
import { INestApplication } from '@nestjs/common';
import {
ClientRedis,
ClientsModule,
Deserializer,
IncomingResponse,
Transport,
} from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import { lastValueFrom } from 'rxjs';
import Redis from 'ioredis';
class BinaryDeserializer implements Deserializer<Buffer, IncomingResponse> {
deserialize(value: Buffer): IncomingResponse {
const firstSeparatorIndex = value.indexOf(':');
const secondSeparatorIndex = value.indexOf(':', firstSeparatorIndex + 1);
return {
id: value.subarray(0, firstSeparatorIndex).toString(),
isDisposed: true,
err: null,
response: value.subarray(secondSeparatorIndex + 1),
};
}
}
describe('REDIS transport', () => {
let app: INestApplication;
let client: ClientRedis;
let pub: Redis;
let sub: Redis;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [
ClientsModule.register({
clients: [
{
name: 'REDIS_SERVICE',
transport: Transport.REDIS,
options: {
returnBuffers: true,
host: '0.0.0.0',
port: 6379,
deserializer: new BinaryDeserializer(),
},
},
],
}),
],
}).compile();
app = module.createNestApplication();
pub = new Redis();
sub = new Redis();
await sub.subscribe('binary', (_, __) => {});
sub.on('message', async (channel, message) => {
const data = JSON.parse(message);
const delay = data.data === 'slow' ? 25 : 0;
const responseBody =
data.data === 'bytes'
? Buffer.from([0, 1, 2, 3, 255])
: Buffer.from(`${data.data}-replied`);
const response = Buffer.concat([
Buffer.from(`${data.id}:${channel}:`),
responseBody,
]);
if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
await pub.publish(`${channel}.reply`, response);
});
client = app.get<ClientRedis>('REDIS_SERVICE');
await client.connect();
await app.init();
});
it('should return a raw binary payload', async () => {
const data = await lastValueFrom(client.send('binary', 'data'));
expect(Buffer.isBuffer(data)).toBe(true);
expect(data).toEqual(Buffer.from('data-replied'));
});
it('should route concurrent raw binary replies to the matching request', async () => {
const [slowResponse, fastResponse] = await Promise.all([
lastValueFrom(client.send('binary', 'slow')),
lastValueFrom(client.send('binary', 'fast')),
]);
expect(Buffer.isBuffer(slowResponse)).toBe(true);
expect(Buffer.isBuffer(fastResponse)).toBe(true);
expect(slowResponse).toEqual(Buffer.from('slow-replied'));
expect(fastResponse).toEqual(Buffer.from('fast-replied'));
});
it('should preserve non-utf8 bytes in the raw binary payload', async () => {
const data = await lastValueFrom(client.send('binary', 'bytes'));
expect(Buffer.isBuffer(data)).toBe(true);
expect(data).toEqual(Buffer.from([0, 1, 2, 3, 255]));
});
afterEach(async () => {
await app.close();
await client.close();
await pub.quit();
await sub.quit();
});
});
@@ -24,7 +24,7 @@ describe('RabbitMQ transport (Fanout Exchange)', () => {
appConsumer = consumerModule.createNestMicroservice<MicroserviceOptions>({
transport: Transport.RMQ,
options: {
urls: [`amqp://0.0.0.0:5672`],
urls: [`amqp://localhost:5672`],
queue: '',
exchange: 'test.fanout',
exchangeType: 'fanout',
@@ -19,9 +19,9 @@ describe('RabbitMQ transport', () => {
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.RMQ,
options: {
urls: [`amqp://0.0.0.0:5672`],
urls: [`amqp://localhost:5672`],
queue: 'test',
queueOptions: { durable: false },
queueOptions: { durable: true },
socketOptions: { noDelay: true },
},
});
@@ -0,0 +1,159 @@
/**
* Integration test for the TCP json-socket pipelined-frames stack-overflow fix.
*
* Vulnerability: handleData() called itself recursively once per fully-received
* frame, causing RangeError: Maximum call stack size exceeded for payloads with
* many back-to-back frames (e.g. `"2#{}" * 12_000`).
*/
import { Controller, INestMicroservice } from '@nestjs/common';
import {
MessagePattern,
MicroserviceOptions,
Transport,
} from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import * as net from 'net';
@Controller()
class PingController {
@MessagePattern({ cmd: 'ping' })
ping(): string {
return 'pong';
}
}
function frame(obj: unknown): string {
const json = JSON.stringify(obj);
return `${json.length}#${json}`;
}
/**
* Decode as many complete `"<len>#<json>"` frames as possible from `raw`,
* returning parsed objects and the leftover bytes.
*/
function parseFrames(raw: string): {
frames: Record<string, unknown>[];
remaining: string;
} {
const frames: Record<string, unknown>[] = [];
let buf = raw;
while (true) {
const delim = buf.indexOf('#');
if (delim === -1) break;
const len = parseInt(buf.substring(0, delim), 10);
if (isNaN(len)) break;
const payload = buf.substring(delim + 1);
if (payload.length < len) break;
frames.push(
JSON.parse(payload.substring(0, len)) as Record<string, unknown>,
);
buf = payload.substring(len);
}
return { frames, remaining: buf };
}
const TEST_PORT = 4500;
describe('TCP pipelined frames – stack-overflow regression (CVE fix)', () => {
let app: INestMicroservice;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [PingController],
}).compile();
app = module.createNestMicroservice<MicroserviceOptions>({
transport: Transport.TCP,
options: { host: '127.0.0.1', port: TEST_PORT },
logger: false,
});
await app.listen();
});
afterEach(async () => {
await app.close();
});
it('server remains responsive after receiving a 12_000-frame pipelined burst', async () => {
/**
* Each `2#{}` frame is a valid JsonSocket event frame (payload `{}`, length
* 2 characters). The server treats it as a fire-and-forget event (no `id`
* field → no response), so no back-pressure from server replies.
*/
const FRAME_COUNT = 12_000;
const burstFrame = '2#{}';
const pingPayload = JSON.stringify({
pattern: { cmd: 'ping' },
data: null,
id: 'integration-test-ping',
});
const pingFrame = frame({
pattern: { cmd: 'ping' },
data: null,
id: 'integration-test-ping',
});
expect(pingFrame).toBe(`${pingPayload.length}#${pingPayload}`);
await new Promise<void>((resolve, reject) => {
let rawBuf = '';
const socket = new net.Socket();
const done = (err?: Error) => {
socket.destroy();
err ? reject(err) : resolve();
};
const timeout = setTimeout(
() =>
done(
new Error(
'Timed out waiting for pong – the server likely crashed during the burst',
),
),
8_000,
);
socket.setEncoding('utf8');
socket.on('error', err => {
clearTimeout(timeout);
done(
new Error(`Socket error (server may have crashed): ${err.message}`),
);
});
socket.on('data', (chunk: string) => {
rawBuf += chunk;
const { frames } = parseFrames(rawBuf);
const pong = frames.find(f => f['id'] === 'integration-test-ping');
if (pong) {
clearTimeout(timeout);
try {
expect(pong['response']).toBe('pong');
expect(pong['err']).toBeUndefined();
done();
} catch (e) {
done(e as Error);
}
}
});
socket.connect(TEST_PORT, '127.0.0.1', () => {
// Send burst + ping as a single write so they arrive in one TCP segment
// and exercise the iterative parsing loop in one handleData() call.
const combined = Buffer.from(
burstFrame.repeat(FRAME_COUNT) + pingFrame,
);
socket.write(combined);
});
});
}, 15_000);
});
@@ -19,7 +19,7 @@ describe('RabbitMQ transport (Topic Exchange - wildcards)', () => {
app.connectMicroservice<MicroserviceOptions>({
transport: Transport.RMQ,
options: {
urls: [`amqp://0.0.0.0:5672`],
urls: [`amqp://localhost:5672`],
queue: 'test2',
wildcards: true,
},
@@ -18,7 +18,7 @@ export class RMQBroadcastController {
options: {
urls: [`amqp://localhost:5672`],
queue: 'test_broadcast',
queueOptions: { durable: false },
queueOptions: { durable: true },
socketOptions: { noDelay: true },
},
});
@@ -25,7 +25,7 @@ export class RMQController {
options: {
urls: [`amqp://localhost:5672`],
queue: 'test',
queueOptions: { durable: false },
queueOptions: { durable: true },
socketOptions: { noDelay: true },
},
});
@@ -69,7 +69,7 @@ export class RMQController {
options: {
urls: [`amqp://localhost:5671`, `amqp://localhost:5672`],
queue: 'test',
queueOptions: { durable: false },
queueOptions: { durable: true },
socketOptions: { noDelay: true },
},
});
@@ -15,3 +15,18 @@ export async function randomPort(): Promise<number> {
});
});
}
export async function getAvailableIpv4Host(
preferredHost = '127.0.0.5',
): Promise<string> {
const server = net.createServer();
return new Promise(resolve => {
server.once('error', () => {
resolve('127.0.0.1');
});
server.listen(0, preferredHost, () => {
server.close(() => resolve(preferredHost));
});
});
}
@@ -1,5 +1,13 @@
import { Controller, MessageEvent, Sse } from '@nestjs/common';
import { interval, map, Observable } from 'rxjs';
import { Type } from 'class-transformer';
import { IsInt } from 'class-validator';
import { Controller, MessageEvent, Query, Sse } from '@nestjs/common';
import { interval, map, Observable, of } from 'rxjs';
class SseQueryDto {
@Type(() => Number)
@IsInt()
limit!: number;
}
@Controller()
export class AppController {
@@ -9,4 +17,25 @@ export class AppController {
map(() => ({ data: { hello: 'world' } }) as MessageEvent),
);
}
@Sse('sse/validated')
sseWithValidatedQuery(@Query() query: SseQueryDto): Observable<MessageEvent> {
return of({ data: { limit: query.limit } });
}
@Sse('sse/burst')
sseBurst(
@Query('n') n = '20',
@Query('size') size = '65536',
): Observable<MessageEvent> {
const count = parseInt(n, 10);
const payload = 'X'.repeat(parseInt(size, 10));
return new Observable(subscriber => {
for (let i = 0; i < count; i++) {
subscriber.next({ data: payload });
}
subscriber.complete();
});
}
}
@@ -185,6 +185,221 @@ describe('Transient scope', () => {
});
});
describe('when multiple DEFAULT parents inject the same TRANSIENT -> TRANSIENT chain', () => {
let app: INestApplication;
@Injectable({ scope: Scope.TRANSIENT })
class IsolatedNestedTransient {
public static instanceCount = 0;
public readonly instanceId: number;
constructor() {
IsolatedNestedTransient.instanceCount++;
this.instanceId = IsolatedNestedTransient.instanceCount;
}
}
@Injectable({ scope: Scope.TRANSIENT })
class IsolatedTransientLogger {
public static instanceCount = 0;
public readonly instanceId: number;
constructor(public readonly nested: IsolatedNestedTransient) {
IsolatedTransientLogger.instanceCount++;
this.instanceId = IsolatedTransientLogger.instanceCount;
}
}
@Injectable()
class ServiceA {
constructor(public readonly logger: IsolatedTransientLogger) {}
}
@Injectable()
class ServiceB {
constructor(public readonly logger: IsolatedTransientLogger) {}
}
beforeAll(async () => {
IsolatedNestedTransient.instanceCount = 0;
IsolatedTransientLogger.instanceCount = 0;
const module = await Test.createTestingModule({
providers: [
ServiceA,
ServiceB,
IsolatedTransientLogger,
IsolatedNestedTransient,
],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should create separate TransientLogger instances for each DEFAULT parent', () => {
const serviceA = app.get(ServiceA);
const serviceB = app.get(ServiceB);
expect(serviceA.logger.instanceId).to.not.equal(
serviceB.logger.instanceId,
);
});
it('should create separate nested TRANSIENT instances for each DEFAULT parent', () => {
const serviceA = app.get(ServiceA);
const serviceB = app.get(ServiceB);
expect(serviceA.logger.nested.instanceId).to.not.equal(
serviceB.logger.nested.instanceId,
);
});
afterAll(async () => {
await app.close();
});
});
describe('when multiple DEFAULT parents inject a deeply nested TRANSIENT chain', () => {
let app: INestApplication;
@Injectable({ scope: Scope.TRANSIENT })
class DepthSixTransient {
public static constructorCalled = false;
public static instanceCount = 0;
public readonly instanceId: number;
public readonly initialized = true;
constructor() {
DepthSixTransient.constructorCalled = true;
DepthSixTransient.instanceCount++;
this.instanceId = DepthSixTransient.instanceCount;
}
}
@Injectable({ scope: Scope.TRANSIENT })
class DepthFiveTransient {
public static constructorCalled = false;
public static instanceCount = 0;
public readonly instanceId: number;
constructor(public readonly next: DepthSixTransient) {
DepthFiveTransient.constructorCalled = true;
DepthFiveTransient.instanceCount++;
this.instanceId = DepthFiveTransient.instanceCount;
}
}
@Injectable({ scope: Scope.TRANSIENT })
class DepthFourTransient {
public static constructorCalled = false;
constructor(public readonly next: DepthFiveTransient) {
DepthFourTransient.constructorCalled = true;
}
}
@Injectable({ scope: Scope.TRANSIENT })
class DepthThreeTransient {
public static constructorCalled = false;
constructor(public readonly next: DepthFourTransient) {
DepthThreeTransient.constructorCalled = true;
}
}
@Injectable({ scope: Scope.TRANSIENT })
class DepthTwoTransient {
public static constructorCalled = false;
constructor(public readonly next: DepthThreeTransient) {
DepthTwoTransient.constructorCalled = true;
}
}
@Injectable({ scope: Scope.TRANSIENT })
class DepthOneTransient {
public static constructorCalled = false;
constructor(public readonly next: DepthTwoTransient) {
DepthOneTransient.constructorCalled = true;
}
}
@Injectable()
class DeepServiceA {
constructor(public readonly chain: DepthOneTransient) {}
}
@Injectable()
class DeepServiceB {
constructor(public readonly chain: DepthOneTransient) {}
}
beforeAll(async () => {
DepthOneTransient.constructorCalled = false;
DepthTwoTransient.constructorCalled = false;
DepthThreeTransient.constructorCalled = false;
DepthFourTransient.constructorCalled = false;
DepthFiveTransient.constructorCalled = false;
DepthSixTransient.constructorCalled = false;
DepthFiveTransient.instanceCount = 0;
DepthSixTransient.instanceCount = 0;
const module = await Test.createTestingModule({
providers: [
DeepServiceA,
DeepServiceB,
DepthOneTransient,
DepthTwoTransient,
DepthThreeTransient,
DepthFourTransient,
DepthFiveTransient,
DepthSixTransient,
],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should create separate level-5 transient instances for each DEFAULT parent', () => {
const serviceA = app.get(DeepServiceA);
const serviceB = app.get(DeepServiceB);
expect(serviceA.chain.next.next.next.next.instanceId).to.not.equal(
serviceB.chain.next.next.next.next.instanceId,
);
});
it('should create separate level-6 transient instances for each DEFAULT parent', () => {
const serviceA = app.get(DeepServiceA);
const serviceB = app.get(DeepServiceB);
expect(serviceA.chain.next.next.next.next.next.instanceId).to.not.equal(
serviceB.chain.next.next.next.next.next.instanceId,
);
expect(serviceA.chain.next.next.next.next.next.initialized).to.be.true;
expect(serviceB.chain.next.next.next.next.next.initialized).to.be.true;
});
it('should call constructors for every transient provider in the deep chain', () => {
app.get(DeepServiceA);
app.get(DeepServiceB);
expect(DepthOneTransient.constructorCalled).to.be.true;
expect(DepthTwoTransient.constructorCalled).to.be.true;
expect(DepthThreeTransient.constructorCalled).to.be.true;
expect(DepthFourTransient.constructorCalled).to.be.true;
expect(DepthFiveTransient.constructorCalled).to.be.true;
expect(DepthSixTransient.constructorCalled).to.be.true;
});
afterAll(async () => {
await app.close();
});
});
describe('when nested transient providers are used in request scope', () => {
let server: any;
let app: INestApplication;
+213 -24
View File
@@ -10739,6 +10739,7 @@
"version": "5.7.4",
"resolved": "https://registry.npmjs.org/fastify/-/fastify-5.7.4.tgz",
"integrity": "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -10954,6 +10955,7 @@
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.4.0.tgz",
"integrity": "sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -17272,6 +17274,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -17493,6 +17496,7 @@
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
@@ -17724,6 +17728,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
@@ -17742,6 +17747,7 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -17751,6 +17757,7 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -17760,6 +17767,7 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@@ -17772,6 +17780,7 @@
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
@@ -24889,6 +24898,7 @@
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -24943,6 +24953,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4"
@@ -25038,10 +25049,10 @@
},
"packages/common": {
"name": "@nestjs/common",
"version": "11.1.13",
"version": "11.1.20",
"license": "MIT",
"dependencies": {
"file-type": "21.3.0",
"file-type": "21.3.4",
"iterare": "1.2.1",
"load-esm": "1.0.3",
"tslib": "2.8.1",
@@ -25066,21 +25077,39 @@
}
}
},
"packages/common/node_modules/file-type": {
"version": "21.3.4",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz",
"integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==",
"license": "MIT",
"dependencies": {
"@tokenizer/inflate": "^0.4.1",
"strtok3": "^10.3.4",
"token-types": "^6.1.1",
"uint8array-extras": "^1.4.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"packages/core": {
"name": "@nestjs/core",
"version": "11.1.13",
"version": "11.1.20",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@nuxt/opencollective": "0.4.1",
"fast-safe-stringify": "2.1.1",
"iterare": "1.2.1",
"path-to-regexp": "8.3.0",
"path-to-regexp": "8.4.2",
"tslib": "2.8.1",
"uid": "2.0.2"
},
"devDependencies": {
"@nestjs/common": "11.1.13"
"@nestjs/common": "11.1.20"
},
"engines": {
"node": ">= 20"
@@ -25109,17 +25138,27 @@
}
}
},
"packages/core/node_modules/path-to-regexp": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"packages/microservices": {
"name": "@nestjs/microservices",
"version": "11.1.13",
"version": "11.1.20",
"license": "MIT",
"dependencies": {
"iterare": "1.2.1",
"tslib": "2.8.1"
},
"devDependencies": {
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13"
"@nestjs/common": "11.1.20",
"@nestjs/core": "11.1.20"
},
"funding": {
"type": "opencollective",
@@ -25172,18 +25211,18 @@
},
"packages/platform-express": {
"name": "@nestjs/platform-express",
"version": "11.1.13",
"version": "11.1.20",
"license": "MIT",
"dependencies": {
"cors": "2.8.6",
"express": "5.2.1",
"multer": "2.0.2",
"path-to-regexp": "8.3.0",
"multer": "2.1.1",
"path-to-regexp": "8.4.2",
"tslib": "2.8.1"
},
"devDependencies": {
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13"
"@nestjs/common": "11.1.20",
"@nestjs/core": "11.1.20"
},
"funding": {
"type": "opencollective",
@@ -25194,19 +25233,91 @@
"@nestjs/core": "^11.0.0"
}
},
"packages/platform-express/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"packages/platform-express/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"packages/platform-express/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"packages/platform-express/node_modules/multer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"packages/platform-express/node_modules/path-to-regexp": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"packages/platform-express/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"packages/platform-fastify": {
"name": "@nestjs/platform-fastify",
"version": "11.1.13",
"version": "11.1.20",
"license": "MIT",
"dependencies": {
"@fastify/cors": "11.2.0",
"@fastify/formbody": "8.0.2",
"fast-querystring": "1.1.2",
"fastify": "5.7.4",
"fastify": "5.8.5",
"fastify-plugin": "5.1.0",
"find-my-way": "9.4.0",
"find-my-way": "9.6.0",
"light-my-request": "6.6.0",
"path-to-regexp": "8.3.0",
"path-to-regexp": "8.4.2",
"reusify": "1.1.0",
"tslib": "2.8.1"
},
@@ -25229,9 +25340,66 @@
}
}
},
"packages/platform-fastify/node_modules/fastify": {
"version": "5.8.5",
"resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz",
"integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@fastify/ajv-compiler": "^4.0.5",
"@fastify/error": "^4.0.0",
"@fastify/fast-json-stringify-compiler": "^5.0.0",
"@fastify/proxy-addr": "^5.0.0",
"abstract-logging": "^2.0.1",
"avvio": "^9.0.0",
"fast-json-stringify": "^6.0.0",
"find-my-way": "^9.0.0",
"light-my-request": "^6.0.0",
"pino": "^9.14.0 || ^10.1.0",
"process-warning": "^5.0.0",
"rfdc": "^1.3.1",
"secure-json-parse": "^4.0.0",
"semver": "^7.6.0",
"toad-cache": "^3.7.0"
}
},
"packages/platform-fastify/node_modules/find-my-way": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz",
"integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-querystring": "^1.0.0",
"safe-regex2": "^5.0.0"
},
"engines": {
"node": ">=20"
}
},
"packages/platform-fastify/node_modules/path-to-regexp": {
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"packages/platform-socket.io": {
"name": "@nestjs/platform-socket.io",
"version": "11.1.13",
"version": "11.1.20",
"license": "MIT",
"dependencies": {
"socket.io": "4.8.3",
@@ -25249,11 +25417,11 @@
},
"packages/platform-ws": {
"name": "@nestjs/platform-ws",
"version": "11.1.13",
"version": "11.1.20",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1",
"ws": "8.19.0"
"ws": "8.20.1"
},
"funding": {
"type": "opencollective",
@@ -25265,9 +25433,30 @@
"rxjs": "^7.1.0"
}
},
"packages/platform-ws/node_modules/ws": {
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"packages/testing": {
"name": "@nestjs/testing",
"version": "11.1.13",
"version": "11.1.20",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
@@ -25293,7 +25482,7 @@
},
"packages/websockets": {
"name": "@nestjs/websockets",
"version": "11.1.13",
"version": "11.1.20",
"license": "MIT",
"dependencies": {
"iterare": "1.2.1",
@@ -25301,8 +25490,8 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13"
"@nestjs/common": "11.1.20",
"@nestjs/core": "11.1.20"
},
"peerDependencies": {
"@nestjs/common": "^11.0.0",
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
@@ -38,6 +38,7 @@ export function UsePipes(
pipe && (isFunction(pipe) || isFunction(pipe.transform));
if (descriptor) {
validateEach(target.constructor, pipes, isPipeValid, '@UsePipes', 'pipe');
extendArrayMetadata(PIPES_METADATA, pipes, descriptor.value);
return descriptor;
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@nestjs/common",
"version": "11.1.13",
"version": "11.1.20",
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
"author": "Kamil Mysliwiec",
"homepage": "https://nestjs.com",
@@ -26,7 +26,7 @@
"./*": "./*.js"
},
"dependencies": {
"file-type": "21.3.0",
"file-type": "21.3.4",
"iterare": "1.2.1",
"load-esm": "1.0.3",
"tslib": "2.8.1",
@@ -78,6 +78,21 @@ export class FileTypeValidator extends FileValidator<
: errorMessage;
}
/**
* If the file buffer is not available, and fallbackToMimetype is not enabled,
* we cannot perform magic number validation,
* so we return a specific error message indicating
* that validation could not be performed due to missing buffer.
*/
if (
file?.mimetype &&
!file.buffer &&
!this.validationOptions?.fallbackToMimetype &&
!this.validationOptions?.skipMagicNumbersValidation
) {
return `Validation failed (file buffer is not available; file type validation could not be performed; expected type is ${this.validationOptions.fileType})`;
}
if (file?.mimetype) {
const baseMessage = `Validation failed (current file type is ${file.mimetype}, expected type is ${this.validationOptions.fileType})`;
+1 -1
View File
@@ -55,7 +55,7 @@ export class ParseDatePipe implements PipeTransform {
return this.options.default ? this.options.default() : value;
}
if (!value) {
if (isNil(value) || value === '') {
throw this.exceptionFactory('Validation failed (no Date provided)');
}
@@ -598,7 +598,7 @@ export class ConsoleLogger implements LoggerService {
return value;
}
private getContextAndMessagesToPrint(args: unknown[]) {
protected getContextAndMessagesToPrint(args: unknown[]) {
if (args?.length <= 1) {
return { messages: args, context: this.context };
}
@@ -638,7 +638,7 @@ export class ConsoleLogger implements LoggerService {
return { messages, context, params };
}
private getContextAndStackAndMessagesToPrint(args: unknown[]) {
protected getContextAndStackAndMessagesToPrint(args: unknown[]) {
if (args.length === 2) {
if (this.isStackFormat(args[1])) {
return {
@@ -682,7 +682,7 @@ export class ConsoleLogger implements LoggerService {
};
}
private isStackFormat(stack: unknown) {
protected isStackFormat(stack: unknown) {
if (!isString(stack) && !isUndefined(stack)) {
return false;
}
@@ -690,7 +690,7 @@ export class ConsoleLogger implements LoggerService {
return /^(.)+\n\s+at .+:\d+:\d+/.test(stack!);
}
private getColorByLogLevel(level: LogLevel) {
protected getColorByLogLevel(level: LogLevel) {
switch (level) {
case 'debug':
return clc.magentaBright;
@@ -33,5 +33,11 @@ describe('@UsePipes', () => {
} catch (e) {
expect(e).toBeInstanceOf(InvalidDecoratorItemException);
}
try {
UsePipes('test' as any)(() => {}, 'test', {}); // with descriptor
} catch (e) {
expect(e).to.be.instanceof(InvalidDecoratorItemException);
}
});
});
@@ -14,6 +14,10 @@ const jpegBuffer = Buffer.from([
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46,
]);
const pdfBuffer = Buffer.from([
0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x37, 0x0a,
]);
describe('FileTypeValidator', () => {
describe('isValid', () => {
describe('support file types', () => {
@@ -279,7 +283,7 @@ describe('FileTypeValidator', () => {
fileType,
});
const file = { mimetype: currentFileType } as IFile;
const file = { mimetype: currentFileType, buffer: pngBuffer } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).toBe(
`Validation failed (current file type is ${currentFileType}, expected type is ${fileType})`,
@@ -290,7 +294,7 @@ describe('FileTypeValidator', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: /^image\//,
});
const file = { mimetype: 'application/pdf' } as IFile;
const file = { mimetype: 'application/pdf', buffer: pdfBuffer } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).toBe(
`Validation failed (current file type is application/pdf, expected type is /^image\\//)`,
@@ -301,13 +305,28 @@ describe('FileTypeValidator', () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'jpeg',
});
const file = { mimetype: 'image/png' } as IFile;
const file = { mimetype: 'image/png', buffer: pngBuffer } as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).toBe(
'Validation failed (current file type is image/png, expected type is jpeg)',
);
});
it('should return a specific error message when file buffer is not available and fallbackToMimetype is not enabled', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'image/jpeg',
fallbackToMimetype: false,
});
const file = {
mimetype: 'image/jpeg',
} as IFile;
expect(fileTypeValidator.buildErrorMessage(file)).to.equal(
`Validation failed (file buffer is not available; file type validation could not be performed; expected type is image/jpeg)`,
);
});
it('should return false for text file with small buffer and correct mimetype but fail magic number validation', async () => {
const fileTypeValidator = new FileTypeValidator({
fileType: 'text/plain',
@@ -23,6 +23,13 @@ describe('ParseDatePipe', () => {
expect(transformedNumber.getTime()).toBe(asNumber);
});
it('should parse zero timestamp as a valid date', () => {
const transformedDate = target.transform(0)!;
expect(transformedDate).to.be.instanceOf(Date);
expect(transformedDate.getTime()).to.equal(0);
});
it('should not throw an error if the value is undefined/null and optional is true', () => {
const target = new ParseDatePipe({ optional: true });
const value = target.transform(undefined);
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
+6 -1
View File
@@ -127,9 +127,14 @@ For more common dependency resolution issues, see: https://docs.nestjs.com/faq/c
);
dependenciesName[index] = '?';
const tokenFragment =
!isImportTypeIssue && name !== undefined ? ` ${dependencyName}` : '';
const contextLabel = isImportTypeIssue ? 'current' : moduleName;
message += ` (`;
message += dependenciesName.join(', ');
message += `). Please make sure that the argument ${isImportTypeIssue ? 'dependency' : dependencyName} at index [${index}] is available in the ${isImportTypeIssue ? 'current' : moduleName} context.`;
message += `). Please make sure that the argument${tokenFragment} at index [${index}]`;
message += ` is available in the ${contextLabel} module.`;
message += potentialSolutions;
return message;
+1 -1
View File
@@ -23,7 +23,7 @@ export * from './metadata-scanner.js';
export * from './middleware/index.js';
export * from './nest-application.js';
export * from './nest-application-context.js';
export { NestFactory } from './nest-factory.js';
export { IEntryNestModule, NestFactory } from './nest-factory.js';
export * from './repl/index.js';
export * from './router/index.js';
export * from './services/index.js';
+280 -119
View File
@@ -4,6 +4,7 @@ import {
Logger,
type LoggerService,
type OptionalFactoryDependency,
Scope,
} from '@nestjs/common';
import {
type Controller,
@@ -78,6 +79,12 @@ export interface InjectorDependencyContext {
dependencies?: InjectorDependency[];
}
interface ResolutionContext {
contextId: ContextId;
inquirer?: InstanceWrapper;
effectiveInquirerId?: string;
}
export class Injector {
private logger: LoggerService = new Logger('InjectorLogger');
private readonly instanceDecorator: (target: unknown) => unknown = (
@@ -90,6 +97,10 @@ export class Injector {
* Whether to enable preview mode.
*/
preview: boolean;
/**
* Whether to enable deterministic graph snapshot generation.
*/
snapshot?: boolean;
/**
* Function to decorate a freshly created instance.
*/
@@ -124,18 +135,20 @@ export class Injector {
wrapper: InstanceWrapper<T>,
collection: Map<InjectionToken, InstanceWrapper>,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
) {
const inquirerId = this.getInquirerId(inquirer);
const inquirerId = this.getContextInquirerId(resolutionContext);
const instanceHost = wrapper.getInstanceByContextId(
this.getContextId(contextId, wrapper),
this.getContextId(resolutionContext.contextId, wrapper),
inquirerId,
);
if (instanceHost.isPending) {
const settlementSignal = wrapper.settlementSignal;
if (inquirer && settlementSignal?.isCycle(inquirer.id)) {
if (
resolutionContext.inquirer &&
settlementSignal?.isCycle(resolutionContext.inquirer.id)
) {
throw new CircularDependencyException(`"${wrapper.name}"`);
}
@@ -159,21 +172,24 @@ export class Injector {
}
try {
const t0 = this.getNowTimestamp();
const localResolutionContext = this.createResolutionContext(
resolutionContext.contextId,
wrapper,
inquirerId,
);
const callback = async (instances: unknown[], depth = 0) => {
const properties = await this.resolveProperties(
wrapper,
moduleRef,
inject as InjectionToken[],
contextId,
wrapper,
inquirer,
localResolutionContext,
resolutionContext.inquirer,
);
const instance = await this.instantiateClass(
instances,
wrapper,
targetWrapper,
contextId,
inquirer,
wrapper.isTransient ? localResolutionContext : resolutionContext,
);
this.applyProperties(instance, properties);
@@ -186,13 +202,12 @@ export class Injector {
moduleRef,
inject as InjectionToken[],
callback,
contextId,
wrapper,
inquirer,
localResolutionContext,
resolutionContext.inquirer,
);
} catch (err) {
wrapper.removeInstanceByContextId(
this.getContextId(contextId, wrapper),
this.getContextId(resolutionContext.contextId, wrapper),
inquirerId,
);
@@ -218,8 +233,7 @@ export class Injector {
wrapper,
collection,
moduleRef,
contextId,
inquirer || wrapper,
this.createResolutionContext(contextId, inquirer || wrapper),
);
}
@@ -233,8 +247,7 @@ export class Injector {
wrapper,
controllers,
moduleRef,
contextId,
wrapper,
this.createResolutionContext(contextId, wrapper),
);
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
}
@@ -250,26 +263,30 @@ export class Injector {
wrapper,
injectables,
moduleRef,
contextId,
inquirer,
this.createResolutionContext(contextId, inquirer),
);
}
public async loadProvider(
wrapper: InstanceWrapper<Injectable>,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
) {
if (this.shouldSkipProviderLoading(wrapper, resolutionContext)) {
return;
}
const providers = moduleRef.providers;
await this.loadInstance<Injectable>(
wrapper,
providers,
moduleRef,
contextId,
inquirer,
resolutionContext,
);
await this.loadEnhancersPerContext(
wrapper,
resolutionContext.contextId,
wrapper,
);
await this.loadEnhancersPerContext(wrapper, contextId, wrapper);
}
public applySettlementSignal<T>(
@@ -289,17 +306,16 @@ export class Injector {
moduleRef: Module,
inject: InjectorDependency[] | undefined,
callback: (args: unknown[], depth?: number) => void | Promise<void>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
parentInquirer?: InstanceWrapper,
) {
const metadata = wrapper.getCtorMetadata();
if (metadata && contextId !== STATIC_CONTEXT) {
if (metadata && resolutionContext.contextId !== STATIC_CONTEXT) {
const deps = await this.loadCtorMetadata(
metadata,
contextId,
inquirer,
resolutionContext.contextId,
resolutionContext.inquirer,
parentInquirer,
);
return callback(deps);
@@ -323,19 +339,23 @@ export class Injector {
return parentInquirer && parentInquirer.instance;
}
if (inquirer?.isTransient && parentInquirer) {
if (resolutionContext.inquirer?.isTransient && parentInquirer) {
// When `inquirer` is transient too, inherit the parent inquirer
// This is required to ensure that transient providers are only resolved
// when requested
inquirer.attachRootInquirer(parentInquirer);
resolutionContext.inquirer.attachRootInquirer(parentInquirer);
}
const nestedResolutionContext =
this.getStaticTransientResolutionContext(
resolutionContext,
parentInquirer,
);
const paramWrapper = await this.resolveSingleParam<T>(
wrapper,
param as Type | string | symbol,
{ index, dependencies },
moduleRef,
contextId,
inquirer,
nestedResolutionContext,
index,
);
@@ -346,17 +366,15 @@ export class Injector {
*/
await paramBarrier.signalAndWait();
const effectiveInquirer = this.getEffectiveInquirer(
const effectiveResolutionContext = this.getEffectiveResolutionContext(
paramWrapper,
inquirer,
resolutionContext,
parentInquirer,
contextId,
);
const paramWrapperWithInstance = await this.resolveComponentHost(
moduleRef,
paramWrapper,
contextId,
effectiveInquirer,
effectiveResolutionContext,
);
if (paramWrapperWithInstance.hierarchyLevel > depth) {
@@ -364,8 +382,11 @@ export class Injector {
}
const instanceHost = paramWrapperWithInstance.getInstanceByContextId(
this.getContextId(contextId, paramWrapperWithInstance),
this.getInquirerId(effectiveInquirer),
this.getContextId(
effectiveResolutionContext.contextId,
paramWrapperWithInstance,
),
effectiveResolutionContext.effectiveInquirerId,
);
if (!instanceHost.isResolved && !paramWrapperWithInstance.forwardRef) {
isResolved = false;
@@ -447,7 +468,7 @@ export class Injector {
const selfParams = this.reflectSelfParams<T>(type);
selfParams.forEach(({ index, param }) => (paramtypes[index] = param));
return paramtypes;
return Array.from(paramtypes);
}
public reflectOptionalParams<T>(type: Type<T>): any[] {
@@ -463,8 +484,7 @@ export class Injector {
param: Type<any> | string | symbol,
dependencyContext: InjectorDependencyContext,
moduleRef: Module,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
keyOrIndex?: symbol | string | number,
) {
if (isUndefined(param)) {
@@ -483,8 +503,7 @@ export class Injector {
token,
dependencyContext,
wrapper,
contextId,
inquirer,
resolutionContext,
keyOrIndex,
);
}
@@ -505,11 +524,10 @@ export class Injector {
token: InjectionToken,
dependencyContext: InjectorDependencyContext,
wrapper: InstanceWrapper<T>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
keyOrIndex?: symbol | string | number,
): Promise<InstanceWrapper> {
this.printResolvingDependenciesLog(token, inquirer);
this.printResolvingDependenciesLog(token, resolutionContext.inquirer);
this.printLookingForProviderLog(token, moduleRef);
const providers = moduleRef.providers;
return this.lookupComponent(
@@ -517,8 +535,7 @@ export class Injector {
moduleRef,
{ ...dependencyContext, name: token },
wrapper,
contextId,
inquirer,
resolutionContext,
keyOrIndex,
);
}
@@ -526,27 +543,27 @@ export class Injector {
public async resolveComponentHost<T>(
moduleRef: Module,
instanceWrapper: InstanceWrapper<T | Promise<T>>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
): Promise<InstanceWrapper> {
const inquirerId = this.getInquirerId(inquirer);
const inquirerId = this.getContextInquirerId(resolutionContext);
const instanceHost = instanceWrapper.getInstanceByContextId(
this.getContextId(contextId, instanceWrapper),
this.getContextId(resolutionContext.contextId, instanceWrapper),
inquirerId,
);
if (!instanceHost.isResolved && !instanceWrapper.forwardRef) {
inquirer?.settlementSignal?.insertRef(instanceWrapper.id);
resolutionContext.inquirer?.settlementSignal?.insertRef(
instanceWrapper.id,
);
await this.loadProvider(
instanceWrapper,
instanceWrapper.host ?? moduleRef,
contextId,
inquirer,
resolutionContext,
);
} else if (
!instanceHost.isResolved &&
instanceWrapper.forwardRef &&
(contextId !== STATIC_CONTEXT || !!inquirerId)
(resolutionContext.contextId !== STATIC_CONTEXT || !!inquirerId)
) {
/**
* When circular dependency has been detected between
@@ -558,7 +575,7 @@ export class Injector {
instanceHost.donePromise &&
void instanceHost.donePromise
.then(() =>
this.loadProvider(instanceWrapper, moduleRef, contextId, inquirer),
this.loadProvider(instanceWrapper, moduleRef, resolutionContext),
)
.catch(err => {
instanceWrapper.settlementSignal?.error(err);
@@ -566,11 +583,15 @@ export class Injector {
}
if (instanceWrapper.async) {
const host = instanceWrapper.getInstanceByContextId(
this.getContextId(contextId, instanceWrapper),
this.getContextId(resolutionContext.contextId, instanceWrapper),
inquirerId,
);
host.instance = await host.instance;
instanceWrapper.setInstanceByContextId(contextId, host, inquirerId);
instanceWrapper.setInstanceByContextId(
resolutionContext.contextId,
host,
inquirerId,
);
}
return instanceWrapper;
}
@@ -580,8 +601,7 @@ export class Injector {
moduleRef: Module,
dependencyContext: InjectorDependencyContext,
wrapper: InstanceWrapper<T>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
keyOrIndex?: symbol | string | number,
): Promise<InstanceWrapper<T>> {
const token = wrapper.token || wrapper.name;
@@ -604,8 +624,7 @@ export class Injector {
dependencyContext,
moduleRef,
wrapper,
contextId,
inquirer,
resolutionContext,
keyOrIndex,
);
}
@@ -614,8 +633,7 @@ export class Injector {
dependencyContext: InjectorDependencyContext,
moduleRef: Module,
wrapper: InstanceWrapper<T>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
keyOrIndex?: symbol | string | number,
) {
const instanceWrapper = await this.lookupComponentInImports(
@@ -623,8 +641,7 @@ export class Injector {
dependencyContext.name!,
wrapper,
new Set<string>(),
contextId,
inquirer,
resolutionContext,
keyOrIndex,
);
if (isNil(instanceWrapper)) {
@@ -643,8 +660,7 @@ export class Injector {
name: InjectionToken,
wrapper: InstanceWrapper,
moduleRegistry: Set<string> = new Set<string>(),
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
keyOrIndex?: symbol | string | number,
isTraversing?: boolean,
): Promise<any> {
@@ -673,8 +689,7 @@ export class Injector {
name,
wrapper,
moduleRegistry,
contextId,
inquirer,
resolutionContext,
keyOrIndex,
true,
);
@@ -688,9 +703,9 @@ export class Injector {
instanceWrapperRef = providers.get(name)!;
this.addDependencyMetadata(keyOrIndex!, wrapper, instanceWrapperRef);
const inquirerId = this.getInquirerId(inquirer);
const inquirerId = this.getContextInquirerId(resolutionContext);
const instanceHost = instanceWrapperRef.getInstanceByContextId(
this.getContextId(contextId, instanceWrapperRef),
this.getContextId(resolutionContext.contextId, instanceWrapperRef),
inquirerId,
);
if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
@@ -709,16 +724,19 @@ export class Injector {
wrapper: InstanceWrapper<T>,
moduleRef: Module,
inject?: InjectorDependency[],
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
parentInquirer?: InstanceWrapper,
): Promise<PropertyDependency[]> {
if (!isNil(inject)) {
return [];
}
const metadata = wrapper.getPropertiesMetadata();
if (metadata && contextId !== STATIC_CONTEXT) {
return this.loadPropertiesMetadata(metadata, contextId, inquirer);
if (metadata && resolutionContext.contextId !== STATIC_CONTEXT) {
return this.loadPropertiesMetadata(
metadata,
resolutionContext.contextId,
resolutionContext.inquirer,
);
}
const properties = this.reflectProperties(wrapper.metatype as Type<any>);
const propertyBarrier = new Barrier(properties.length);
@@ -737,13 +755,17 @@ export class Injector {
return parentInquirer && parentInquirer.instance;
}
const nestedResolutionContext =
this.getStaticTransientResolutionContext(
resolutionContext,
parentInquirer,
);
const paramWrapper = await this.resolveSingleParam<T>(
wrapper,
item.name as string,
dependencyContext,
moduleRef,
contextId,
inquirer,
nestedResolutionContext,
item.key,
);
@@ -754,24 +776,26 @@ export class Injector {
*/
await propertyBarrier.signalAndWait();
const effectivePropertyInquirer = this.getEffectiveInquirer(
paramWrapper,
inquirer,
parentInquirer,
contextId,
);
const effectivePropertyResolutionContext =
this.getEffectiveResolutionContext(
paramWrapper,
resolutionContext,
parentInquirer,
);
const paramWrapperWithInstance = await this.resolveComponentHost(
moduleRef,
paramWrapper,
contextId,
effectivePropertyInquirer,
effectivePropertyResolutionContext,
);
if (!paramWrapperWithInstance) {
return undefined;
}
const instanceHost = paramWrapperWithInstance.getInstanceByContextId(
this.getContextId(contextId, paramWrapperWithInstance),
this.getInquirerId(effectivePropertyInquirer),
this.getContextId(
effectivePropertyResolutionContext.contextId,
paramWrapperWithInstance,
),
effectivePropertyResolutionContext.effectiveInquirerId,
);
return instanceHost.instance;
} catch (err) {
@@ -823,20 +847,15 @@ export class Injector {
instances: any[],
wrapper: InstanceWrapper,
targetMetatype: InstanceWrapper,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
): Promise<T> {
const { metatype, inject } = wrapper;
const inquirerId = this.getInquirerId(inquirer);
const inquirerId = this.getContextInquirerId(resolutionContext);
const instanceHost = targetMetatype.getInstanceByContextId(
this.getContextId(contextId, targetMetatype),
this.getContextId(resolutionContext.contextId, targetMetatype),
inquirerId,
);
const isInContext =
wrapper.isStatic(contextId, inquirer) ||
wrapper.isInRequestScope(contextId, inquirer) ||
wrapper.isLazyTransient(contextId, inquirer) ||
wrapper.isExplicitlyRequested(contextId, inquirer);
const isInContext = this.isInContext(wrapper, resolutionContext);
if (this.options?.preview && !wrapper.host?.initOnPreview) {
instanceHost.isResolved = true;
@@ -875,8 +894,15 @@ export class Injector {
if (!wrapper) {
const injectionToken = (instance as any).constructor!;
wrapper = collection.get(injectionToken);
} else {
wrapper = collection.get(wrapper.token) ?? wrapper;
}
await this.loadInstance(wrapper!, collection, moduleRef, ctx, wrapper);
await this.loadInstance(
wrapper!,
collection,
moduleRef,
this.createResolutionContext(ctx, wrapper),
);
await this.loadEnhancersPerContext(wrapper!, ctx, wrapper);
const host = wrapper!.getInstanceByContextId(
@@ -891,6 +917,9 @@ export class Injector {
ctx: ContextId,
inquirer?: InstanceWrapper,
) {
if (ctx === STATIC_CONTEXT) {
return;
}
const enhancers = wrapper.getEnhancersMetadata() || [];
const loadEnhancer = (item: InstanceWrapper) => {
const hostModule = item.host!;
@@ -898,8 +927,7 @@ export class Injector {
item,
hostModule.injectables,
hostModule,
ctx,
inquirer,
this.createResolutionContext(ctx, inquirer),
);
};
await Promise.all(enhancers.map(loadEnhancer));
@@ -923,16 +951,15 @@ export class Injector {
);
return hosts.map((item, index) => {
const dependency = metadata[index];
const effectiveInquirer = this.getEffectiveInquirer(
const effectiveInquirerId = this.getEffectiveInquirerId(
dependency,
inquirer,
this.createResolutionContext(contextId, inquirer),
parentInquirer,
contextId,
);
return item?.getInstanceByContextId(
this.getContextId(contextId, item),
this.getInquirerId(effectiveInquirer),
effectiveInquirerId,
).instance;
});
}
@@ -948,8 +975,7 @@ export class Injector {
host: await this.resolveComponentHost(
item.host!,
item,
contextId,
inquirer,
this.createResolutionContext(contextId, inquirer),
),
})),
);
@@ -970,6 +996,75 @@ export class Injector {
return inquirer ? inquirer.id : undefined;
}
private createResolutionContext(
contextId: ContextId,
inquirer?: InstanceWrapper,
effectiveInquirerId?: string,
): ResolutionContext {
return {
contextId,
inquirer,
effectiveInquirerId,
};
}
private getContextInquirerId({
inquirer,
effectiveInquirerId,
}: ResolutionContext): string | undefined {
return effectiveInquirerId ?? this.getInquirerId(inquirer);
}
private isInContext(
wrapper: InstanceWrapper,
resolutionContext: ResolutionContext,
) {
return (
wrapper.isStatic(
resolutionContext.contextId,
resolutionContext.inquirer,
) ||
wrapper.isInRequestScope(
resolutionContext.contextId,
resolutionContext.inquirer,
) ||
wrapper.isLazyTransient(
resolutionContext.contextId,
resolutionContext.inquirer,
) ||
wrapper.isExplicitlyRequested(
resolutionContext.contextId,
resolutionContext.inquirer,
)
);
}
private shouldSkipProviderLoading(
wrapper: InstanceWrapper<Injectable>,
resolutionContext: ResolutionContext,
): boolean {
const isSnapshotGraphCompilation = !!this.options?.snapshot;
const isStaticContext = resolutionContext.contextId === STATIC_CONTEXT;
const hasNoInquirer = !resolutionContext.inquirer;
const isTopLevelStaticTransientOrRequestProvider =
hasNoInquirer && (wrapper.isTransient || wrapper.scope === Scope.REQUEST);
const isStaticInquirerOutsideResolutionContext =
!!resolutionContext.inquirer &&
!this.isInContext(
resolutionContext.inquirer,
this.createResolutionContext(
resolutionContext.contextId,
resolutionContext.inquirer,
),
);
const shouldSkipForStaticBootstrap =
isStaticContext &&
(isTopLevelStaticTransientOrRequestProvider ||
isStaticInquirerOutsideResolutionContext);
return !isSnapshotGraphCompilation && shouldSkipForStaticBootstrap;
}
/**
* For nested TRANSIENT dependencies (TRANSIENT -> TRANSIENT) in non-static contexts,
* returns parentInquirer to ensure each parent TRANSIENT gets its own instance.
@@ -979,16 +1074,79 @@ export class Injector {
*/
private getEffectiveInquirer(
dependency: InstanceWrapper | undefined,
inquirer: InstanceWrapper | undefined,
resolutionContext: ResolutionContext,
parentInquirer: InstanceWrapper | undefined,
contextId: ContextId,
): InstanceWrapper | undefined {
return dependency?.isTransient &&
const { inquirer, contextId } = resolutionContext;
if (dependency?.isTransient && inquirer?.isTransient && parentInquirer) {
if (contextId === STATIC_CONTEXT) {
return inquirer.getRootInquirer() ?? parentInquirer;
}
return parentInquirer;
}
return inquirer;
}
private getEffectiveInquirerId(
dependency: InstanceWrapper | undefined,
resolutionContext: ResolutionContext,
parentInquirer: InstanceWrapper | undefined,
): string | undefined {
const { contextId, inquirer, effectiveInquirerId } = resolutionContext;
if (
contextId === STATIC_CONTEXT &&
dependency?.isTransient &&
inquirer?.isTransient &&
parentInquirer &&
contextId !== STATIC_CONTEXT
? parentInquirer
: inquirer;
parentInquirer
) {
const baseInquirerId =
effectiveInquirerId ?? this.getInquirerId(parentInquirer);
return `${baseInquirerId}:${inquirer.id}`;
}
const effectiveInquirer = this.getEffectiveInquirer(
dependency,
resolutionContext,
parentInquirer,
);
return this.getInquirerId(effectiveInquirer);
}
private getStaticTransientResolutionContext(
resolutionContext: ResolutionContext,
parentInquirer: InstanceWrapper | undefined,
): ResolutionContext {
const { contextId, inquirer, effectiveInquirerId } = resolutionContext;
if (
contextId === STATIC_CONTEXT &&
inquirer?.isTransient &&
parentInquirer
) {
const baseInquirerId =
effectiveInquirerId ?? this.getInquirerId(parentInquirer);
return this.createResolutionContext(
contextId,
inquirer,
`${baseInquirerId}:${inquirer.id}`,
);
}
return resolutionContext;
}
private getEffectiveResolutionContext(
dependency: InstanceWrapper | undefined,
resolutionContext: ResolutionContext,
parentInquirer: InstanceWrapper | undefined,
): ResolutionContext {
return this.createResolutionContext(
resolutionContext.contextId,
this.getEffectiveInquirer(dependency, resolutionContext, parentInquirer),
this.getEffectiveInquirerId(
dependency,
resolutionContext,
parentInquirer,
),
);
}
private resolveScopedComponentHost(
@@ -1002,8 +1160,11 @@ export class Injector {
: this.resolveComponentHost(
item.host!,
item,
contextId,
this.getEffectiveInquirer(item, inquirer, parentInquirer, contextId),
this.getEffectiveResolutionContext(
item,
this.createResolutionContext(contextId, inquirer),
parentInquirer,
),
);
}
+22 -6
View File
@@ -2,6 +2,7 @@ import { type IntrospectionResult, Scope, type Type } from '@nestjs/common';
import { getClassScope } from '../helpers/get-class-scope.js';
import { isDurable } from '../helpers/is-durable.js';
import { AbstractInstanceResolver } from './abstract-instance-resolver.js';
import { STATIC_CONTEXT } from './constants.js';
import { NestContainer } from './container.js';
import { Injector } from './injector.js';
import { InstanceLinksHost } from './instance-links-host.js';
@@ -36,10 +37,12 @@ export abstract class ModuleRef extends AbstractInstanceResolver {
constructor(protected readonly container: NestContainer) {
super();
const contextOptions = container.contextOptions;
this.injector = new Injector({
preview: container.contextOptions?.preview!,
instanceDecorator:
container.contextOptions?.instrument?.instanceDecorator,
preview: contextOptions?.preview ?? false,
snapshot: contextOptions?.snapshot,
instanceDecorator: contextOptions?.instrument?.instanceDecorator,
});
}
@@ -171,7 +174,14 @@ export abstract class ModuleRef extends AbstractInstanceResolver {
host: moduleRef,
});
/* eslint-disable-next-line no-async-promise-executor */
if (type?.prototype) {
wrapper.setInstanceByContextId(contextId ?? STATIC_CONTEXT, {
instance: Object.create(type.prototype),
isResolved: false,
isPending: false,
});
}
return new Promise<T>(async (resolve, reject) => {
try {
const callback = async (instances: any[]) => {
@@ -179,7 +189,10 @@ export abstract class ModuleRef extends AbstractInstanceResolver {
wrapper,
moduleRef,
undefined,
contextId,
{
contextId: contextId ?? STATIC_CONTEXT,
inquirer: wrapper,
},
);
const instance = new type(...instances);
this.injector.applyProperties(instance, properties);
@@ -190,7 +203,10 @@ export abstract class ModuleRef extends AbstractInstanceResolver {
moduleRef,
undefined,
callback,
contextId,
{
contextId: contextId ?? STATIC_CONTEXT,
inquirer: wrapper,
},
);
} catch (err) {
reject(err);
@@ -334,7 +334,12 @@ export class MiddlewareModule<
res: TResponse,
next: () => void,
) => {
if (applicationRef.getRequestMethod?.(req) === requestMethod) {
const actualRequestMethod = applicationRef.getRequestMethod?.(req);
if (
actualRequestMethod === requestMethod ||
(actualRequestMethod === RequestMethod[RequestMethod.HEAD] &&
requestMethod === RequestMethod[RequestMethod.GET])
) {
return proxy(req, res, next);
}
return next();
+7 -1
View File
@@ -36,7 +36,12 @@ import {
Logger,
} from '@nestjs/common';
type IEntryNestModule =
/**
* Represents the entry (root) module type accepted by the NestFactory methods.
*
* @publicApi
*/
export type IEntryNestModule =
| Type<any>
| DynamicModule
| ForwardReference
@@ -218,6 +223,7 @@ export class NestFactoryStatic {
const injector = new Injector({
preview: options.preview!,
snapshot: options.snapshot,
instanceDecorator: options.instrument?.instanceDecorator,
});
const instanceLoader = new InstanceLoader(
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@nestjs/core",
"version": "11.1.13",
"version": "11.1.20",
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -42,12 +42,12 @@
"@nuxt/opencollective": "0.4.1",
"fast-safe-stringify": "2.1.1",
"iterare": "1.2.1",
"path-to-regexp": "8.3.0",
"path-to-regexp": "8.4.2",
"tslib": "2.8.1",
"uid": "2.0.2"
},
"devDependencies": {
"@nestjs/common": "11.1.13"
"@nestjs/common": "11.1.20"
},
"peerDependencies": {
"@nestjs/common": "^11.0.0",
@@ -449,11 +449,17 @@ export class RouterExecutionContext {
res: TResponse,
req: TRequest,
) => {
const rawResponse = (res as { raw?: TResponse }).raw ?? res;
await this.responseController.sse(
result,
(res as any).raw || res,
rawResponse,
(req as any).raw || req,
{ additionalHeaders: res.getHeaders?.() as any },
{
additionalHeaders: res.getHeaders?.(),
statusCode:
(res as { statusCode?: number }).statusCode ??
(rawResponse as { statusCode?: number }).statusCode,
},
);
};
}
@@ -107,7 +107,10 @@ export class RouterResponseController {
result: TInput | Promise<TInput>,
response: TResponse,
request: TRequest,
options?: { additionalHeaders: AdditionalHeaders },
options?: {
additionalHeaders?: AdditionalHeaders;
statusCode?: number;
},
) {
// It's possible that we sent headers already so don't use a stream
if (response.writableEnded) {
@@ -120,52 +123,90 @@ export class RouterResponseController {
const stream = new SseStream(request);
// Extract custom status code from response if it was set
const customStatusCode = (response as any).statusCode;
const pipeOptions =
typeof customStatusCode !== 'undefined'
? { ...options, statusCode: customStatusCode }
: options;
const statusCode =
options?.statusCode ??
(response as { statusCode?: number }).statusCode ??
200;
stream.pipe(response, pipeOptions);
stream.pipe(response, {
additionalHeaders: options?.additionalHeaders,
statusCode,
});
const subscription = observableResult
.pipe(
map((message): MessageEvent => {
if (isObject(message)) {
return message as MessageEvent;
}
return new Promise<void>((resolve, reject) => {
let settled = false;
return { data: message as object | string };
}),
concatMap(
message =>
new Promise<void>(resolve =>
stream.writeMessage(message, () => resolve()),
),
),
catchError(err => {
const data = err instanceof Error ? err.message : err;
stream.writeMessage({ type: 'error', data }, writeError => {
if (writeError) {
this.logger.error(writeError);
const onClose = () => {
settled = true;
subscription.unsubscribe();
if (!stream.writableEnded) {
stream.end();
}
response.end();
resolve();
};
const subscription = observableResult
.pipe(
map((message): MessageEvent => {
if (isObject(message)) {
return message as MessageEvent;
}
});
return EMPTY;
}),
)
.subscribe({
complete: () => {
response.end();
},
});
return { data: message as object | string };
}),
concatMap(
message =>
new Promise<void>(resolve =>
stream.writeMessage(message, () => resolve()),
),
),
catchError(err => {
if (!stream.headersCommitted) {
throw err;
}
request.on('close', () => {
subscription.unsubscribe();
if (!stream.writableEnded) {
stream.end();
}
const data = err instanceof Error ? err.message : err;
stream.writeMessage({ type: 'error', data }, writeError => {
if (writeError) {
this.logger.error(writeError);
}
});
return EMPTY;
}),
)
.subscribe({
error: err => {
settled = true;
request.removeListener('close', onClose);
if (!stream.writableEnded) {
stream.end();
}
reject(err);
},
complete: () => {
settled = true;
request.removeListener('close', onClose);
if (!stream.writableEnded) {
stream.end();
}
resolve();
},
});
// Commit SSE headers on the next macrotask. Pipe validation errors
// propagate through microtasks (which complete before macrotasks),
// so if the lifecycle errored, `settled` is already true and we
// skip the write. Otherwise headers are sent immediately rather
// than waiting for the first Observable emission.
setTimeout(() => {
if (!settled) {
stream.commitHeaders();
}
}, 0);
request.on('close', onClose);
});
}
+47 -12
View File
@@ -52,6 +52,10 @@ export type HeaderStream = WritableHeaderStream & ReadHeaders;
*/
export class SseStream extends Transform {
private lastEventId: number | null = null;
private _headersCommitted = false;
private _destination: WritableHeaderStream | null = null;
private _statusCode = 200;
private _additionalHeaders: AdditionalHeaders | undefined;
constructor(req?: IncomingMessage) {
super({ objectMode: true });
@@ -62,6 +66,10 @@ export class SseStream extends Transform {
}
}
get headersCommitted(): boolean {
return this._headersCommitted;
}
pipe<T extends WritableHeaderStream>(
destination: T,
options?: {
@@ -70,10 +78,31 @@ export class SseStream extends Transform {
end?: boolean;
},
): T {
if (destination.writeHead) {
const statusCode = options?.statusCode ?? 200;
destination.writeHead(statusCode, {
...options?.additionalHeaders,
this._destination = destination;
this._statusCode = options?.statusCode ?? 200;
this._additionalHeaders = options?.additionalHeaders;
return super.pipe(destination, options);
}
/**
* Writes SSE headers to the destination if they have not been sent yet.
* Headers are deferred until the first message so that, if the observable
* errors before any data is emitted, the HTTP status code can still be
* changed by an exception filter.
*/
commitHeaders(): void {
if (this._headersCommitted || !this._destination) {
return;
}
if (this._destination.writableEnded) {
return;
}
this._headersCommitted = true;
const statusCode = this._statusCode ?? 200;
const additionalHeaders = this._additionalHeaders;
if (this._destination.writeHead) {
this._destination.writeHead(statusCode, {
...additionalHeaders,
// See https://github.com/dunglas/mercure/blob/master/hub/subscribe.go#L124-L130
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
@@ -85,11 +114,9 @@ export class SseStream extends Transform {
// NGINX support https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/#x-accel-buffering
'X-Accel-Buffering': 'no',
});
destination.flushHeaders?.();
this._destination.flushHeaders?.();
}
destination.write('\n');
return super.pipe(destination, options);
this._destination.write('\n');
}
_transform(
@@ -97,9 +124,17 @@ export class SseStream extends Transform {
encoding: string,
callback: (error?: Error | null, data?: any) => void,
) {
let data = message.type ? `event: ${message.type}\n` : '';
data += message.id ? `id: ${message.id}\n` : '';
data += message.retry ? `retry: ${message.retry}\n` : '';
this.commitHeaders();
const sanitize = (val: string | number) =>
String(val).replace(/[\r\n]/g, '');
let data = message.type ? `event: ${sanitize(message.type)}\n` : '';
data +=
message.id !== undefined && message.id !== null
? `id: ${sanitize(message.id)}\n`
: '';
data += message.retry ? `retry: ${sanitize(message.retry)}\n` : '';
data += message.data ? toDataString(message.data) : '';
data += '\n';
this.push(data);
@@ -113,7 +148,7 @@ export class SseStream extends Transform {
message: MessageEvent,
cb: (error: Error | null | undefined) => void,
) {
if (!message.id) {
if (message.id === undefined || message.id === null) {
this.lastEventId!++;
message.id = this.lastEventId!.toString();
}
+86 -10
View File
@@ -15,7 +15,7 @@ describe('Error Messages', () => {
const index = 0;
it('should display class', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the CatService (?, CatService). Please make sure that the argument dependency at index [0] is available in the current context.
stringCleaner(`Nest can't resolve dependencies of the CatService (?, CatService). Please make sure that the argument at index [0] is available in the current module.
Potential solutions:
- If dependency is a provider, is it part of the current Module?
@@ -40,7 +40,7 @@ describe('Error Messages', () => {
});
it('should display the provide token', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the CatService (?, MY_TOKEN). Please make sure that the argument dependency at index [0] is available in the current context.
stringCleaner(`Nest can't resolve dependencies of the CatService (?, MY_TOKEN). Please make sure that the argument at index [0] is available in the current module.
Potential solutions:
- If dependency is a provider, is it part of the current Module?
@@ -63,7 +63,7 @@ describe('Error Messages', () => {
});
it('should display the provide token as double-quoted string for string-based tokens', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the CatService (?). Please make sure that the argument "FooRepository" at index [0] is available in the current context.
stringCleaner(`Nest can't resolve dependencies of the CatService (?). Please make sure that the argument "FooRepository" at index [0] is available in the current module.
Potential solutions:
- If "FooRepository" is a provider, is it part of the current Module?
@@ -87,7 +87,7 @@ describe('Error Messages', () => {
});
it('should display the function name', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the CatService (?, CatFunction). Please make sure that the argument dependency at index [0] is available in the current context.
stringCleaner(`Nest can't resolve dependencies of the CatService (?, CatFunction). Please make sure that the argument at index [0] is available in the current module.
Potential solutions:
- If dependency is a provider, is it part of the current Module?
@@ -110,7 +110,7 @@ describe('Error Messages', () => {
});
it('should use "+" if unknown dependency name', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the CatService (?, +). Please make sure that the argument dependency at index [0] is available in the current context.
stringCleaner(`Nest can't resolve dependencies of the CatService (?, +). Please make sure that the argument at index [0] is available in the current module.
Potential solutions:
- If dependency is a provider, is it part of the current Module?
@@ -133,7 +133,7 @@ describe('Error Messages', () => {
});
it('should display the module name', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the CatService (?, MY_TOKEN). Please make sure that the argument dependency at index [0] is available in the TestModule context.
stringCleaner(`Nest can't resolve dependencies of the CatService (?, MY_TOKEN). Please make sure that the argument at index [0] is available in the TestModule module.
Potential solutions:
- Is TestModule a valid NestJS module?
@@ -169,7 +169,7 @@ describe('Error Messages', () => {
});
it('should display the symbol name of the provider', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the Symbol(CatProvider) (?). Please make sure that the argument dependency at index [0] is available in the current context.
stringCleaner(`Nest can't resolve dependencies of the Symbol(CatProvider) (?). Please make sure that the argument at index [0] is available in the current module.
Potential solutions:
- If dependency is a provider, is it part of the current Module?
@@ -192,7 +192,7 @@ describe('Error Messages', () => {
});
it('should display the symbol dependency of the provider', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the CatProvider (?, Symbol(DogProvider)). Please make sure that the argument dependency at index [0] is available in the current context.
stringCleaner(`Nest can't resolve dependencies of the CatProvider (?, Symbol(DogProvider)). Please make sure that the argument at index [0] is available in the current module.
Potential solutions:
- If dependency is a provider, is it part of the current Module?
@@ -215,7 +215,7 @@ describe('Error Messages', () => {
});
it('should detect likely import type issue and provide specific guidance', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the ResourceController (ResourceService, ?). Please make sure that the argument dependency at index [1] is available in the current context.
stringCleaner(`Nest can't resolve dependencies of the ResourceController (ResourceService, ?). Please make sure that the argument at index [1] is available in the current module.
Potential solutions:
- The dependency at index [1] appears to be undefined at runtime
@@ -241,7 +241,7 @@ describe('Error Messages', () => {
});
it('should detect import type issue with mixed dependencies', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the ResourceController (ValidService, ?, AnotherService). Please make sure that the argument dependency at index [1] is available in the current context.
stringCleaner(`Nest can't resolve dependencies of the ResourceController (ValidService, ?, AnotherService). Please make sure that the argument at index [1] is available in the current module.
Potential solutions:
- The dependency at index [1] appears to be undefined at runtime
@@ -268,6 +268,82 @@ describe('Error Messages', () => {
expect(actualMessage).toBe(expectedResult);
});
it('should display class token name in argument label when name is provided', () => {
class UserRepository {}
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the UserService (?). Please make sure that the argument UserRepository at index [0] is available in the current module.
Potential solutions:
- If UserRepository is a provider, is it part of the current Module?
- If UserRepository is exported from a separate @Module, is that module imported within Module?
@Module({
imports: [ /* the Module containing UserRepository */ ]
})
For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors
`);
const actualMessage = stringCleaner(
new UnknownDependenciesException('UserService', {
index: 0,
dependencies: [UserRepository],
name: UserRepository,
}).message,
);
expect(actualMessage).to.equal(expectedResult);
});
it('should display string token name in argument label when name is provided', () => {
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the UserService (?). Please make sure that the argument "DATABASE_URL" at index [0] is available in the current module.
Potential solutions:
- If "DATABASE_URL" is a provider, is it part of the current Module?
- If "DATABASE_URL" is exported from a separate @Module, is that module imported within Module?
@Module({
imports: [ /* the Module containing "DATABASE_URL" */ ]
})
For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors
`);
const actualMessage = stringCleaner(
new UnknownDependenciesException('UserService', {
index: 0,
dependencies: ['DATABASE_URL'],
name: 'DATABASE_URL',
}).message,
);
expect(actualMessage).to.equal(expectedResult);
});
it('should display symbol token name in argument label when name is provided', () => {
const TOKEN = Symbol('MY_TOKEN');
const expectedResult =
stringCleaner(`Nest can't resolve dependencies of the UserService (?). Please make sure that the argument Symbol(MY_TOKEN) at index [0] is available in the current module.
Potential solutions:
- If Symbol(MY_TOKEN) is a provider, is it part of the current Module?
- If Symbol(MY_TOKEN) is exported from a separate @Module, is that module imported within Module?
@Module({
imports: [ /* the Module containing Symbol(MY_TOKEN) */ ]
})
For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors
`);
const actualMessage = stringCleaner(
new UnknownDependenciesException('UserService', {
index: 0,
dependencies: [TOKEN],
name: TOKEN,
}).message,
);
expect(actualMessage).to.equal(expectedResult);
});
it('should add documentation links to export errors', () => {
const expectedResult =
stringCleaner(`Nest cannot export a provider/module that is not a part of the currently processed module (TestModule). Please verify whether the exported TestService is available in this particular context.
+25 -10
View File
@@ -261,8 +261,10 @@ describe('Injector', () => {
wrapper,
module.controllers,
module,
expect.anything(),
wrapper,
expect.objectContaining({
contextId: STATIC_CONTEXT,
inquirer: wrapper,
}),
);
});
});
@@ -284,8 +286,10 @@ describe('Injector', () => {
wrapper,
module.injectables,
module,
expect.anything(),
undefined,
expect.objectContaining({
contextId: STATIC_CONTEXT,
inquirer: undefined,
}),
);
});
});
@@ -570,7 +574,9 @@ describe('Injector', () => {
Promise.reject(error),
);
await injector.resolveComponentHost(module, wrapper, contextId);
await injector.resolveComponentHost(module, wrapper, {
contextId,
});
await new Promise(resolve => setImmediate(resolve));
expect(errorSpy).toHaveBeenCalledOnce();
@@ -609,7 +615,9 @@ describe('Injector', () => {
describe('when context is static', () => {
it('should instantiate class', async () => {
const wrapper = new InstanceWrapper({ metatype: TestClass });
await injector.instantiateClass([], wrapper, wrapper, STATIC_CONTEXT);
await injector.instantiateClass([], wrapper, wrapper, {
contextId: STATIC_CONTEXT,
});
expect(wrapper.instance).not.toBeUndefined();
expect(wrapper.instance).toBeInstanceOf(TestClass);
@@ -619,7 +627,9 @@ describe('Injector', () => {
inject: [],
metatype: (() => ({})) as any,
});
await injector.instantiateClass([], wrapper, wrapper, STATIC_CONTEXT);
await injector.instantiateClass([], wrapper, wrapper, {
contextId: STATIC_CONTEXT,
});
expect(wrapper.instance).not.toBeUndefined();
});
@@ -628,7 +638,9 @@ describe('Injector', () => {
it('should not instantiate class', async () => {
const ctx = { id: 3 };
const wrapper = new InstanceWrapper({ metatype: TestClass });
await injector.instantiateClass([], wrapper, wrapper, ctx);
await injector.instantiateClass([], wrapper, wrapper, {
contextId: ctx,
});
expect(wrapper.instance).toBeUndefined();
expect(wrapper.getInstanceByContextId(ctx).isResolved).toBe(true);
@@ -639,7 +651,9 @@ describe('Injector', () => {
inject: [],
metatype: vi.fn() as any,
});
await injector.instantiateClass([], wrapper, wrapper, { id: 2 });
await injector.instantiateClass([], wrapper, wrapper, {
contextId: { id: 2 },
});
expect(wrapper.instance).toBeUndefined();
expect(wrapper.metatype as any).not.toHaveBeenCalled();
});
@@ -672,6 +686,7 @@ describe('Injector', () => {
describe('loadEnhancersPerContext', () => {
it('should load enhancers per context id', async () => {
const contextId = { id: 1 };
const wrapper = new InstanceWrapper();
wrapper.addEnhancerMetadata(
new InstanceWrapper({
@@ -688,7 +703,7 @@ describe('Injector', () => {
.spyOn(injector, 'loadInstance')
.mockImplementation(async () => ({}) as any);
await injector.loadEnhancersPerContext(wrapper, STATIC_CONTEXT);
await injector.loadEnhancersPerContext(wrapper, contextId);
expect(loadInstanceStub).toHaveBeenCalledTimes(2);
});
});
@@ -97,18 +97,12 @@ describe('Nested Transient Isolation', () => {
it('should create separate TRANSIENT instances for each parent', async () => {
const contextId = { id: 1 };
await injector.loadInstance(
parent1Wrapper,
module.providers,
module,
await injector.loadInstance(parent1Wrapper, module.providers, module, {
contextId,
);
await injector.loadInstance(
parent2Wrapper,
module.providers,
module,
});
await injector.loadInstance(parent2Wrapper, module.providers, module, {
contextId,
);
});
const parent1Instance =
parent1Wrapper.getInstanceByContextId(contextId).instance;
@@ -124,18 +118,12 @@ describe('Nested Transient Isolation', () => {
it('should create separate nested TRANSIENT instances for each parent TRANSIENT', async () => {
const contextId = { id: 1 };
await injector.loadInstance(
parent1Wrapper,
module.providers,
module,
await injector.loadInstance(parent1Wrapper, module.providers, module, {
contextId,
);
await injector.loadInstance(
parent2Wrapper,
module.providers,
module,
});
await injector.loadInstance(parent2Wrapper, module.providers, module, {
contextId,
);
});
const parent1Instance =
parent1Wrapper.getInstanceByContextId(contextId).instance;
@@ -152,24 +140,15 @@ describe('Nested Transient Isolation', () => {
const contextId1 = { id: 1 };
const contextId2 = { id: 2 };
await injector.loadInstance(
parent1Wrapper,
module.providers,
module,
contextId1,
);
await injector.loadInstance(
parent2Wrapper,
module.providers,
module,
contextId1,
);
await injector.loadInstance(
parent1Wrapper,
module.providers,
module,
contextId2,
);
await injector.loadInstance(parent1Wrapper, module.providers, module, {
contextId: contextId1,
});
await injector.loadInstance(parent2Wrapper, module.providers, module, {
contextId: contextId1,
});
await injector.loadInstance(parent1Wrapper, module.providers, module, {
contextId: contextId2,
});
const ctx1Parent1 =
parent1Wrapper.getInstanceByContextId(contextId1).instance;
@@ -194,19 +173,27 @@ describe('Nested Transient Isolation', () => {
@Injectable({ scope: Scope.TRANSIENT })
class NestedTransientService {
public static constructorCalled = false;
public static instanceCount = 0;
public readonly instanceId: number;
public readonly value = 'nested-initialized';
constructor() {
NestedTransientService.constructorCalled = true;
NestedTransientService.instanceCount++;
this.instanceId = NestedTransientService.instanceCount;
}
}
@Injectable({ scope: Scope.TRANSIENT })
class TransientService {
public static constructorCalled = false;
public static instanceCount = 0;
public readonly instanceId: number;
constructor(public readonly nested: NestedTransientService) {
TransientService.constructorCalled = true;
TransientService.instanceCount++;
this.instanceId = TransientService.instanceCount;
}
}
@@ -215,13 +202,21 @@ describe('Nested Transient Isolation', () => {
constructor(public readonly transient: TransientService) {}
}
@Injectable()
class DefaultScopedParent2 {
constructor(public readonly transient: TransientService) {}
}
let nestedTransientWrapper: InstanceWrapper;
let transientWrapper: InstanceWrapper;
let parentWrapper: InstanceWrapper;
let parent2Wrapper: InstanceWrapper;
beforeEach(() => {
NestedTransientService.constructorCalled = false;
NestedTransientService.instanceCount = 0;
TransientService.constructorCalled = false;
TransientService.instanceCount = 0;
nestedTransientWrapper = new InstanceWrapper({
name: NestedTransientService.name,
@@ -247,9 +242,18 @@ describe('Nested Transient Isolation', () => {
host: module,
});
parent2Wrapper = new InstanceWrapper({
name: DefaultScopedParent2.name,
token: DefaultScopedParent2,
metatype: DefaultScopedParent2,
scope: Scope.DEFAULT,
host: module,
});
module.providers.set(NestedTransientService, nestedTransientWrapper);
module.providers.set(TransientService, transientWrapper);
module.providers.set(DefaultScopedParent, parentWrapper);
module.providers.set(DefaultScopedParent2, parent2Wrapper);
});
it('should instantiate nested TRANSIENT providers from DEFAULT scope', async () => {
@@ -265,5 +269,20 @@ describe('Nested Transient Isolation', () => {
);
expect(parentInstance.transient.nested.value).toBe('nested-initialized');
});
it('should isolate nested TRANSIENT instances across DEFAULT parents in STATIC context', async () => {
await injector.loadInstance(parentWrapper, module.providers, module);
await injector.loadInstance(parent2Wrapper, module.providers, module);
const parent1Instance = parentWrapper.instance;
const parent2Instance = parent2Wrapper.instance;
expect(parent1Instance.transient.instanceId).to.not.equal(
parent2Instance.transient.instanceId,
);
expect(parent1Instance.transient.nested.instanceId).to.not.equal(
parent2Instance.transient.nested.instanceId,
);
});
});
});
@@ -525,6 +525,45 @@ describe('RouterExecutionContext', () => {
}),
);
});
it('should pass through status and headers from the wrapper response at handle time', async () => {
const rawResponse = new PassThrough() as HeaderStream;
rawResponse.write = vi.fn() as any;
rawResponse.writeHead = vi.fn() as any;
rawResponse.flushHeaders = vi.fn() as any;
const response = {
raw: rawResponse,
statusCode: 203,
getHeaders: vi
.fn()
.mockReturnValue({ 'access-control-headers': 'at-handle-time' }),
};
const result = of('test');
const request = new PassThrough();
request.on = vi.fn() as any;
vi.spyOn(contextCreator, 'reflectRenderTemplate').mockReturnValue(
undefined!,
);
vi.spyOn(contextCreator, 'reflectSse').mockReturnValue('/');
const handler = contextCreator.createHandleResponseFn(
null!,
true,
undefined,
200,
) as HandlerResponseBasicFn;
await handler(result, response as any, request);
expect(rawResponse.writeHead).toHaveBeenCalledWith(
203,
expect.objectContaining({
'access-control-headers': 'at-handle-time',
}),
);
});
});
});
});
@@ -518,26 +518,31 @@ data: test
});
describe('when there is an error', () => {
it('should close the request', () =>
new Promise<void>(done => {
const result = new Subject();
const response = new Writable();
response.end = done as any;
response._write = () => {};
it('should reject when the stream errors before headers are committed', async () => {
const result = new Subject();
const response = new Writable();
response._write = () => {};
const request = new Writable();
request._write = () => {};
const request = new Writable();
request._write = () => {};
void routerResponseController.sse(
result,
response as unknown as ServerResponse,
request as unknown as IncomingMessage,
);
const promise = routerResponseController.sse(
result,
response as unknown as ServerResponse,
request as unknown as IncomingMessage,
);
result.error(new Error('Some error'));
}));
result.error(new Error('Some error'));
await expect(promise).rejects.toThrow('Some error');
});
it('should write the error message to the stream', async () => {
let resolveFirstChunk: (() => void) | undefined;
const firstChunkWritten = new Promise<void>(resolve => {
resolveFirstChunk = resolve;
});
class Sink extends Writable {
private readonly chunks: string[] = [];
@@ -547,6 +552,8 @@ data: test
callback: (error?: Error | null) => void,
): void {
this.chunks.push(chunk);
resolveFirstChunk?.();
resolveFirstChunk = undefined;
callback();
}
@@ -563,24 +570,21 @@ data: test
const result = new Subject();
const response = new Sink();
const request = new PassThrough();
void routerResponseController.sse(
const promise = routerResponseController.sse(
result,
response as unknown as ServerResponse,
request as unknown as IncomingMessage,
);
result.next('hello');
await firstChunkWritten;
result.error(new Error('Some error'));
request.destroy();
await promise;
await written(response);
expect(response.content).toEqual(
`
event: error
id: 1
data: Some error
`,
);
expect(response.content).toContain('event: error');
expect(response.content).toContain('data: Some error');
});
});
});
+5 -1
View File
@@ -142,6 +142,7 @@ data: hello
},
);
sse.pipe(sink);
sse.writeMessage({ data: 'hello' }, noop);
}));
it('sets additional headers when provided', () =>
@@ -159,6 +160,7 @@ data: hello
sse.pipe(sink, {
additionalHeaders: { 'access-control-headers': 'some-cors-value' },
});
sse.writeMessage({ data: 'hello' }, noop);
}));
it('sets custom status code when provided', () =>
@@ -175,6 +177,7 @@ data: hello
sse.pipe(sink, {
statusCode: 404,
});
sse.writeMessage({ data: 'hello' }, noop);
}));
it('defaults to 200 status code when not provided', () =>
@@ -189,6 +192,7 @@ data: hello
);
sse.pipe(sink);
sse.writeMessage({ data: 'hello' }, noop);
}));
it('allows an eventsource to connect', () =>
@@ -197,6 +201,7 @@ data: hello
const server = createServer((req, res) => {
sse = new SseStream(req);
sse.pipe(res);
process.nextTick(() => sse.writeMessage({ data: 'hello' }, noop));
});
server.listen(() => {
@@ -208,7 +213,6 @@ data: hello
es.close();
server.close(() => resolve());
};
es.onopen = () => sse.writeMessage({ data: 'hello' }, noop);
es.onerror = e =>
reject(new Error(`Error from EventSource: ${JSON.stringify(e)}`));
});
@@ -0,0 +1,82 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { INQUIRER, ModuleRef } from '@nestjs/core';
import { Test } from '@nestjs/testing';
describe('Transient scope', () => {
const SOME_TOKEN = Symbol('SomeToken');
@Injectable({ scope: Scope.TRANSIENT })
class TransientService {
public context: string;
constructor(
@Inject(INQUIRER) private inquirer: any,
@Inject(SOME_TOKEN) public token: string,
) {
this.context = inquirer.constructor.name;
}
}
@Injectable()
class RegularService {
constructor(
public transient: TransientService,
@Inject(SOME_TOKEN) public token: string,
) {}
}
@Injectable()
class DynamicService {
constructor(
public regular: RegularService,
public transient: TransientService,
) {}
}
describe('creating an instance with moduleRef.create', () => {
let service: DynamicService;
let moduleRef: ModuleRef;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
RegularService,
TransientService,
DynamicService,
{ provide: SOME_TOKEN, useValue: 'some-value' },
],
}).compile();
moduleRef = module.get(ModuleRef);
service = await moduleRef.create(DynamicService);
});
it('should be able to inject a regular dependency', async () => {
expect(service.regular.token).toBe('some-value');
expect(service.regular.transient.context).toBe(RegularService.name);
});
it('should be able to inject a transient-scoped dependency', async () => {
expect(service.transient.token).toBe('some-value');
expect(service.transient.context).toBe(DynamicService.name);
});
it('should work correctly when there is another class that injects the same dependency', async () => {
@Injectable()
class AnotherDynamicService {
constructor(
public regular: RegularService,
public transient: TransientService,
) {}
}
const service2 = await moduleRef.create(AnotherDynamicService);
expect(service2.regular.token).toBe('some-value');
expect(service2.regular.transient.context).toBe(RegularService.name);
expect(service2.transient.token).toBe('some-value');
expect(service2.transient.context).toBe(AnotherDynamicService.name);
});
});
});
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
@@ -306,7 +306,7 @@ export class ClientKafka
this._consumer.on(this._consumer.events.STOP, () =>
this._status$.next(KafkaStatus.STOPPED),
);
this.consumer.on(this._consumer.events.CRASH, () =>
this._consumer.on(this._consumer.events.CRASH, () =>
this._status$.next(KafkaStatus.CRASHED),
);
}
+29 -3
View File
@@ -16,6 +16,9 @@ import { loadPackage } from '@nestjs/common/internal';
// type Redis = import('ioredis').Redis;
type Redis = any;
type RedisOutputOptions = {
returnBuffers?: boolean;
};
/**
* @publicApi
*/
@@ -32,7 +35,10 @@ export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
callback: RedisEvents[keyof RedisEvents];
}> = [];
constructor(protected readonly options: Required<RedisOptions>['options']) {
constructor(
protected readonly options: Required<RedisOptions>['options'] &
RedisOutputOptions,
) {
super();
this.initializeSerializer(options);
@@ -84,6 +90,7 @@ export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
}
public async createClient(): Promise<Redis> {
const clientInfoTag = this.getOptionsProp(this.options, 'clientInfoTag');
const redisPackage = await loadPackage(
'ioredis',
ClientRedis.name,
@@ -94,6 +101,7 @@ export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
host: REDIS_DEFAULT_HOST,
port: REDIS_DEFAULT_PORT,
...this.getClientOptions(),
...(clientInfoTag && { clientInfoTag }),
lazyConnect: true,
});
}
@@ -138,7 +146,10 @@ export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
if (!this.wasInitialConnectionSuccessful) {
this.wasInitialConnectionSuccessful = true;
this.subClient.on('message', this.createResponseCallback());
this.subClient.on(
this.options.returnBuffers ? 'messageBuffer' : 'message',
this.createResponseCallback(),
);
}
});
}
@@ -222,12 +233,27 @@ export class ClientRedis extends ClientProxy<RedisEvents, RedisStatus> {
buffer: string,
) => Promise<void> {
return async (channel: string, buffer: string) => {
const packet = JSON.parse(buffer);
let packet: any;
try {
packet = JSON.parse(buffer);
} catch (err) {
this.logger.debug(
'Redis response packet is not in json format, bypassing...',
);
packet = buffer;
}
const { err, response, isDisposed, id } =
await this.deserializer.deserialize(packet);
const callback = this.routingMap.get(id);
if (!callback) {
if (Buffer.isBuffer(buffer))
this.logger.debug(
'You have to parse your buffer on your own to get id from it, because it is not in json format',
);
this.logger.debug(
'No matching callback found for Redis response packet with id: ' + id,
);
return;
}
if (isDisposed || err) {
+22 -1
View File
@@ -11,6 +11,7 @@ import {
} from 'rxjs';
import { first, map, retryWhen, scan, skip, switchMap } from 'rxjs/operators';
import {
BLOCKED_RMQ_MESSAGE,
DISCONNECTED_RMQ_MESSAGE,
RMQ_DEFAULT_IS_GLOBAL_PREFETCH_COUNT,
RMQ_DEFAULT_NO_ASSERT,
@@ -20,6 +21,7 @@ import {
RMQ_DEFAULT_QUEUE,
RMQ_DEFAULT_QUEUE_OPTIONS,
RMQ_DEFAULT_URL,
UNBLOCKED_RMQ_MESSAGE,
} from '../constants.js';
import { RmqEvents, RmqEventsMap, RmqStatus } from '../events/rmq.events.js';
import { ReadPacket, RmqOptions, WritePacket } from '../interfaces/index.js';
@@ -121,6 +123,8 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
this.registerErrorListener(this.client);
this.registerDisconnectListener(this.client);
this.registerConnectListener(this.client);
this.registerBlockedListener(this.client);
this.registerUnblockedListener(this.client);
this.pendingEventListeners.forEach(({ event, callback }) =>
this.client!.on(event, callback),
);
@@ -299,6 +303,23 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
});
}
public registerBlockedListener(client: AmqpConnectionManager): void {
client.addListener(
RmqEventsMap.BLOCKED,
({ reason }: { reason: string }) => {
this._status$.next(RmqStatus.BLOCKED);
this.logger.warn(BLOCKED_RMQ_MESSAGE(reason));
},
);
}
public registerUnblockedListener(client: AmqpConnectionManager): void {
client.addListener(RmqEventsMap.UNBLOCKED, () => {
this._status$.next(RmqStatus.UNBLOCKED);
this.logger.log(UNBLOCKED_RMQ_MESSAGE);
});
}
public on<
EventKey extends keyof RmqEvents = keyof RmqEvents,
EventCallback extends RmqEvents[EventKey] = RmqEvents[EventKey],
@@ -346,7 +367,7 @@ export class ClientRMQ extends ClientProxy<RmqEvents, RmqStatus> {
options,
);
if (isDisposed || err) {
callback?.({
return callback?.({
err,
response,
isDisposed: true,
+3
View File
@@ -58,5 +58,8 @@ export const NO_MESSAGE_HANDLER = `There is no matching message handler defined
export const DISCONNECTED_RMQ_MESSAGE = `Disconnected from RMQ. Trying to reconnect.`;
export const CONNECTION_FAILED_MESSAGE =
'Connection to transport failed. Trying to reconnect...';
export const BLOCKED_RMQ_MESSAGE = (reason: string) =>
`RMQ broker has blocked the connection (flow control). Reason: ${reason}`;
export const UNBLOCKED_RMQ_MESSAGE = 'RMQ broker has unblocked the connection.';
export const NATS_DEFAULT_GRACE_PERIOD = 10000;
@@ -1,15 +1,20 @@
type VoidCallback = () => void;
type OnErrorCallback = (error: Error) => void;
type OnBlockedCallback = (arg: { reason: string }) => void;
export const enum RmqStatus {
DISCONNECTED = 'disconnected',
CONNECTED = 'connected',
BLOCKED = 'blocked',
UNBLOCKED = 'unblocked',
}
export const enum RmqEventsMap {
ERROR = 'error',
DISCONNECT = 'disconnect',
CONNECT = 'connect',
BLOCKED = 'blocked',
UNBLOCKED = 'unblocked',
}
/**
@@ -21,4 +26,6 @@ export type RmqEvents = {
error: OnErrorCallback;
disconnect: VoidCallback;
connect: VoidCallback;
blocked: OnBlockedCallback;
unblocked: VoidCallback;
};
+7
View File
@@ -35,6 +35,13 @@ export interface IORedisOptions {
*/
connectionName?: string;
/**
* Set a tag to identify the framework using the Redis client library.
* This is appended to the library name (e.g., "ioredis(nestjs_v11.0.0)").
* @link https://redis.io/docs/latest/commands/client-setinfo/
*/
clientInfoTag?: string;
/**
* If set, client will send AUTH command with the value of this option as the first argument when connected.
* This is supported since Redis 6.
+34 -22
View File
@@ -33,19 +33,24 @@ export class JsonSocket extends TcpSocket {
: dataRaw;
this.buffer += data;
if (this.buffer.length > this.maxBufferSize) {
const bufferLength = this.buffer.length;
this.buffer = '';
throw new MaxPacketLengthExceededException(bufferLength);
}
// Iterative loop replaces recursion to prevent stack overflow on pipelined
// TCP messages (e.g. many small frames arriving in one read event).
while (true) {
if (this.buffer.length > this.maxBufferSize) {
const bufferLength = this.buffer.length;
this.buffer = '';
throw new MaxPacketLengthExceededException(bufferLength);
}
if (this.contentLength === null) {
const i = this.buffer.indexOf(this.delimiter);
/**
* Check if the buffer has the delimiter (#),
* if not, the end of the buffer string might be in the middle of a content length string
*/
if (i !== -1) {
if (this.contentLength === null) {
const i = this.buffer.indexOf(this.delimiter);
/**
* Check if the buffer has the delimiter (#),
* if not, the end of the buffer string might be in the middle of a content length string
*/
if (i === -1) {
break;
}
const rawContentLength = this.buffer.substring(0, i);
this.contentLength = parseInt(rawContentLength, 10);
@@ -56,17 +61,24 @@ export class JsonSocket extends TcpSocket {
}
this.buffer = this.buffer.substring(i + 1);
}
}
if (this.contentLength !== null) {
const length = this.buffer.length;
if (length === this.contentLength) {
this.handleMessage(this.buffer);
} else if (length > this.contentLength) {
const message = this.buffer.substring(0, this.contentLength);
const rest = this.buffer.substring(this.contentLength);
this.handleMessage(message);
this.handleData(rest);
if (this.contentLength !== null) {
const length = this.buffer.length;
if (length === this.contentLength) {
this.handleMessage(this.buffer);
// handleMessage resets contentLength and buffer; next iteration will break
} else if (length > this.contentLength) {
const message = this.buffer.substring(0, this.contentLength);
const rest = this.buffer.substring(this.contentLength);
this.handleMessage(message); // resets this.buffer to ''
this.buffer = rest; // restore remaining data for next iteration
continue;
} else {
// Incomplete message — wait for more data
break;
}
} else {
break;
}
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@nestjs/microservices",
"version": "11.1.13",
"version": "11.1.20",
"description": "Nest - modern, fast, powerful node.js web framework (@microservices)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -29,8 +29,8 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13"
"@nestjs/common": "11.1.20",
"@nestjs/core": "11.1.20"
},
"peerDependencies": {
"@grpc/grpc-js": "*",
@@ -11,7 +11,9 @@ export class NatsRecordSerializer implements Serializer<
const natsMessage =
packet?.data && isObject(packet.data) && packet.data instanceof NatsRecord
? packet.data
: new NatsRecordBuilder(packet?.data).build();
: new NatsRecordBuilder(packet?.data)
.setHeaders(packet?.headers)
.build();
return {
data: JSON.stringify({ ...packet, data: natsMessage.data }),
+1 -1
View File
@@ -467,7 +467,7 @@ export class ServerGrpc extends Server<never, never> {
.toLowerCase()
.indexOf('cancelled');
if (isCancelledError) {
if (isCancelledError !== -1) {
call.end();
return;
}
@@ -110,6 +110,7 @@ export class ServerRedis extends Server<RedisEvents, RedisStatus> {
}
public async createRedisClient(): Promise<Redis> {
const clientInfoTag = this.getOptionsProp(this.options, 'clientInfoTag');
const redisPackage = await this.loadPackage(
'ioredis',
ServerRedis.name,
@@ -120,6 +121,7 @@ export class ServerRedis extends Server<RedisEvents, RedisStatus> {
port: REDIS_DEFAULT_PORT,
host: REDIS_DEFAULT_HOST,
...this.getClientOptions(),
...(clientInfoTag && { clientInfoTag }),
lazyConnect: true,
});
}
+23 -1
View File
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
import { createRequire } from 'module';
import {
BLOCKED_RMQ_MESSAGE,
CONNECTION_FAILED_MESSAGE,
DISCONNECTED_RMQ_MESSAGE,
NO_MESSAGE_HANDLER,
@@ -16,6 +17,7 @@ import {
RMQ_DEFAULT_URL,
RMQ_NO_EVENT_HANDLER,
RMQ_NO_MESSAGE_HANDLER,
UNBLOCKED_RMQ_MESSAGE,
} from '../constants.js';
import { RmqContext } from '../ctx-host/index.js';
import { Transport } from '../enums/index.js';
@@ -128,6 +130,8 @@ export class ServerRMQ extends Server<RmqEvents, RmqStatus> {
this.registerConnectListener();
this.registerDisconnectListener();
this.registerBlockedListener();
this.registerUnblockedListener();
this.pendingEventListeners.forEach(({ event, callback }) =>
this.server!.on(event, callback),
);
@@ -186,6 +190,20 @@ export class ServerRMQ extends Server<RmqEvents, RmqStatus> {
});
}
private registerBlockedListener() {
this.server!.on(RmqEventsMap.BLOCKED, ({ reason }: { reason: string }) => {
this._status$.next(RmqStatus.BLOCKED);
this.logger.warn(BLOCKED_RMQ_MESSAGE(reason));
});
}
private registerUnblockedListener() {
this.server!.on(RmqEventsMap.UNBLOCKED, () => {
this._status$.next(RmqStatus.UNBLOCKED);
this.logger.log(UNBLOCKED_RMQ_MESSAGE);
});
}
public async setupChannel(channel: Channel, callback: Function) {
const noAssert =
this.getOptionsProp(this.options, 'noAssert') ??
@@ -416,13 +434,17 @@ export class ServerRMQ extends Server<RmqEvents, RmqStatus> {
}
}
private initializeWildcardHandlersIfExist() {
protected initializeWildcardHandlersIfExist() {
if (this.wildcardHandlers.size !== 0) {
return;
}
const handlers = this.getHandlers();
handlers.forEach((handler, pattern) => {
if (typeof pattern !== 'string') {
return;
}
if (
pattern.includes(RMQ_WILDCARD_ALL) ||
pattern.includes(RMQ_WILDCARD_SINGLE)
@@ -291,6 +291,47 @@ describe('ClientKafka', () => {
});
});
describe('registerConsumerEventListeners', () => {
it('should use the _consumer backing field, not the consumer getter', () => {
const consumerOnSpy = vi.fn();
const fakeConsumer = {
events: {
CONNECT: 'consumer.connect',
DISCONNECT: 'consumer.disconnect',
REBALANCING: 'consumer.rebalancing',
STOP: 'consumer.stop',
CRASH: 'consumer.crash',
},
on: consumerOnSpy,
};
// Set the backing field directly
untypedClient._consumer = fakeConsumer;
// Replace the consumer getter stub with one that throws,
// ensuring the method only uses _consumer (backing field)
consumerStub.mockImplementation(() => {
throw new Error(
'Consumer getter should not be called in registerConsumerEventListeners',
);
});
expect(() =>
untypedClient.registerConsumerEventListeners(),
).to.not.throw();
expect(consumerOnSpy).toHaveBeenCalledTimes(5);
const registeredEvents = consumerOnSpy.mock.calls.map(
(args: any[]) => args[0],
);
expect(registeredEvents).toContain('consumer.connect');
expect(registeredEvents).toContain('consumer.disconnect');
expect(registeredEvents).toContain('consumer.rebalancing');
expect(registeredEvents).toContain('consumer.stop');
expect(registeredEvents).toContain('consumer.crash');
});
});
describe('connect', () => {
let consumerAssignmentsStub: any;
let bindTopicsStub: ReturnType<typeof vi.fn>;
@@ -176,6 +176,30 @@ describe('ClientRedis', () => {
expect(callback).not.toHaveBeenCalled();
});
});
describe('custom binary format (not json)', () => {
it('should use buffer directly without parsing it as json', async () => {
const clientWithBuffers = new ClientRedis({ returnBuffers: true });
const callback = vi.fn();
const str = `${responseMessage.id}|${responseMessage.response}`;
const bufferMessage = Buffer.from(str);
vi.spyOn(
Reflect.get(clientWithBuffers, 'deserializer'),
'deserialize',
).mockResolvedValue({
...responseMessage,
response: bufferMessage,
});
const subscription = clientWithBuffers.createResponseCallback();
clientWithBuffers['routingMap'].set(responseMessage.id, callback);
await subscription('channel', bufferMessage as any);
expect(callback).toHaveBeenCalledWith({
err: undefined,
response: bufferMessage,
});
});
});
});
describe('close', () => {
const untypedClient = client as any;
@@ -307,6 +331,62 @@ describe('ClientRedis', () => {
client.registerReadyListener(emitter as any);
expect(callback.mock.calls[0][0]).toEqual('ready');
});
it('should register "message" event when returnBuffers is not set', () => {
const onSpy = vi.fn();
const client = new ClientRedis({});
const untypedClient = client as any;
const emitter = {
on: onSpy,
};
untypedClient.wasInitialConnectionSuccessful = false;
untypedClient.subClient = emitter;
client.registerReadyListener(emitter as any);
const readyHandler = onSpy.mock.calls[0][1];
readyHandler();
expect(onSpy).toHaveBeenCalledTimes(2);
expect(onSpy.mock.calls[1][0]).toEqual('message');
});
it('should register "message" event when returnBuffers is false', () => {
const onSpy = vi.fn();
const client = new ClientRedis({ returnBuffers: false });
const untypedClient = client as any;
const emitter = {
on: onSpy,
};
untypedClient.wasInitialConnectionSuccessful = false;
untypedClient.subClient = emitter;
client.registerReadyListener(emitter as any);
const readyHandler = onSpy.mock.calls[0][1];
readyHandler();
expect(onSpy).toHaveBeenCalledTimes(2);
expect(onSpy.mock.calls[1][0]).toEqual('message');
});
it('should register "messageBuffer" event when returnBuffers is true', () => {
const onSpy = vi.fn();
const clientWithBuffers = new ClientRedis({ returnBuffers: true });
const untypedClientWithBuffers = clientWithBuffers as any;
const emitter = {
on: onSpy,
};
untypedClientWithBuffers.wasInitialConnectionSuccessful = false;
untypedClientWithBuffers.subClient = emitter;
clientWithBuffers.registerReadyListener(emitter as any);
const readyHandler = onSpy.mock.calls[0][1];
readyHandler();
expect(onSpy).toHaveBeenCalledTimes(2);
expect(onSpy.mock.calls[1][0]).toEqual('messageBuffer');
});
});
describe('registerReconnectListener', () => {
it('should bind reconnect event handler', () => {
@@ -394,4 +474,24 @@ describe('ClientRedis', () => {
);
});
});
describe('createClient', () => {
it('should not set clientInfoTag when not provided', async () => {
const clientWithoutTag = new ClientRedis({});
const redisClient = await clientWithoutTag.createClient();
expect(redisClient).to.be.ok;
// Verify no clientInfoTag was set (opt-in only)
expect(redisClient.options.clientInfoTag).to.be.undefined;
});
it('should use clientInfoTag when provided', async () => {
const clientWithTag = new ClientRedis({ clientInfoTag: 'my-app' });
const redisClient = await clientWithTag.createClient();
expect(redisClient).to.be.ok;
// Verify the clientInfoTag was used
expect(redisClient.options.clientInfoTag).to.equal('my-app');
});
});
});
@@ -0,0 +1,119 @@
import { EventEmitter } from 'events';
import { CorruptedPacketLengthException } from '../../errors/corrupted-packet-length.exception.js';
import { MaxPacketLengthExceededException } from '../../errors/max-packet-length-exceeded.exception.js';
import { JsonSocket } from '../../helpers/json-socket.js';
function makeSocketStub(): any {
const emitter = new EventEmitter();
return Object.assign(emitter, {
write: vi.fn(),
end: vi.fn(),
connect: vi.fn(),
});
}
function frame(obj: unknown): string {
const json = JSON.stringify(obj);
return `${json.length}#${json}`;
}
describe('JsonSocket', () => {
let socketStub: any;
let jsonSocket: JsonSocket;
let received: unknown[];
beforeEach(() => {
socketStub = makeSocketStub();
jsonSocket = new JsonSocket(socketStub);
received = [];
socketStub.on('message', (msg: unknown) => received.push(msg));
});
describe('handleData – basic framing', () => {
it('emits a single message from one complete chunk', () => {
(jsonSocket as any).handleData(frame({ hello: 'world' }));
expect(received).to.have.lengthOf(1);
expect(received[0]).to.deep.equal({ hello: 'world' });
});
it('buffers a partial chunk and emits once complete', () => {
const full = frame({ x: 1 });
(jsonSocket as any).handleData(full.slice(0, 3));
expect(received).to.have.lengthOf(0);
(jsonSocket as any).handleData(full.slice(3));
expect(received).to.have.lengthOf(1);
expect(received[0]).to.deep.equal({ x: 1 });
});
it('emits multiple messages split across chunks', () => {
const payload = frame({ a: 1 }) + frame({ b: 2 });
const mid = Math.floor(payload.length / 2);
(jsonSocket as any).handleData(payload.slice(0, mid));
(jsonSocket as any).handleData(payload.slice(mid));
expect(received).to.have.lengthOf(2);
expect(received[0]).to.deep.equal({ a: 1 });
expect(received[1]).to.deep.equal({ b: 2 });
});
});
describe('handleData – pipelined messages (stack-overflow regression)', () => {
it('processes many small pipelined frames without a stack overflow', () => {
/**
* A payload of `"2#{}" * 12_000` (~47 KB) used to trigger
* "RangeError: Maximum call stack size exceeded" because each
* framed message caused a recursive call back into handleData().
*
* The iterative while-loop fix must process all frames in O(1) stack depth.
*/
const FRAME_COUNT = 12_000;
const singleFrame = frame({}); // "2#{}"
const bigPayload = singleFrame.repeat(FRAME_COUNT);
expect(() => {
(jsonSocket as any).handleData(bigPayload);
}).not.toThrow();
expect(received).toHaveLength(FRAME_COUNT);
});
it('correctly identifies all message payloads in a pipelined burst', () => {
const messages = Array.from({ length: 500 }, (_, i) => ({ id: i }));
const payload = messages.map(frame).join('');
(jsonSocket as any).handleData(payload);
expect(received).toHaveLength(messages.length);
messages.forEach((msg, i) => {
expect(received[i]).toEqual(msg);
});
});
});
describe('handleData – error cases', () => {
it('throws CorruptedPacketLengthException for a non-numeric length prefix', () => {
expect(() => {
(jsonSocket as any).handleData('abc#{}');
}).toThrow(CorruptedPacketLengthException);
});
it('throws MaxPacketLengthExceededException when buffer exceeds maxBufferSize', () => {
const tiny = new JsonSocket(socketStub, { maxBufferSize: 5 });
expect(() => {
(tiny as any).handleData('123456#');
}).toThrow(MaxPacketLengthExceededException);
});
it('resets state after a corrupted length so subsequent valid frames are processed', () => {
expect(() => (jsonSocket as any).handleData('bad#')).toThrow(
CorruptedPacketLengthException,
);
// After the error the socket state must be clean
(jsonSocket as any).handleData(frame({ ok: true }));
expect(received).toHaveLength(1);
expect(received[0]).toEqual({ ok: true });
});
});
});
@@ -4,6 +4,7 @@ import { NatsRecordSerializer } from '../../serializers/nats-record.serializer.j
describe('NatsRecordSerializer', () => {
let instance: NatsRecordSerializer;
beforeEach(() => {
instance = new NatsRecordSerializer();
});
@@ -58,6 +59,26 @@ describe('NatsRecordSerializer', () => {
});
});
it('applies packet headers when data is not a NatsRecord', () => {
const natsHeaders = nats.headers();
natsHeaders.set('x-response', 'enabled');
expect(
instance.serialize({
data: { value: 'string' },
headers: natsHeaders,
}),
).to.deep.eq({
headers: natsHeaders,
data: JSON.stringify({
data: {
value: 'string',
},
headers: natsHeaders,
}),
});
});
it('nats message with data and nats headers', () => {
const natsHeaders = nats.headers();
natsHeaders.set('1', 'header_1');
@@ -294,4 +294,24 @@ describe('ServerRedis', () => {
expect(handler).toHaveBeenCalledWith(data, expect.any(BaseRpcContext));
});
});
describe('createRedisClient', () => {
it('should not set clientInfoTag when not provided', async () => {
const serverWithoutTag = new ServerRedis({});
const redisClient = await serverWithoutTag.createRedisClient();
expect(redisClient).to.be.ok;
// Verify no clientInfoTag was set (opt-in only)
expect(redisClient.options.clientInfoTag).to.be.undefined;
});
it('should use clientInfoTag when provided', async () => {
const serverWithTag = new ServerRedis({ clientInfoTag: 'my-app' });
const redisClient = await serverWithTag.createRedisClient();
expect(redisClient).to.be.ok;
// Verify the clientInfoTag was used
expect(redisClient.options.clientInfoTag).to.equal('my-app');
});
});
});
@@ -58,7 +58,7 @@ describe('ServerRMQ', () => {
});
it('should bind "connectFailed" event to handler', async () => {
await server.listen(callbackSpy);
expect(onStub.mock.calls[3][0]).toBe('connectFailed');
expect(onStub.mock.calls.map(call => call[0])).toContain('connectFailed');
});
describe('when "start" throws an exception', () => {
it('should call callback with a thrown error as an argument', async () => {
@@ -119,4 +119,22 @@ describe('transformPatternToRoute', () => {
});
});
});
describe('when gets value exceeding max depth or max keys', () => {
it('should return special string indicating the limit was reached', () => {
const deepNestedPattern = {
a: { b: { c: { d: { e: { f: { g: 'too deep' } } } } } },
};
const tooManyKeysPattern = Object.fromEntries(
Array.from({ length: 25 }, (_, i) => [`key${i}`, `value${i}`]),
);
expect(transformPatternToRoute(deepNestedPattern)).to.be.equal(
'{"a":{"b":{"c":{"d":{"e":{"f":[MAX_DEPTH_REACHED]}}}}}}',
);
expect(transformPatternToRoute(tooManyKeysPattern)).to.be.equal(
'[TOO_MANY_KEYS]',
);
});
});
});
@@ -1,39 +1,64 @@
import { MsPattern } from '../interfaces/index.js';
import { isObject, isString, isNumber } from '@nestjs/common/internal';
const DEFAULT_MAX_DEPTH = 5;
const DEFAULT_MAX_KEYS = 20;
const escape = (s: string) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
/**
* Transforms the Pattern to Route.
* 1. If Pattern is a `string`, it will be returned as it is.
* 2. If Pattern is a `number`, it will be converted to `string`.
* 3. If Pattern is a `JSON` object, it will be transformed to Route. For that end,
* the function will sort properties of `JSON` Object and creates `route` string
* according to the following template:
* <key1>:<value1>/<key2>:<value2>/.../<keyN>:<valueN>
* Transforms the Pattern to Route safely.
*
* @param {MsPattern} pattern - client pattern
* @param pattern - client pattern
* @param depth - current recursion depth
* @param maxDepth - maximum allowed recursion depth
* @param maxKeys - maximum allowed keys per object
* @returns string
*/
export function transformPatternToRoute(pattern: MsPattern): string {
export function transformPatternToRoute(
pattern: MsPattern,
depth = 0,
maxDepth = DEFAULT_MAX_DEPTH,
maxKeys = DEFAULT_MAX_KEYS,
): string {
if (isString(pattern) || isNumber(pattern)) {
return `${pattern}`;
}
if (!isObject(pattern)) {
// For non-string, non-number, non-object values
return pattern;
}
const sortedKeys = Object.keys(pattern).sort((a, b) =>
('' + a).localeCompare(b),
);
if (depth > maxDepth) {
return '[MAX_DEPTH_REACHED]';
}
const keys = Object.keys(pattern);
if (keys.length > maxKeys) {
return '[TOO_MANY_KEYS]';
}
const sortedKeys = keys.sort((a, b) => ('' + a).localeCompare(b));
const parts = sortedKeys.map(key => {
const value = pattern[key];
let partialRoute = `"${escape(key)}":`;
// Only quote strings, numbers and objects are handled recursively
if (isString(value)) {
partialRoute += `"${escape(transformPatternToRoute(value, depth + 1, maxDepth, maxKeys))}"`;
} else {
partialRoute += transformPatternToRoute(
value,
depth + 1,
maxDepth,
maxKeys,
);
}
// Creates the array of Pattern params from sorted keys and their corresponding values
const sortedPatternParams = sortedKeys.map(key => {
let partialRoute = `"${key}":`;
partialRoute += isString(pattern[key])
? `"${transformPatternToRoute(pattern[key])}"`
: transformPatternToRoute(pattern[key]);
return partialRoute;
});
const route = sortedPatternParams.join(',');
return `{${route}}`;
return `{${parts.join(',')}}`;
}
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
@@ -111,25 +111,7 @@ export class ExpressAdapter extends AbstractHttpAdapter<
return response.send();
}
if (body instanceof StreamableFile) {
const streamHeaders = body.getHeaders();
if (
response.getHeader('Content-Type') === undefined &&
streamHeaders.type !== undefined
) {
response.setHeader('Content-Type', streamHeaders.type);
}
if (
response.getHeader('Content-Disposition') === undefined &&
streamHeaders.disposition !== undefined
) {
response.setHeader('Content-Disposition', streamHeaders.disposition);
}
if (
response.getHeader('Content-Length') === undefined &&
streamHeaders.length !== undefined
) {
response.setHeader('Content-Length', streamHeaders.length);
}
this.applyStreamHeaders(response, body);
const stream = body.getStream();
stream.once('error', err => {
body.errorHandler(err, response);
@@ -574,4 +556,27 @@ export class ExpressAdapter extends AbstractHttpAdapter<
)
);
}
private setHeaderIfNotExists(
response: any,
name: string,
value?: string | string[] | number,
) {
if (value !== undefined && response.getHeader(name) === undefined) {
const headerValue = Array.isArray(value) ? value.join(',') : value;
response.setHeader(name, headerValue);
}
}
private applyStreamHeaders(response: any, streamable: StreamableFile) {
const headers = streamable.getHeaders();
this.setHeaderIfNotExists(response, 'Content-Type', headers.type);
this.setHeaderIfNotExists(
response,
'Content-Disposition',
headers.disposition,
);
this.setHeaderIfNotExists(response, 'Content-Length', headers.length);
}
}
@@ -31,6 +31,9 @@ export interface MulterOptions {
/** Keep the full path of files instead of just the base name (Default: false) */
preservePath?: boolean;
/** Default character set for part header values (e.g. filename) (Default: 'latin1') */
defParamCharset?: string;
fileFilter?(
req: any,
file: {
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@nestjs/platform-express",
"version": "11.1.13",
"version": "11.1.20",
"description": "Nest - modern, fast, powerful node.js web framework (@platform-express)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -27,13 +27,13 @@
"dependencies": {
"cors": "2.8.6",
"express": "5.2.1",
"multer": "2.0.2",
"path-to-regexp": "8.3.0",
"multer": "2.1.1",
"path-to-regexp": "8.4.2",
"tslib": "2.8.1"
},
"devDependencies": {
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13"
"@nestjs/common": "11.1.20",
"@nestjs/core": "11.1.20"
},
"peerDependencies": {
"@nestjs/common": "^11.0.0",
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
@@ -623,7 +623,9 @@ export class FastifyAdapter<
public enableCors(options?: FastifyCorsOptions) {
this.register(
import('@fastify/cors') as Parameters<TInstance['register']>[0],
import('@fastify/cors') as unknown as Parameters<
TInstance['register']
>[0],
options,
);
}
@@ -720,7 +722,8 @@ export class FastifyAdapter<
queryParamsIndex >= 0
? req.originalUrl.slice(0, queryParamsIndex)
: req.originalUrl;
pathname = safeDecodeURI(pathname).path;
pathname = this.sanitizeUrl(pathname);
if (!re.exec(pathname + '/') && normalizedPath) {
return next();
@@ -825,7 +828,9 @@ export class FastifyAdapter<
private async registerMiddie() {
this.isMiddieRegistered = true;
await this.register(middie as Parameters<TInstance['register']>[0]);
await this.register(
middie as unknown as Parameters<TInstance['register']>[0],
);
}
private getRequestOriginalUrl(rawRequest: TRawRequest) {
@@ -898,4 +903,50 @@ export class FastifyAdapter<
}
return this.instance.route(routeToInject);
}
private sanitizeUrl(url: string): string {
const initialConfig = this.instance.initialConfig as FastifyServerOptions;
const routerOptions =
initialConfig.routerOptions as Partial<FastifyServerOptions>;
if (
routerOptions.ignoreDuplicateSlashes ||
initialConfig.ignoreDuplicateSlashes
) {
url = this.removeDuplicateSlashes(url);
}
if (
routerOptions.ignoreTrailingSlash ||
initialConfig.ignoreTrailingSlash
) {
url = this.trimLastSlash(url);
}
if (
routerOptions.caseSensitive === false ||
initialConfig.caseSensitive === false
) {
url = url.toLowerCase();
}
return safeDecodeURI(
url,
routerOptions.useSemicolonDelimiter ||
initialConfig.useSemicolonDelimiter,
).path;
}
private removeDuplicateSlashes(path: string) {
const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
return path.indexOf('//') !== -1
? path.replace(REMOVE_DUPLICATE_SLASHES_REGEXP, '/')
: path;
}
private trimLastSlash(path: string) {
if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
return path.slice(0, -1);
}
return path;
}
}
@@ -5,6 +5,7 @@ import {
FastifyPluginCallback,
FastifyReply,
FastifyRequest,
FastifyServerOptions,
HookHandlerDoneFunction,
} from 'fastify';
import fp from 'fastify-plugin';
@@ -29,6 +30,17 @@ interface MiddlewareEntry<
fn: MiddlewareFn<Req, Res, Ctx>;
}
function bindLast<F extends (...args: any[]) => any>(
fn: F,
last: Last<Parameters<F>>,
): (...args: DropLast<Parameters<F>>) => ReturnType<F> {
return (...args: any[]) => fn(...args, last);
}
// Helper types
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type DropLast<T extends any[]> = T extends [...infer Rest, any] ? Rest : never;
/**
* A clone of `@fastify/middie` engine https://github.com/fastify/middie
* with an extra vulnerability fix. Path is now decoded before matching to
@@ -38,13 +50,16 @@ function middie<
Req extends { url: string; originalUrl?: string },
Res extends { finished?: boolean; writableEnded?: boolean },
Ctx = unknown,
>(complete: (err: unknown, req: Req, res: Res, ctx: Ctx) => void) {
>(
complete: (err: unknown, req: Req, res: Res, ctx: Ctx) => void,
initialConfig: FastifyServerOptions | null,
) {
const middlewares: MiddlewareEntry<Req, Res, Ctx>[] = [];
const pool = reusify(Holder as any);
return {
use,
run,
run: bindLast(run, initialConfig),
};
function use(
@@ -80,7 +95,12 @@ function middie<
return this;
}
function run(req: Req, res: Res, ctx: Ctx) {
function run(
req: Req,
res: Res,
ctx: Ctx,
initialConfig: FastifyServerOptions | null,
) {
if (!middlewares.length) {
complete(null, req, res, ctx);
return;
@@ -93,6 +113,7 @@ function middie<
holder.res = res;
holder.url = sanitizeUrl(req.url);
holder.context = ctx;
holder.initialConfig = initialConfig;
holder.done();
}
@@ -101,6 +122,7 @@ function middie<
res: Res | null;
url: string | null;
context: Ctx | null;
initialConfig: FastifyServerOptions | null;
i: number;
done: (err?: unknown) => void;
}
@@ -110,6 +132,7 @@ function middie<
this.res = null;
this.url = null;
this.context = null;
this.initialConfig = null;
this.i = 0;
const that = this;
@@ -136,7 +159,33 @@ function middie<
if (regexp) {
// Decode URL before matching to avoid bypassing middleware
const decodedUrl = safeDecodeURI(url).path;
let sanitizedUrl = url;
if (
that.initialConfig!.ignoreDuplicateSlashes ||
that.initialConfig!.routerOptions?.ignoreDuplicateSlashes
) {
sanitizedUrl = removeDuplicateSlashes(sanitizedUrl);
}
if (
that.initialConfig!.ignoreTrailingSlash ||
that.initialConfig!.routerOptions?.ignoreTrailingSlash
) {
sanitizedUrl = trimLastSlash(sanitizedUrl);
}
if (
that.initialConfig!.caseSensitive === false ||
that.initialConfig!.routerOptions?.caseSensitive === false
) {
sanitizedUrl = sanitizedUrl.toLowerCase();
}
const decodedUrl = safeDecodeURI(
sanitizedUrl,
(that.initialConfig?.routerOptions as any)?.useSemicolonDelimiter ||
that.initialConfig?.useSemicolonDelimiter,
).path;
const result = regexp.exec(decodedUrl);
if (result) {
req.url = req.url.replace(result[0], '');
@@ -155,12 +204,27 @@ function middie<
that.req = null;
that.res = null;
that.context = null;
that.initialConfig = null;
that.i = 0;
pool.release(that as any);
}
}
}
function removeDuplicateSlashes(path: string) {
const REMOVE_DUPLICATE_SLASHES_REGEXP = /\/\/+/g;
return path.indexOf('//') !== -1
? path.replace(REMOVE_DUPLICATE_SLASHES_REGEXP, '/')
: path;
}
function trimLastSlash(path: string) {
if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) {
return path.slice(0, -1);
}
return path;
}
function sanitizeUrl(url: string): string {
for (let i = 0, len = url.length; i < len; i++) {
const charCode = url.charCodeAt(i);
@@ -215,7 +279,7 @@ function fastifyMiddie(
fastify.decorate('use', use as any);
fastify[kMiddlewares] = [];
fastify[kMiddieHasMiddlewares] = false;
fastify[kMiddie] = middie(onMiddieEnd);
fastify[kMiddie] = middie(onMiddieEnd, fastify.initialConfig);
const hook = options.hook || 'onRequest';
@@ -296,7 +360,7 @@ function fastifyMiddie(
function onRegister(instance: FastifyInstance) {
const middlewares = instance[kMiddlewares].slice() as Array<Array<unknown>>;
instance[kMiddlewares] = [];
instance[kMiddie] = middie(onMiddieEnd);
instance[kMiddie] = middie(onMiddieEnd, instance.initialConfig);
instance[kMiddieHasMiddlewares] = false;
instance.decorate('use', use as any);
for (const middleware of middlewares) {
@@ -359,9 +423,9 @@ declare module 'fastify' {
* with an extra vulnerability fix. Path is now decoded before matching to
* avoid bypassing middleware with encoded characters.
*/
export default fp(fastifyMiddie, {
export default (fp as any)(fastifyMiddie, {
fastify: '5.x',
name: '@fastify/middie',
});
}) as unknown as typeof fastifyMiddie;
export { fastifyMiddie };
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@nestjs/platform-fastify",
"version": "11.1.13",
"version": "11.1.20",
"description": "Nest - modern, fast, powerful node.js web framework (@platform-fastify)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -28,11 +28,11 @@
"@fastify/cors": "11.2.0",
"@fastify/formbody": "8.0.2",
"fast-querystring": "1.1.2",
"fastify": "5.7.4",
"fastify": "5.8.5",
"fastify-plugin": "5.1.0",
"find-my-way": "9.4.0",
"find-my-way": "9.6.0",
"light-my-request": "6.6.0",
"path-to-regexp": "8.3.0",
"path-to-regexp": "8.4.2",
"reusify": "1.1.0",
"tslib": "2.8.1"
},
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
@@ -12,6 +12,8 @@ import { DISCONNECT_EVENT } from '@nestjs/websockets/internal';
* @publicApi
*/
export class IoAdapter extends AbstractWsAdapter {
private readonly disconnectMap = new WeakMap<Socket, Observable<any>>();
public create(
port: number,
options?: ServerOptions & { namespace?: string; server?: any },
@@ -39,10 +41,11 @@ export class IoAdapter extends AbstractWsAdapter {
handlers: MessageMappingProperties[],
transform: (data: any) => Observable<any>,
) {
const disconnect$ = fromEvent(socket, DISCONNECT_EVENT).pipe(
share(),
first(),
);
let disconnect$ = this.disconnectMap.get(socket);
if (!disconnect$) {
disconnect$ = fromEvent(socket, DISCONNECT_EVENT).pipe(share(), first());
this.disconnectMap.set(socket, disconnect$);
}
handlers.forEach(({ message, callback, isAckHandledManually }) => {
const source$ = fromEvent(socket, message).pipe(
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nestjs/platform-socket.io",
"version": "11.1.13",
"version": "11.1.20",
"description": "Nest - modern, fast, powerful node.js web framework (@platform-socket.io)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -0,0 +1,48 @@
import { of } from 'rxjs';
import { IoAdapter } from '../adapters/io-adapter.js';
describe('IoAdapter', () => {
let adapter: IoAdapter;
beforeEach(() => {
adapter = new IoAdapter();
});
describe('bindMessageHandlers', () => {
it('should register only one disconnect listener regardless of call count', () => {
const addListenerSpy = vi.fn();
const fakeSocket = {
on: addListenerSpy,
off: vi.fn(),
addListener: addListenerSpy,
removeListener: vi.fn(),
} as any;
const handler = {
message: 'test-event',
methodName: 'handleTestEvent',
callback: vi.fn().mockReturnValue({ data: 'response' }),
isAckHandledManually: false,
};
const transform = (data: any) => of(data);
// Call bindMessageHandlers twice on the same socket
// (simulates two gateways sharing the same socket)
adapter.bindMessageHandlers(fakeSocket, [handler], transform);
adapter.bindMessageHandlers(fakeSocket, [handler], transform);
const disconnectCalls = addListenerSpy.mock.calls.filter(
call => call[0] === 'disconnect',
);
expect(disconnectCalls).toHaveLength(1);
// message handlers should still be registered per call
const messageCalls = addListenerSpy.mock.calls.filter(
call => call[0] === 'test-event',
);
expect(messageCalls).toHaveLength(2);
});
});
});
@@ -0,0 +1,3 @@
{
"extends": "../../../tsconfig.spec.json"
}
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@nestjs/platform-ws",
"version": "11.1.13",
"version": "11.1.20",
"description": "Nest - modern, fast, powerful node.js web framework (@platform-ws)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -26,7 +26,7 @@
},
"dependencies": {
"tslib": "2.8.1",
"ws": "8.19.0"
"ws": "8.20.1"
},
"peerDependencies": {
"@nestjs/common": "^11.0.0",
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nestjs/testing",
"version": "11.1.13",
"version": "11.1.20",
"description": "Nest - modern, fast, powerful node.js web framework (@testing)",
"author": "Kamil Mysliwiec",
"license": "MIT",
+11 -8
View File
@@ -1,4 +1,5 @@
import type { NestContainer } from '@nestjs/core';
import type { ContextId } from '@nestjs/core/injector/instance-wrapper.js';
import { MockFactory } from './interfaces/index.js';
import {
STATIC_CONTEXT,
@@ -8,6 +9,12 @@ import {
type Module,
} from '@nestjs/core/internal';
interface ResolutionContext {
contextId: ContextId;
inquirer?: InstanceWrapper;
effectiveInquirerId?: string;
}
/**
* @publicApi
*/
@@ -28,8 +35,7 @@ export class TestingInjector extends Injector {
name: any,
dependencyContext: InjectorDependencyContext,
wrapper: InstanceWrapper<T>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
keyOrIndex?: string | number,
): Promise<InstanceWrapper> {
try {
@@ -38,8 +44,7 @@ export class TestingInjector extends Injector {
name,
dependencyContext,
wrapper,
contextId,
inquirer,
resolutionContext,
keyOrIndex,
);
return existingProviderWrapper;
@@ -51,15 +56,13 @@ export class TestingInjector extends Injector {
public async resolveComponentHost<T>(
moduleRef: Module,
instanceWrapper: InstanceWrapper<T>,
contextId = STATIC_CONTEXT,
inquirer?: InstanceWrapper,
resolutionContext: ResolutionContext = { contextId: STATIC_CONTEXT },
): Promise<InstanceWrapper> {
try {
const existingProviderWrapper = await super.resolveComponentHost(
moduleRef,
instanceWrapper,
contextId,
inquirer,
resolutionContext,
);
return existingProviderWrapper;
} catch (err) {
+2 -1
View File
@@ -184,10 +184,11 @@ export class TestingModuleBuilder {
private async createInstancesOfDependencies(
graphInspector: GraphInspector,
options: { preview?: boolean },
options: { preview?: boolean; snapshot?: boolean },
) {
const injector = new TestingInjector({
preview: options?.preview ?? false,
snapshot: options?.snapshot ?? false,
});
const instanceLoader = new TestingInstanceLoader(
this.container,
+2 -1
View File
@@ -29,7 +29,7 @@ Nest is a framework for building efficient, scalable <a href="https://nodejs.org
## Philosophy
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front and backend applications, giving rise to awesome projects like <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a>, and <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>In recent years, thanks to Node.js, JavaScript has become the “lingua franca” of the web for both front-end and back-end applications, giving rise to awesome projects like <a href="https://angular.dev/" target="_blank">Angular</a>, <a href="https://react.dev/" target="_blank">React</a>, and <a href="https://vuejs.org/" target="_blank">Vue</a>, which improve developer productivity and enable the construction of fast, testable, and extensible frontend applications. However, on the server-side, while there are a lot of superb libraries, helpers, and tools for Node, none of them effectively solve the main problem - the architecture.</p>
<p>Nest aims to provide an application architecture out of the box which allows for effortless creation of highly testable, scalable, and loosely coupled and easily maintainable applications. The architecture is heavily inspired by Angular.</p>
## Getting started
@@ -95,6 +95,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
<td align="center" valign="middle"><a href="https://crawljobs.com" target="_blank"><img src="https://nestjs.com/img/logos/crawljobs-logo.svg" width="130" valign="middle" /></a></td>
</tr><tr>
<td align="center" valign="middle"><a href="https://pandektes.com" target="_blank"><img src="https://nestjs.com/img/logos/pandektes-logo.png" width="65" valign="middle" /></a></td>
<td align="center" valign="middle"><a href="https://www.fintechcrafts.com/" target="_blank"><img src="https://nestjs.com/img/logos/fintechcrafts-logo.svg" width="65" valign="middle" /></a></td>
</tr>
</table>
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@nestjs/websockets",
"version": "11.1.13",
"version": "11.1.20",
"description": "Nest - modern, fast, powerful node.js web framework (@websockets)",
"author": "Kamil Mysliwiec",
"license": "MIT",
@@ -26,8 +26,8 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13"
"@nestjs/common": "11.1.20",
"@nestjs/core": "11.1.20"
},
"peerDependencies": {
"@nestjs/common": "^11.0.0",
+1 -1
View File
@@ -32,7 +32,7 @@
## 哲学
<p>
近年、Node.jsの発展によって、JavaScriptはフロントエンドとバックエンドの両方でWebの「共通言語」になりました。そしてフロントエンドでは、<a href="https://angular.io/" target="_blank">Angular</a>、<a href="https://github.com/facebook/react" target="_blank">React</a>、<a href="https://github.com/vuejs/vue" target="_blank">Vue</a>などの素晴らしいプロジェクトが生まれ、開発効率が飛躍的に向上しました。一方で、サーバーサイドでは、Node.jsエコシステム上に優れたライブラリ、ヘルパー、ツールがたくさんありますが、それらのどれもが主要な問題であるアーキテクチャを効果的に解決するものではありませんでした。
近年、Node.jsの発展によって、JavaScriptはフロントエンドとバックエンドの両方でWebの「共通言語」になりました。そしてフロントエンドでは、<a href="https://angular.dev/" target="_blank">Angular</a>、<a href="https://react.dev/" target="_blank">React</a>、<a href="https://vuejs.org/" target="_blank">Vue</a>などの素晴らしいプロジェクトが生まれ、開発効率が飛躍的に向上しました。一方で、サーバーサイドでは、Node.jsエコシステム上に優れたライブラリ、ヘルパー、ツールがたくさんありますが、それらのどれもが主要な問題であるアーキテクチャを効果的に解決するものではありませんでした。
</p>
<p>Nestは、実用的なアーキテクチャをすぐに構築できることを目的としています。具体的には、テストフレンドリーで、スケーラブルで、疎結合で、運用にやさしいアプリケーションを構築できるようになっています。また、アーキテクチャはAngularにインスパイアされています。</p>
+1 -1
View File
@@ -30,7 +30,7 @@ Nest는 효율적이고 확장 가능한 <a href="https://nodejs.org" target="_b
## 철학
<p>최근 몇 년간 Javascript는 Node.js의 발전에 따라 웹의 프론트엔드와 백엔드에 있어서 "공용어"가 되었습니다. 그리고 프론트엔드에서는 <a href="https://angular.io/" target="_blank">Angular</a>와 <a href="https://github.com/facebook/react" target="_blank">React</a>, <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>와 같이 멋진 프로젝트들이 등장하여 개발자의 생산성을 향상시켰으며, 빠르고 테스트와 확장이 가능한 프론트엔드 애플리케이션을 만들 수 있게 해주었습니다. 하지만 서버 사이드에서는 Node.js를 위한 우수한 라이브러리와 툴이 많은 가운데, 주요 문제를 효과적으로 해결하는 것은 없었습니다. 그 주요 문제란 바로 아키텍처(architecture)입니다.</p>
<p>최근 몇 년간 Javascript는 Node.js의 발전에 따라 웹의 프론트엔드와 백엔드에 있어서 "공용어"가 되었습니다. 그리고 프론트엔드에서는 <a href="https://angular.dev/" target="_blank">Angular</a>와 <a href="https://react.dev/" target="_blank">React</a>, <a href="https://vuejs.org/" target="_blank">Vue</a>와 같이 멋진 프로젝트들이 등장하여 개발자의 생산성을 향상시켰으며, 빠르고 테스트와 확장이 가능한 프론트엔드 애플리케이션을 만들 수 있게 해주었습니다. 하지만 서버 사이드에서는 Node.js를 위한 우수한 라이브러리와 툴이 많은 가운데, 주요 문제를 효과적으로 해결하는 것은 없었습니다. 그 주요 문제란 바로 아키텍처(architecture)입니다.</p>
<p>Nest는 테스트 친화적이면서 확장이 가능하고 결합이 느슨하며, 유지 관리가 용이한 애플리케이션을 손쉽게 만들 수 있는 애플리케이션 아키텍처 제공을 목표로 합니다. 이 아키텍처는 Angular에서 많은 영감을 받았습니다.</p>
## 시작하기
+1 -1
View File
@@ -31,7 +31,7 @@
## 设计哲学
<p>近几年,由于 Node.js,JavaScript 已经成为 Web 前端和后端应用程序的「通用语言」,从而产生了像<a href="https://angular.cn" target="_blank">Angular</a>、<a href="https://github.com/facebook/react">React</a>、<a href="https://github.com/vuejs/vue" target="_blank">Vue</a>等令人耳目一新的项目,这些项目提高了开发人员的生产力,使得可以快速构建可测试的且可扩展的前端应用程序。 然而,在服务器端,虽然有很多优秀的库、helper 和 Node 工具,但是它们都没有有效地解决主要问题 - 架构。</p>
<p>近几年,由于 Node.js,JavaScript 已经成为 Web 前端和后端应用程序的「通用语言」,从而产生了像<a href="https://angular.cn" target="_blank">Angular</a>、<a href="https://react.dev/">React</a>、<a href="https://vuejs.org/" target="_blank">Vue</a>等令人耳目一新的项目,这些项目提高了开发人员的生产力,使得可以快速构建可测试的且可扩展的前端应用程序。 然而,在服务器端,虽然有很多优秀的库、helper 和 Node 工具,但是它们都没有有效地解决主要问题 - 架构。</p>
<p>Nest 旨在提供一个开箱即用的应用程序体系结构,允许轻松创建高度可测试,可扩展,松散耦合且易于维护的应用程序。</p>
@@ -7,7 +7,10 @@ export class RedisIoAdapter extends IoAdapter {
private adapterConstructor: ReturnType<typeof createAdapter>;
async connectToRedis(): Promise<void> {
const pubClient = createClient({ url: `redis://localhost:6379` });
const pubClient = createClient({
url: `redis://localhost:6379`,
clientInfoTag: this.getClientInfoTag(),
});
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
@@ -20,4 +23,15 @@ export class RedisIoAdapter extends IoAdapter {
server.adapter(this.adapterConstructor);
return server;
}
private getClientInfoTag(): string {
try {
// Try to get NestJS version from package.json
const nestVersion = require('@nestjs/common/package.json').version;
return `nestjs_v${nestVersion}`;
} catch {
// Fallback if version cannot be determined
return 'nestjs';
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ version: "3"
services:
mongodb:
image: mongo:8.2
image: mongo:8.3
environment:
- MONGODB_DATABASE="test"
ports:
+1 -1
View File
@@ -7,7 +7,7 @@
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
[linux-url]: https://travis-ci.org/nestjs/nest
<p align="center">A progressive <a href="https://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.io" target="blank">Angular</a>.</p>
<p align="center">A progressive <a href="https://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.dev/" target="blank">Angular</a>.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
+1 -1
View File
@@ -7,7 +7,7 @@
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
[linux-url]: https://travis-ci.org/nestjs/nest
<p align="center">A progressive <a href="https://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.io" target="blank">Angular</a>.</p>
<p align="center">A progressive <a href="https://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.dev/" target="blank">Angular</a>.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
+1 -1
View File
@@ -7,7 +7,7 @@
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
[linux-url]: https://travis-ci.org/nestjs/nest
<p align="center">A progressive <a href="https://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.io" target="blank">Angular</a>.</p>
<p align="center">A progressive <a href="https://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.dev/" target="blank">Angular</a>.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
+1 -1
View File
@@ -7,7 +7,7 @@
[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
[linux-url]: https://travis-ci.org/nestjs/nest
<p align="center">A progressive <a href="https://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.io" target="blank">Angular</a>.</p>
<p align="center">A progressive <a href="https://nodejs.org" target="blank">Node.js</a> framework for building efficient and scalable server-side applications, heavily inspired by <a href="https://angular.dev/" target="blank">Angular</a>.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
+53
View File
@@ -0,0 +1,53 @@
{
"name": "34-using-esm-packages",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"type": "module",
"engines": {
"node": ">=18.8"
},
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "oxlint src --fix",
"test": "vitest run",
"test:watch": "vitest",
"test:cov": "vitest run --coverage",
"test:debug": "vitest --inspect-brk --no-file-parallelism",
"test:e2e": "vitest run --config ./vitest.config.e2e.mts"
},
"dependencies": {
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13",
"@nestjs/platform-express": "11.1.13",
"delay": "7.x",
"reflect-metadata": "0.2.2",
"rimraf": "6.1.2",
"rxjs": "7.8.2",
"superjson": "2.x"
},
"devDependencies": {
"@nestjs/cli": "11.0.16",
"@nestjs/schematics": "11.0.9",
"@nestjs/testing": "11.1.13",
"@types/express": "5.0.6",
"@types/node": "24.10.11",
"@types/supertest": "6.0.3",
"oxlint": "1.58.0",
"prettier": "3.8.1",
"source-map-support": "0.5.21",
"supertest": "7.2.2",
"ts-loader": "9.5.4",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"typescript": "6.0.2",
"vitest": "4.1.2"
}
}
@@ -0,0 +1,52 @@
{
"name": "35-use-esm-package-after-node22",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"type": "module",
"engines": {
"node": ">=22"
},
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start --exec \"node --experimental-require-module\"",
"start:dev": "nest start --exec \"node --experimental-require-module\" --watch",
"start:debug": "nest start --exec \"node --experimental-require-module\" --debug --watch",
"start:prod": "node --experimental-require-module dist/main",
"lint": "oxlint src --fix",
"test": "vitest run",
"test:watch": "vitest",
"test:cov": "vitest run --coverage",
"test:debug": "vitest --inspect-brk --no-file-parallelism",
"test:e2e": "vitest run --config ./vitest.config.e2e.mts"
},
"dependencies": {
"@nestjs/common": "11.1.13",
"@nestjs/core": "11.1.13",
"@nestjs/platform-express": "11.1.13",
"reflect-metadata": "0.2.2",
"rimraf": "6.1.2",
"rxjs": "7.8.2",
"superjson": "2.x"
},
"devDependencies": {
"@nestjs/cli": "11.0.16",
"@nestjs/schematics": "11.0.9",
"@nestjs/testing": "11.1.13",
"@types/express": "5.0.6",
"@types/node": "24.10.11",
"@types/supertest": "6.0.3",
"oxlint": "1.58.0",
"prettier": "3.8.1",
"source-map-support": "0.5.21",
"supertest": "7.2.2",
"ts-loader": "9.5.4",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"typescript": "6.0.2",
"vitest": "4.1.2"
}
}
+59
View File
@@ -0,0 +1,59 @@
/**
* Cross-platform wrapper that forwards arguments to whichever Docker Compose
* implementation is available on the host:
*
* 1. `docker compose` (Compose V2 plugin, shipped with modern Docker Desktop)
* 2. `docker-compose` (legacy standalone V1 binary)
*
* This exists because contributors run a mix of both and neither is guaranteed
* to be installed. The wrapper prefers V2 when available and transparently
* falls back to V1, so npm scripts like `test:docker:up` work everywhere.
*/
import { spawnSync } from 'node:child_process';
const forwardedArgs = process.argv.slice(2);
const isWindows = process.platform === 'win32';
function hasComposeV2() {
const result = spawnSync('docker', ['compose', 'version'], {
stdio: 'ignore',
shell: isWindows,
});
return result.status === 0;
}
function hasComposeV1() {
const probe = isWindows ? 'where' : 'which';
const result = spawnSync(probe, ['docker-compose'], {
stdio: 'ignore',
shell: isWindows,
});
return result.status === 0;
}
function run(command, args) {
const result = spawnSync(command, args, {
stdio: 'inherit',
shell: isWindows,
});
if (result.error) {
console.error(
`[docker-compose] Failed to execute: ${result.error.message}`,
);
process.exit(1);
}
process.exit(result.status == null ? 1 : result.status);
}
if (hasComposeV2()) {
run('docker', ['compose', ...forwardedArgs]);
} else if (hasComposeV1()) {
run('docker-compose', forwardedArgs);
} else {
console.error(
"[docker-compose] Neither 'docker compose' (V2) nor 'docker-compose' (V1) was found on PATH.\n" +
'Install Docker Desktop or the Docker Compose plugin and try again.',
);
process.exit(1);
}
+4 -1
View File
@@ -2,4 +2,7 @@
npm run build &>/dev/null
# 2. Start docker containers to perform integration tests
npm run test:docker:up
npm run test:docker:up
# 3. Wait for RabbitMQ to accept AMQP connections
npm run test:docker:wait:rmq
+5 -2
View File
@@ -4,5 +4,8 @@ npm run build &>/dev/null
# 2. Start docker containers to perform integration tests
npm run test:docker:up
# 3. Run integration tests
npm run test:integration
# 3. Wait for RabbitMQ to accept AMQP connections
npm run test:docker:wait:rmq
# 4. Run integration tests
npm run test:integration
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff. Show more