mirror of
https://github.com/nestjs/nest.git
synced 2026-10-28 09:45:43 +08:00
Merge branch 'master' into v12.0.0
# Conflicts: # integration/injector/e2e/injector.spec.ts # integration/microservices/e2e/math-grpc.spec.ts # integration/scopes/e2e/request-modules-scope.spec.ts # integration/scopes/e2e/request-scope.spec.ts # lerna.json # package-lock.json # package.json # packages/core/injector/injector.ts # packages/core/test/errors/test/messages.spec.ts # packages/core/test/injector/injector.spec.ts # sample/01-cats-app/package.json # sample/02-gateways/package.json # sample/03-microservices/package.json # sample/04-grpc/package.json # sample/05-sql-typeorm/package.json # sample/06-mongoose/package.json # sample/07-sequelize/package.json # sample/08-webpack/package.json # sample/09-babel-example/package.json # sample/10-fastify/package.json # sample/11-swagger/package.json # sample/12-graphql-schema-first/package.json # sample/13-mongo-typeorm/package.json # sample/14-mongoose-base/package.json # sample/15-mvc/package.json # sample/16-gateways-ws/package.json # sample/17-mvc-fastify/package.json # sample/18-context/package.json # sample/19-auth-jwt/package.json # sample/20-cache/package.json # sample/21-serializer/package.json # sample/22-graphql-prisma/package.json # sample/23-graphql-code-first/package.json # sample/24-serve-static/package.json # sample/25-dynamic-modules/package.json # sample/26-queues/package.json # sample/27-scheduling/package.json # sample/28-sse/package.json # sample/29-file-upload/package.json # sample/30-event-emitter/package.json # sample/31-graphql-federation-code-first/gateway/package.json # sample/31-graphql-federation-code-first/posts-application/package.json # sample/31-graphql-federation-code-first/users-application/package.json # sample/32-graphql-federation-schema-first/gateway/package.json # sample/32-graphql-federation-schema-first/posts-application/package.json # sample/32-graphql-federation-schema-first/users-application/package.json # sample/33-graphql-mercurius/package.json # sample/36-hmr-esm/package.json
This commit is contained in:
44 files changed
+1710
-719
No files matched your search
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ services:
|
||||
zookeeper:
|
||||
container_name: test-zookeeper
|
||||
hostname: zookeeper
|
||||
image: confluentinc/cp-zookeeper:7.9.6
|
||||
image: confluentinc/cp-zookeeper:7.9.7
|
||||
ports:
|
||||
- "2181:2181"
|
||||
environment:
|
||||
@@ -60,7 +60,7 @@ services:
|
||||
kafka:
|
||||
container_name: test-kafka
|
||||
hostname: kafka
|
||||
image: confluentinc/cp-kafka:8.2.0
|
||||
image: confluentinc/cp-kafka:8.2.1
|
||||
depends_on:
|
||||
- zookeeper
|
||||
ports:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Global, Injectable, Module, Scope } from '@nestjs/common';
|
||||
import { RuntimeException } from '@nestjs/core/errors/exceptions/runtime.exception.js';
|
||||
import { UnknownDependenciesException } from '@nestjs/core/errors/exceptions/unknown-dependencies.exception.js';
|
||||
import { UnknownExportException } from '@nestjs/core/errors/exceptions/unknown-export.exception.js';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import {
|
||||
DYNAMIC_TOKEN,
|
||||
@@ -17,6 +19,65 @@ import {
|
||||
} from '../src/self-injection/self-injection-provider.module.js';
|
||||
|
||||
describe('Injector', () => {
|
||||
describe('when the same provider class is declared in multiple modules', () => {
|
||||
it('should bootstrap when module-local provider instances have non-static dependency trees', async () => {
|
||||
@Injectable()
|
||||
class FirstGlobalDependency {}
|
||||
|
||||
@Injectable()
|
||||
class SecondGlobalDependency {}
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
class RequestScopedDependency {}
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [FirstGlobalDependency, SecondGlobalDependency],
|
||||
exports: [FirstGlobalDependency, SecondGlobalDependency],
|
||||
})
|
||||
class GlobalDepsModule {}
|
||||
|
||||
@Injectable()
|
||||
class SharedService {
|
||||
constructor(
|
||||
public readonly firstDependency: FirstGlobalDependency,
|
||||
public readonly secondDependency: SecondGlobalDependency,
|
||||
public readonly requestScopedDependency: RequestScopedDependency,
|
||||
) {}
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [SharedService, RequestScopedDependency],
|
||||
exports: [SharedService],
|
||||
})
|
||||
class ModuleA {}
|
||||
|
||||
@Module({
|
||||
providers: [SharedService, RequestScopedDependency],
|
||||
})
|
||||
class ModuleB {}
|
||||
|
||||
@Module({
|
||||
imports: [GlobalDepsModule, ModuleA, ModuleB],
|
||||
})
|
||||
class AppModule {}
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
const app = await Promise.race([
|
||||
NestFactory.create(AppModule, { logger: false }),
|
||||
new Promise<never>((_, reject) => {
|
||||
timeout = setTimeout(
|
||||
() => reject(new Error('Application initialization timed out')),
|
||||
1000,
|
||||
);
|
||||
}),
|
||||
]);
|
||||
clearTimeout(timeout!);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when "providers" and "exports" properties are inconsistent', () => {
|
||||
it(`should fail with "UnknownExportException"`, async () => {
|
||||
const builder = Test.createTestingModule({
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
Injectable,
|
||||
Module,
|
||||
Scope,
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST, createContextId } from '@nestjs/core';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('Parallel request-scoped resolution', () => {
|
||||
const ASYNC_REQUEST_SCOPED = 'ASYNC_REQUEST_SCOPED';
|
||||
const SAME_CONTEXT_REQUEST_SCOPED = 'SAME_CONTEXT_REQUEST_SCOPED';
|
||||
const OVERLAP_REQUEST_COUNT = 1000;
|
||||
|
||||
@Injectable()
|
||||
class ResolveInflightGate {
|
||||
private arrivalCount = 0;
|
||||
private released = false;
|
||||
private waitingResolver?: () => void;
|
||||
|
||||
async waitForOverlap() {
|
||||
if (this.released) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.arrivalCount++;
|
||||
if (this.arrivalCount === 1) {
|
||||
await new Promise<void>(resolve => {
|
||||
this.waitingResolver = resolve;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.released = true;
|
||||
this.waitingResolver?.();
|
||||
this.waitingResolver = undefined;
|
||||
}
|
||||
|
||||
async yieldOneTurn() {
|
||||
await new Promise<void>(resolve => setImmediate(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
class RequestReader {
|
||||
constructor(
|
||||
@Inject(REQUEST)
|
||||
public readonly request: {
|
||||
requestId: string;
|
||||
},
|
||||
) {}
|
||||
}
|
||||
|
||||
const AsyncRequestScopedProvider = {
|
||||
provide: ASYNC_REQUEST_SCOPED,
|
||||
scope: Scope.REQUEST,
|
||||
inject: [RequestReader, ResolveInflightGate],
|
||||
useFactory: async (
|
||||
requestReader: RequestReader,
|
||||
inflightGate: ResolveInflightGate,
|
||||
) => {
|
||||
await inflightGate.waitForOverlap();
|
||||
|
||||
return {
|
||||
requestId: requestReader.request.requestId,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const SameContextRequestScopedProvider = {
|
||||
provide: SAME_CONTEXT_REQUEST_SCOPED,
|
||||
scope: Scope.REQUEST,
|
||||
inject: [RequestReader, ResolveInflightGate],
|
||||
useFactory: async (
|
||||
requestReader: RequestReader,
|
||||
inflightGate: ResolveInflightGate,
|
||||
) => {
|
||||
await inflightGate.yieldOneTurn();
|
||||
|
||||
return {
|
||||
requestId: requestReader.request.requestId,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
class BubbledProvider {
|
||||
constructor(
|
||||
@Inject(ASYNC_REQUEST_SCOPED)
|
||||
public readonly payload: {
|
||||
requestId: string;
|
||||
},
|
||||
public readonly requestReader: RequestReader,
|
||||
) {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class SameContextBubbledProvider {
|
||||
constructor(
|
||||
@Inject(SAME_CONTEXT_REQUEST_SCOPED)
|
||||
public readonly payload: {
|
||||
requestId: string;
|
||||
},
|
||||
public readonly requestReader: RequestReader,
|
||||
) {}
|
||||
}
|
||||
|
||||
@Controller({ path: 'parallel', scope: Scope.REQUEST })
|
||||
class ParallelController {
|
||||
constructor(
|
||||
@Inject(ASYNC_REQUEST_SCOPED)
|
||||
public readonly payload: {
|
||||
requestId: string;
|
||||
},
|
||||
public readonly requestReader: RequestReader,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
getPayload() {
|
||||
return this.payload;
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
controllers: [ParallelController],
|
||||
providers: [
|
||||
ResolveInflightGate,
|
||||
RequestReader,
|
||||
AsyncRequestScopedProvider,
|
||||
SameContextRequestScopedProvider,
|
||||
BubbledProvider,
|
||||
SameContextBubbledProvider,
|
||||
],
|
||||
})
|
||||
class ParallelModule {}
|
||||
|
||||
let testingModule: TestingModule;
|
||||
|
||||
const prepareContexts = (moduleRef: TestingModule, count: number) =>
|
||||
Array.from({ length: count }, (_, index) => {
|
||||
const contextId = createContextId();
|
||||
const request = { requestId: `request-${index}` };
|
||||
moduleRef.registerRequestByContextId(request, contextId);
|
||||
return { contextId, request };
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
testingModule = await Test.createTestingModule({
|
||||
imports: [ParallelModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
it('should isolate request-scoped factory providers across overlapping context resolutions', async function () {
|
||||
this.timeout(20000);
|
||||
const contexts = prepareContexts(testingModule, OVERLAP_REQUEST_COUNT);
|
||||
|
||||
const payloads = await Promise.all(
|
||||
contexts.map(({ contextId }) =>
|
||||
testingModule.resolve<{ requestId: string }>(
|
||||
ASYNC_REQUEST_SCOPED,
|
||||
contextId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(payloads).to.deep.equal(
|
||||
contexts.map(({ request }) => ({ requestId: request.requestId })),
|
||||
);
|
||||
expect(new Set(payloads).size).to.equal(contexts.length);
|
||||
});
|
||||
|
||||
it('should bubble singleton providers to request scope during overlapping context resolutions', async function () {
|
||||
this.timeout(20000);
|
||||
const contexts = prepareContexts(testingModule, OVERLAP_REQUEST_COUNT);
|
||||
|
||||
const providers = await Promise.all(
|
||||
contexts.map(({ contextId }) =>
|
||||
testingModule.resolve(BubbledProvider, contextId),
|
||||
),
|
||||
);
|
||||
|
||||
expect(new Set(providers).size).to.equal(contexts.length);
|
||||
expect(providers.map(provider => provider.payload)).to.deep.equal(
|
||||
contexts.map(({ request }) => ({ requestId: request.requestId })),
|
||||
);
|
||||
expect(
|
||||
providers.map(provider => provider.requestReader.request.requestId),
|
||||
).to.deep.equal(contexts.map(({ request }) => request.requestId));
|
||||
});
|
||||
|
||||
it('should reuse a request-scoped factory provider for overlapping resolutions in the same context', async function () {
|
||||
this.timeout(20000);
|
||||
const [{ contextId, request }] = prepareContexts(testingModule, 1);
|
||||
|
||||
const providers = await Promise.all(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () =>
|
||||
testingModule.resolve<{ requestId: string }>(
|
||||
SAME_CONTEXT_REQUEST_SCOPED,
|
||||
contextId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(new Set(providers).size).to.equal(1);
|
||||
expect(providers[0]).to.deep.equal({ requestId: request.requestId });
|
||||
});
|
||||
|
||||
it('should reuse a bubbled provider for overlapping resolutions in the same context', async function () {
|
||||
this.timeout(20000);
|
||||
const [{ contextId, request }] = prepareContexts(testingModule, 1);
|
||||
|
||||
const providers = await Promise.all(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () =>
|
||||
testingModule.resolve(SameContextBubbledProvider, contextId),
|
||||
),
|
||||
);
|
||||
|
||||
expect(new Set(providers).size).to.equal(1);
|
||||
expect(providers[0].payload).to.deep.equal({
|
||||
requestId: request.requestId,
|
||||
});
|
||||
expect(providers[0].requestReader.request).to.equal(request);
|
||||
});
|
||||
|
||||
it('should isolate request-scoped controllers during overlapping context resolutions', async function () {
|
||||
this.timeout(20000);
|
||||
const contexts = prepareContexts(testingModule, OVERLAP_REQUEST_COUNT);
|
||||
|
||||
const controllers = await Promise.all(
|
||||
contexts.map(({ contextId }) =>
|
||||
testingModule.resolve(ParallelController, contextId),
|
||||
),
|
||||
);
|
||||
|
||||
expect(new Set(controllers).size).to.equal(contexts.length);
|
||||
expect(controllers.map(controller => controller.payload)).to.deep.equal(
|
||||
contexts.map(({ request }) => ({ requestId: request.requestId })),
|
||||
);
|
||||
expect(
|
||||
controllers.map(controller => controller.requestReader.request.requestId),
|
||||
).to.deep.equal(contexts.map(({ request }) => request.requestId));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,235 @@
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
Controller,
|
||||
ExceptionFilter,
|
||||
Get,
|
||||
Inject,
|
||||
Injectable,
|
||||
Module,
|
||||
Scope,
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('Request-scoped factory provider', () => {
|
||||
const REQUEST_SCOPED_FACTORY = 'REQUEST_SCOPED_FACTORY';
|
||||
const REQUEST_COUNT = 1000;
|
||||
|
||||
@Catch()
|
||||
class RecordingFilter implements ExceptionFilter {
|
||||
catch(exception: unknown, host: ArgumentsHost) {
|
||||
const response = host.switchToHttp().getResponse<{
|
||||
status: (code: number) => unknown;
|
||||
send: (body: unknown) => unknown;
|
||||
}>();
|
||||
|
||||
response.status(500);
|
||||
response.send({
|
||||
statusCode: 500,
|
||||
message:
|
||||
exception instanceof Error
|
||||
? exception.message
|
||||
: 'Internal server error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticatedTenant {
|
||||
constructor(
|
||||
public readonly tenantName: string,
|
||||
public readonly clientName: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class JwtStrategyService {
|
||||
validate(request: FastifyRequest) {
|
||||
const tenantId = (request.headers['x-tenant-id'] as string) ?? 'tenant-1';
|
||||
const clientId = (request.headers['x-client-id'] as string) ?? 'client-1';
|
||||
return {
|
||||
claim: {
|
||||
tenantId,
|
||||
clientId,
|
||||
},
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class ConfigService {
|
||||
get(_key: string) {
|
||||
return 'config-value';
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
class InflightGate {
|
||||
private arrivalCount = 0;
|
||||
private released = false;
|
||||
private waitingResolver?: () => void;
|
||||
|
||||
async waitForOverlap() {
|
||||
if (this.released) {
|
||||
return;
|
||||
}
|
||||
this.arrivalCount++;
|
||||
if (this.arrivalCount === 1) {
|
||||
await new Promise<void>(resolve => {
|
||||
this.waitingResolver = resolve;
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.released = true;
|
||||
this.waitingResolver?.();
|
||||
this.waitingResolver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const AuthenticatedTenantProvider = {
|
||||
provide: AuthenticatedTenant,
|
||||
inject: [JwtStrategyService, REQUEST, ConfigService],
|
||||
scope: Scope.REQUEST,
|
||||
useFactory: async (
|
||||
strategy: JwtStrategyService,
|
||||
request: FastifyRequest,
|
||||
_config: ConfigService,
|
||||
) => {
|
||||
const verification = strategy.validate(request);
|
||||
|
||||
return new AuthenticatedTenant(
|
||||
verification.claim.tenantId,
|
||||
verification.claim.clientId,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
class ActorService {
|
||||
constructor(@Inject(REQUEST) private readonly request: FastifyRequest) {}
|
||||
|
||||
public getActor() {
|
||||
return (this.request.headers['x-actor-id'] as string) ?? 'system';
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [JwtStrategyService, ConfigService, AuthenticatedTenantProvider],
|
||||
exports: [AuthenticatedTenant],
|
||||
})
|
||||
class AuthModule {}
|
||||
|
||||
@Module({
|
||||
providers: [ActorService],
|
||||
exports: [ActorService],
|
||||
})
|
||||
class ActorModule {}
|
||||
|
||||
@Controller()
|
||||
class TestController {
|
||||
constructor(
|
||||
@Inject(REQUEST_SCOPED_FACTORY)
|
||||
private readonly requestScopedFactory: {
|
||||
actorId: string;
|
||||
tenantId: string;
|
||||
},
|
||||
) {}
|
||||
|
||||
@Get('/posts')
|
||||
public getValue() {
|
||||
return this.requestScopedFactory;
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, ActorModule],
|
||||
controllers: [TestController],
|
||||
providers: [
|
||||
InflightGate,
|
||||
{
|
||||
provide: REQUEST_SCOPED_FACTORY,
|
||||
scope: Scope.REQUEST,
|
||||
inject: [AuthenticatedTenant, ActorService, InflightGate],
|
||||
useFactory: async (
|
||||
authenticatedTenant: AuthenticatedTenant,
|
||||
actorService: ActorService,
|
||||
inflightGate: InflightGate,
|
||||
) => {
|
||||
await inflightGate.waitForOverlap();
|
||||
|
||||
return {
|
||||
actorId: actorService.getActor() ?? '',
|
||||
tenantId: authenticatedTenant.tenantName,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
class TestModule {}
|
||||
|
||||
let app: NestFastifyApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [TestModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication<NestFastifyApplication>(
|
||||
new FastifyAdapter(),
|
||||
{ logger: false },
|
||||
);
|
||||
app.useGlobalFilters(new RecordingFilter());
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('should resolve all injected dependencies across overlapping requests', async function () {
|
||||
this.timeout(20000);
|
||||
const responses = await Promise.all(
|
||||
Array.from({ length: REQUEST_COUNT }, (_, index) =>
|
||||
app.inject({
|
||||
method: 'GET',
|
||||
url: '/posts',
|
||||
headers: {
|
||||
'x-actor-id': `actor-${index}`,
|
||||
'x-client-id': `client-${index}`,
|
||||
'x-tenant-id': `tenant-${index}`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const failures = responses.flatMap((response, index) => {
|
||||
if (response.statusCode !== 200) {
|
||||
return [
|
||||
{
|
||||
body: response.body,
|
||||
index,
|
||||
statusCode: response.statusCode,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
expect(failures).to.deep.equal([]);
|
||||
|
||||
const payloads = responses.map(response => JSON.parse(response.body));
|
||||
expect(payloads).to.deep.equal(
|
||||
Array.from({ length: REQUEST_COUNT }, (_, index) => ({
|
||||
actorId: `actor-${index}`,
|
||||
tenantId: `tenant-${index}`,
|
||||
})),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import { TransientService } from '../src/scoped/transient.service.js';
|
||||
import { Transient3Service } from '../src/scoped/transient3.service.js';
|
||||
|
||||
describe('Scoped Instances', () => {
|
||||
const OVERLAP_REQUEST_COUNT = 1000;
|
||||
let testingModule: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -72,6 +73,46 @@ describe('Scoped Instances', () => {
|
||||
expect(request3.request).toBe(requestProvider);
|
||||
});
|
||||
|
||||
it('should isolate request-scoped providers across parallel context resolutions', async () => {
|
||||
const contexts = Array.from(
|
||||
{ length: OVERLAP_REQUEST_COUNT },
|
||||
(_, index) => {
|
||||
const contextId = createContextId();
|
||||
const requestProvider = { host: `parallel-host-${index}` };
|
||||
|
||||
testingModule.registerRequestByContextId(requestProvider, contextId);
|
||||
return { contextId, requestProvider };
|
||||
},
|
||||
);
|
||||
|
||||
const providers = await Promise.all(
|
||||
contexts.map(({ contextId }) =>
|
||||
testingModule.resolve(ScopedService, contextId),
|
||||
),
|
||||
);
|
||||
|
||||
expect(new Set(providers).size).toBe(contexts.length);
|
||||
expect(providers.map(provider => provider.request)).toEqual(
|
||||
contexts.map(({ requestProvider }) => requestProvider),
|
||||
);
|
||||
}, 20000);
|
||||
|
||||
it('should reuse request-scoped providers for parallel resolutions in the same context', async () => {
|
||||
const contextId = createContextId();
|
||||
const requestProvider = { host: 'shared-host' };
|
||||
|
||||
testingModule.registerRequestByContextId(requestProvider, contextId);
|
||||
|
||||
const providers = await Promise.all(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () =>
|
||||
testingModule.resolve(ScopedService, contextId),
|
||||
),
|
||||
);
|
||||
|
||||
expect(new Set(providers).size).toBe(1);
|
||||
expect(providers[0].request).toBe(requestProvider);
|
||||
}, 20000);
|
||||
|
||||
it('should dynamically resolve request-scoped controller', async () => {
|
||||
const request1 = await testingModule.resolve(ScopedController);
|
||||
const request2 = await testingModule.resolve(ScopedController);
|
||||
@@ -82,6 +123,20 @@ describe('Scoped Instances', () => {
|
||||
expect(request3).not.toBe(request2);
|
||||
});
|
||||
|
||||
it('should isolate request-scoped controllers across parallel context resolutions', async () => {
|
||||
const contexts = Array.from({ length: OVERLAP_REQUEST_COUNT }, () =>
|
||||
createContextId(),
|
||||
);
|
||||
|
||||
const controllers = await Promise.all(
|
||||
contexts.map(contextId =>
|
||||
testingModule.resolve(ScopedController, contextId),
|
||||
),
|
||||
);
|
||||
|
||||
expect(new Set(controllers).size).toBe(contexts.length);
|
||||
}, 20000);
|
||||
|
||||
it('should throw an exception when "get()" method is used for scoped providers', () => {
|
||||
expect(() => testingModule.get(ScopedController)).toThrow(
|
||||
InvalidClassScopeException,
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "LoggingInterceptor",
|
||||
"subtype": "interceptor",
|
||||
"initTime": 0
|
||||
"subtype": "interceptor"
|
||||
}
|
||||
},
|
||||
"55510001": {
|
||||
@@ -30,8 +29,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "UsersService",
|
||||
"initTime": 0
|
||||
"token": "UsersService"
|
||||
}
|
||||
},
|
||||
"91819333": {
|
||||
@@ -306,8 +304,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": true,
|
||||
"token": "HelperService",
|
||||
"initTime": 0
|
||||
"token": "HelperService"
|
||||
}
|
||||
},
|
||||
"941957564": {
|
||||
@@ -356,8 +353,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "META",
|
||||
"initTime": 0
|
||||
"token": "META"
|
||||
}
|
||||
},
|
||||
"1010833816": {
|
||||
@@ -542,7 +538,6 @@
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "HelloController",
|
||||
"initTime": 0,
|
||||
"enhancers": [
|
||||
{
|
||||
"id": "-2010060263",
|
||||
@@ -692,8 +687,7 @@
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "UserByIdPipe",
|
||||
"subtype": "pipe",
|
||||
"initTime": 0
|
||||
"subtype": "pipe"
|
||||
}
|
||||
},
|
||||
"1919157847": {
|
||||
@@ -709,8 +703,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": true,
|
||||
"token": "REQUEST",
|
||||
"initTime": 0
|
||||
"token": "REQUEST"
|
||||
}
|
||||
},
|
||||
"2018972420": {
|
||||
@@ -786,8 +779,7 @@
|
||||
"scope": 1,
|
||||
"transient": true,
|
||||
"exported": true,
|
||||
"token": "INQUIRER",
|
||||
"initTime": 0
|
||||
"token": "INQUIRER"
|
||||
}
|
||||
},
|
||||
"-26938366": {
|
||||
@@ -1103,8 +1095,7 @@
|
||||
"scope": 0,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "HelloModule",
|
||||
"initTime": 0
|
||||
"token": "HelloModule"
|
||||
}
|
||||
},
|
||||
"-1166465801": {
|
||||
@@ -1152,8 +1143,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "HelloService",
|
||||
"initTime": 0
|
||||
"token": "HelloService"
|
||||
}
|
||||
},
|
||||
"-2010060263": {
|
||||
@@ -1246,8 +1236,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "DurableService",
|
||||
"initTime": 0
|
||||
"token": "DurableService"
|
||||
}
|
||||
},
|
||||
"-848516688": {
|
||||
@@ -1262,8 +1251,7 @@
|
||||
"static": false,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "DurableController",
|
||||
"initTime": 0
|
||||
"token": "DurableController"
|
||||
}
|
||||
},
|
||||
"-1096226358": {
|
||||
@@ -1557,8 +1545,7 @@
|
||||
"static": false,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "RequestChainService",
|
||||
"initTime": 0
|
||||
"token": "RequestChainService"
|
||||
}
|
||||
},
|
||||
"-1927427604": {
|
||||
@@ -1574,7 +1561,6 @@
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "RequestChainController",
|
||||
"initTime": 0,
|
||||
"enhancers": [
|
||||
{
|
||||
"id": "18347652",
|
||||
@@ -2031,22 +2017,6 @@
|
||||
},
|
||||
"id": "1059069749"
|
||||
},
|
||||
"1142343545": {
|
||||
"source": "-1758187461",
|
||||
"target": "1004276345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "HelloModule",
|
||||
"sourceClassName": "HelloService",
|
||||
"targetClassName": "Meta",
|
||||
"sourceClassToken": "HelloService",
|
||||
"targetClassToken": "META",
|
||||
"targetModuleName": "HelloModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "1142343545"
|
||||
},
|
||||
"1178436405": {
|
||||
"source": "1893794611",
|
||||
"target": "55510001",
|
||||
@@ -2098,22 +2068,6 @@
|
||||
},
|
||||
"id": "1325313063"
|
||||
},
|
||||
"1497653915": {
|
||||
"source": "55510001",
|
||||
"target": "1004276345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "HelloModule",
|
||||
"sourceClassName": "UsersService",
|
||||
"targetClassName": "Meta",
|
||||
"sourceClassToken": "UsersService",
|
||||
"targetClassToken": "META",
|
||||
"targetModuleName": "HelloModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "1497653915"
|
||||
},
|
||||
"1615322974": {
|
||||
"source": "-1469176692",
|
||||
"target": "555176277",
|
||||
@@ -2213,6 +2167,22 @@
|
||||
},
|
||||
"id": "-21463590"
|
||||
},
|
||||
"-2146943494": {
|
||||
"source": "-234035039",
|
||||
"target": "928565345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "RequestChainModule",
|
||||
"sourceClassName": "RequestChainService",
|
||||
"targetClassName": "HelperService",
|
||||
"sourceClassToken": "RequestChainService",
|
||||
"targetClassToken": "HelperService",
|
||||
"targetModuleName": "HelperModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-2146943494"
|
||||
},
|
||||
"-2105726668": {
|
||||
"source": "-1803759743",
|
||||
"target": "1010833816",
|
||||
@@ -2229,40 +2199,6 @@
|
||||
},
|
||||
"id": "-2105726668"
|
||||
},
|
||||
"-1657371464": {
|
||||
"source": "-1673986099",
|
||||
"target": "1919157847",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "DurableModule",
|
||||
"sourceClassName": "DurableService",
|
||||
"targetClassName": "REQUEST",
|
||||
"sourceClassToken": "DurableService",
|
||||
"targetClassToken": "REQUEST",
|
||||
"targetModuleName": "InternalCoreModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor",
|
||||
"internal": true
|
||||
},
|
||||
"id": "-1657371464"
|
||||
},
|
||||
"-1303681274": {
|
||||
"source": "928565345",
|
||||
"target": "1919157847",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "HelperModule",
|
||||
"sourceClassName": "HelperService",
|
||||
"targetClassName": "REQUEST",
|
||||
"sourceClassToken": "HelperService",
|
||||
"targetClassToken": "REQUEST",
|
||||
"targetModuleName": "InternalCoreModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor",
|
||||
"internal": true
|
||||
},
|
||||
"id": "-1303681274"
|
||||
},
|
||||
"-831049991": {
|
||||
"source": "594986539",
|
||||
"target": "-1721730431",
|
||||
@@ -2279,6 +2215,22 @@
|
||||
},
|
||||
"id": "-831049991"
|
||||
},
|
||||
"-1816180282": {
|
||||
"source": "-848516688",
|
||||
"target": "-1673986099",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "DurableModule",
|
||||
"sourceClassName": "DurableController",
|
||||
"targetClassName": "DurableService",
|
||||
"sourceClassToken": "DurableController",
|
||||
"targetClassToken": "DurableService",
|
||||
"targetModuleName": "DurableModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-1816180282"
|
||||
},
|
||||
"-886102564": {
|
||||
"source": "208171089",
|
||||
"target": "671882984",
|
||||
@@ -2296,22 +2248,6 @@
|
||||
},
|
||||
"id": "-886102564"
|
||||
},
|
||||
"-2146943494": {
|
||||
"source": "-234035039",
|
||||
"target": "928565345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "RequestChainModule",
|
||||
"sourceClassName": "RequestChainService",
|
||||
"targetClassName": "HelperService",
|
||||
"sourceClassToken": "RequestChainService",
|
||||
"targetClassToken": "HelperService",
|
||||
"targetModuleName": "HelperModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-2146943494"
|
||||
},
|
||||
"-2003045613": {
|
||||
"source": "-377928898",
|
||||
"target": "-616397055",
|
||||
@@ -2344,22 +2280,6 @@
|
||||
},
|
||||
"id": "-881420795"
|
||||
},
|
||||
"-1816180282": {
|
||||
"source": "-848516688",
|
||||
"target": "-1673986099",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "DurableModule",
|
||||
"sourceClassName": "DurableController",
|
||||
"targetClassName": "DurableService",
|
||||
"sourceClassToken": "DurableController",
|
||||
"targetClassToken": "DurableService",
|
||||
"targetModuleName": "DurableModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-1816180282"
|
||||
},
|
||||
"-180350082": {
|
||||
"source": "-1550892030",
|
||||
"target": "-593595711",
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "LoggingInterceptor",
|
||||
"subtype": "interceptor",
|
||||
"initTime": 0
|
||||
"subtype": "interceptor"
|
||||
}
|
||||
},
|
||||
"55510001": {
|
||||
@@ -30,8 +29,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "UsersService",
|
||||
"initTime": 0
|
||||
"token": "UsersService"
|
||||
}
|
||||
},
|
||||
"91819333": {
|
||||
@@ -306,8 +304,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": true,
|
||||
"token": "HelperService",
|
||||
"initTime": 0
|
||||
"token": "HelperService"
|
||||
}
|
||||
},
|
||||
"941957564": {
|
||||
@@ -356,8 +353,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "META",
|
||||
"initTime": 0
|
||||
"token": "META"
|
||||
}
|
||||
},
|
||||
"1010833816": {
|
||||
@@ -542,7 +538,6 @@
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "HelloController",
|
||||
"initTime": 0,
|
||||
"enhancers": [
|
||||
{
|
||||
"id": "-2010060263",
|
||||
@@ -676,8 +671,7 @@
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "UserByIdPipe",
|
||||
"subtype": "pipe",
|
||||
"initTime": 0
|
||||
"subtype": "pipe"
|
||||
}
|
||||
},
|
||||
"1919157847": {
|
||||
@@ -693,8 +687,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": true,
|
||||
"token": "REQUEST",
|
||||
"initTime": 0
|
||||
"token": "REQUEST"
|
||||
}
|
||||
},
|
||||
"2018972420": {
|
||||
@@ -770,8 +763,7 @@
|
||||
"scope": 1,
|
||||
"transient": true,
|
||||
"exported": true,
|
||||
"token": "INQUIRER",
|
||||
"initTime": 0
|
||||
"token": "INQUIRER"
|
||||
}
|
||||
},
|
||||
"-26938366": {
|
||||
@@ -1087,8 +1079,7 @@
|
||||
"scope": 0,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "HelloModule",
|
||||
"initTime": 0
|
||||
"token": "HelloModule"
|
||||
}
|
||||
},
|
||||
"-1166465801": {
|
||||
@@ -1136,8 +1127,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "HelloService",
|
||||
"initTime": 0
|
||||
"token": "HelloService"
|
||||
}
|
||||
},
|
||||
"-2010060263": {
|
||||
@@ -1230,8 +1220,7 @@
|
||||
"scope": 2,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "DurableService",
|
||||
"initTime": 0
|
||||
"token": "DurableService"
|
||||
}
|
||||
},
|
||||
"-848516688": {
|
||||
@@ -1246,8 +1235,7 @@
|
||||
"static": false,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "DurableController",
|
||||
"initTime": 0
|
||||
"token": "DurableController"
|
||||
}
|
||||
},
|
||||
"-1096226358": {
|
||||
@@ -1541,8 +1529,7 @@
|
||||
"static": false,
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "RequestChainService",
|
||||
"initTime": 0
|
||||
"token": "RequestChainService"
|
||||
}
|
||||
},
|
||||
"-1927427604": {
|
||||
@@ -1558,7 +1545,6 @@
|
||||
"transient": false,
|
||||
"exported": false,
|
||||
"token": "RequestChainController",
|
||||
"initTime": 0,
|
||||
"enhancers": [
|
||||
{
|
||||
"id": "18347652",
|
||||
@@ -2015,22 +2001,6 @@
|
||||
},
|
||||
"id": "1059069749"
|
||||
},
|
||||
"1142343545": {
|
||||
"source": "-1758187461",
|
||||
"target": "1004276345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "HelloModule",
|
||||
"sourceClassName": "HelloService",
|
||||
"targetClassName": "Meta",
|
||||
"sourceClassToken": "HelloService",
|
||||
"targetClassToken": "META",
|
||||
"targetModuleName": "HelloModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "1142343545"
|
||||
},
|
||||
"1178436405": {
|
||||
"source": "1893794611",
|
||||
"target": "55510001",
|
||||
@@ -2082,22 +2052,6 @@
|
||||
},
|
||||
"id": "1325313063"
|
||||
},
|
||||
"1497653915": {
|
||||
"source": "55510001",
|
||||
"target": "1004276345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "HelloModule",
|
||||
"sourceClassName": "UsersService",
|
||||
"targetClassName": "Meta",
|
||||
"sourceClassToken": "UsersService",
|
||||
"targetClassToken": "META",
|
||||
"targetModuleName": "HelloModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "1497653915"
|
||||
},
|
||||
"1615322974": {
|
||||
"source": "-1469176692",
|
||||
"target": "555176277",
|
||||
@@ -2197,6 +2151,22 @@
|
||||
},
|
||||
"id": "-21463590"
|
||||
},
|
||||
"-2146943494": {
|
||||
"source": "-234035039",
|
||||
"target": "928565345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "RequestChainModule",
|
||||
"sourceClassName": "RequestChainService",
|
||||
"targetClassName": "HelperService",
|
||||
"sourceClassToken": "RequestChainService",
|
||||
"targetClassToken": "HelperService",
|
||||
"targetModuleName": "HelperModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-2146943494"
|
||||
},
|
||||
"-2105726668": {
|
||||
"source": "-1803759743",
|
||||
"target": "1010833816",
|
||||
@@ -2213,40 +2183,6 @@
|
||||
},
|
||||
"id": "-2105726668"
|
||||
},
|
||||
"-1657371464": {
|
||||
"source": "-1673986099",
|
||||
"target": "1919157847",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "DurableModule",
|
||||
"sourceClassName": "DurableService",
|
||||
"targetClassName": "REQUEST",
|
||||
"sourceClassToken": "DurableService",
|
||||
"targetClassToken": "REQUEST",
|
||||
"targetModuleName": "InternalCoreModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor",
|
||||
"internal": true
|
||||
},
|
||||
"id": "-1657371464"
|
||||
},
|
||||
"-1303681274": {
|
||||
"source": "928565345",
|
||||
"target": "1919157847",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "HelperModule",
|
||||
"sourceClassName": "HelperService",
|
||||
"targetClassName": "REQUEST",
|
||||
"sourceClassToken": "HelperService",
|
||||
"targetClassToken": "REQUEST",
|
||||
"targetModuleName": "InternalCoreModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor",
|
||||
"internal": true
|
||||
},
|
||||
"id": "-1303681274"
|
||||
},
|
||||
"-831049991": {
|
||||
"source": "594986539",
|
||||
"target": "-1721730431",
|
||||
@@ -2263,6 +2199,22 @@
|
||||
},
|
||||
"id": "-831049991"
|
||||
},
|
||||
"-1816180282": {
|
||||
"source": "-848516688",
|
||||
"target": "-1673986099",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "DurableModule",
|
||||
"sourceClassName": "DurableController",
|
||||
"targetClassName": "DurableService",
|
||||
"sourceClassToken": "DurableController",
|
||||
"targetClassToken": "DurableService",
|
||||
"targetModuleName": "DurableModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-1816180282"
|
||||
},
|
||||
"-886102564": {
|
||||
"source": "208171089",
|
||||
"target": "671882984",
|
||||
@@ -2280,22 +2232,6 @@
|
||||
},
|
||||
"id": "-886102564"
|
||||
},
|
||||
"-2146943494": {
|
||||
"source": "-234035039",
|
||||
"target": "928565345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "RequestChainModule",
|
||||
"sourceClassName": "RequestChainService",
|
||||
"targetClassName": "HelperService",
|
||||
"sourceClassToken": "RequestChainService",
|
||||
"targetClassToken": "HelperService",
|
||||
"targetModuleName": "HelperModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-2146943494"
|
||||
},
|
||||
"-2003045613": {
|
||||
"source": "-377928898",
|
||||
"target": "-616397055",
|
||||
@@ -2328,22 +2264,6 @@
|
||||
},
|
||||
"id": "-881420795"
|
||||
},
|
||||
"-1816180282": {
|
||||
"source": "-848516688",
|
||||
"target": "-1673986099",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "DurableModule",
|
||||
"sourceClassName": "DurableController",
|
||||
"targetClassName": "DurableService",
|
||||
"sourceClassToken": "DurableController",
|
||||
"targetClassToken": "DurableService",
|
||||
"targetModuleName": "DurableModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-1816180282"
|
||||
},
|
||||
"-180350082": {
|
||||
"source": "-1550892030",
|
||||
"target": "-593595711",
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('GRPC transport', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`GRPC with backpressure control`, async function () {
|
||||
it(`GRPC with backpressure control`, async () => {
|
||||
// This test hit the gRPC server with 1000 messages, but the server
|
||||
// has to process large (> 1MB) messages, so it will definitely hit
|
||||
// issues where writing to the stream needs to be paused until a drain
|
||||
@@ -169,7 +169,7 @@ describe('GRPC transport', () => {
|
||||
});
|
||||
|
||||
expect(receivedIds).toEqual(expectedIds);
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
describe('streaming calls that error', () => {
|
||||
// We want to assert that the application does not crash when an error is encountered with an unhandledRejection
|
||||
|
||||
@@ -6,8 +6,10 @@ import { DurableContextIdStrategy } from '../src/durable/durable-context-id.stra
|
||||
import { DurableModule } from '../src/durable/durable.module.js';
|
||||
|
||||
describe('Durable providers', () => {
|
||||
const OVERLAP_REQUEST_COUNT = 1000;
|
||||
let server: any;
|
||||
let app: INestApplication;
|
||||
let baseUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
@@ -16,7 +18,8 @@ describe('Durable providers', () => {
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
server = app.getHttpServer();
|
||||
await app.init();
|
||||
await app.listen(0);
|
||||
baseUrl = await app.getUrl();
|
||||
|
||||
ContextIdFactory.apply(new DurableContextIdStrategy());
|
||||
});
|
||||
@@ -39,6 +42,18 @@ describe('Durable providers', () => {
|
||||
end(res);
|
||||
});
|
||||
|
||||
const performHttpCallAsync = (
|
||||
tenantId: number,
|
||||
endpoint = '/durable',
|
||||
opts: {
|
||||
forceError: boolean;
|
||||
} = { forceError: false },
|
||||
) =>
|
||||
request(baseUrl)
|
||||
.get(endpoint)
|
||||
.set({ ['x-tenant-id']: String(tenantId) })
|
||||
.set({ ['x-force-error']: opts.forceError ? 'true' : 'false' });
|
||||
|
||||
it(`should share durable providers per tenant`, async () => {
|
||||
let result: request.Response;
|
||||
result = await new Promise<request.Response>(resolve =>
|
||||
@@ -107,6 +122,50 @@ describe('Durable providers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it(`should preserve request context across overlapping requests from different tenants`, async () => {
|
||||
const tenantIds = Array.from(
|
||||
{ length: OVERLAP_REQUEST_COUNT },
|
||||
(_, index) => index + 21,
|
||||
);
|
||||
|
||||
const responses = await Promise.all(
|
||||
tenantIds.map(tenantId =>
|
||||
performHttpCallAsync(tenantId, '/durable/request-context'),
|
||||
),
|
||||
);
|
||||
|
||||
expect(responses.map(response => response.statusCode)).toEqual(
|
||||
tenantIds.map(() => 200),
|
||||
);
|
||||
expect(responses.map(response => response.body)).toEqual(
|
||||
tenantIds.map(tenantId => ({
|
||||
durableService: String(tenantId),
|
||||
nonDurableService: String(tenantId),
|
||||
})),
|
||||
);
|
||||
}, 20000);
|
||||
|
||||
it(`should reuse the durable subtree across overlapping requests for the same tenant`, async () => {
|
||||
const tenantId = 31;
|
||||
|
||||
const responses = await Promise.all(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () =>
|
||||
performHttpCallAsync(tenantId),
|
||||
),
|
||||
);
|
||||
|
||||
const counters = responses
|
||||
.map(response => Number(response.text.match(/Counter: (\d+)/)?.[1]))
|
||||
.sort((left, right) => left - right);
|
||||
|
||||
expect(responses.map(response => response.statusCode)).toEqual(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () => 200),
|
||||
);
|
||||
expect(counters).toEqual(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, (_, index) => index + 1),
|
||||
);
|
||||
}, 20000);
|
||||
|
||||
it(`should not cache durable providers that throw errors`, async () => {
|
||||
let result: request.Response;
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import { RequestChainModule } from '../src/request-chain/request-chain.module.js
|
||||
import { RequestChainService } from '../src/request-chain/request-chain.service.js';
|
||||
|
||||
describe('Request scope (modules propagation)', () => {
|
||||
const OVERLAP_REQUEST_COUNT = 1000;
|
||||
let server;
|
||||
let app: INestApplication;
|
||||
let baseUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
@@ -15,7 +17,8 @@ describe('Request scope (modules propagation)', () => {
|
||||
|
||||
app = module.createNestApplication();
|
||||
server = app.getHttpServer();
|
||||
await app.init();
|
||||
await app.listen(0);
|
||||
baseUrl = await app.getUrl();
|
||||
});
|
||||
|
||||
describe('when service from parent module is request scoped', () => {
|
||||
@@ -37,6 +40,32 @@ describe('Request scope (modules propagation)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when service from parent module is request scoped under overlapping requests', () => {
|
||||
let counterBefore: number;
|
||||
let responses: request.Response[];
|
||||
|
||||
beforeAll(async () => {
|
||||
counterBefore = RequestChainService.COUNTER;
|
||||
responses = await Promise.all(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () =>
|
||||
request(baseUrl).get('/hello'),
|
||||
),
|
||||
);
|
||||
}, 20000);
|
||||
|
||||
it('should complete every overlapping request successfully', () => {
|
||||
expect(responses.map(response => response.status)).toEqual(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () => 200),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create the request-scoped dependency chain for every overlapping request', () => {
|
||||
expect(RequestChainService.COUNTER - counterBefore).toBe(
|
||||
OVERLAP_REQUEST_COUNT,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -16,8 +16,10 @@ class Meta {
|
||||
}
|
||||
|
||||
describe('Request scope', () => {
|
||||
const OVERLAP_REQUEST_COUNT = 1000;
|
||||
let server;
|
||||
let app: INestApplication;
|
||||
let baseUrl: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
@@ -31,7 +33,8 @@ describe('Request scope', () => {
|
||||
|
||||
app = module.createNestApplication();
|
||||
server = app.getHttpServer();
|
||||
await app.init();
|
||||
await app.listen(0);
|
||||
baseUrl = await app.getUrl();
|
||||
});
|
||||
|
||||
describe('when one service is request scoped', () => {
|
||||
@@ -76,6 +79,69 @@ describe('Request scope', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when overlapping requests are handled', () => {
|
||||
let controllerCounterBefore: number;
|
||||
let usersCounterBefore: number;
|
||||
let pipeCounterBefore: number;
|
||||
let interceptorCounterBefore: number;
|
||||
let guardCounterBefore: number;
|
||||
let pipeDataLengthBefore: number;
|
||||
let interceptorDataLengthBefore: number;
|
||||
let guardDataLengthBefore: number;
|
||||
let responses: request.Response[];
|
||||
|
||||
beforeAll(async () => {
|
||||
controllerCounterBefore = HelloController.COUNTER;
|
||||
usersCounterBefore = UsersService.COUNTER;
|
||||
pipeCounterBefore = UserByIdPipe.COUNTER;
|
||||
interceptorCounterBefore = Interceptor.COUNTER;
|
||||
guardCounterBefore = Guard.COUNTER;
|
||||
pipeDataLengthBefore = UserByIdPipe.REQUEST_SCOPED_DATA.length;
|
||||
interceptorDataLengthBefore = Interceptor.REQUEST_SCOPED_DATA.length;
|
||||
guardDataLengthBefore = Guard.REQUEST_SCOPED_DATA.length;
|
||||
|
||||
responses = await Promise.all(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () =>
|
||||
request(baseUrl).get('/hello'),
|
||||
),
|
||||
);
|
||||
}, 20000);
|
||||
|
||||
it('should complete every overlapping request successfully', () => {
|
||||
expect(responses.map(response => response.status)).toEqual(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () => 200),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create a request-scoped controller and service for every overlapping request', () => {
|
||||
expect(HelloController.COUNTER - controllerCounterBefore).toBe(
|
||||
OVERLAP_REQUEST_COUNT,
|
||||
);
|
||||
expect(UsersService.COUNTER - usersCounterBefore).toBe(
|
||||
OVERLAP_REQUEST_COUNT,
|
||||
);
|
||||
});
|
||||
|
||||
it('should create request-scoped enhancers for every overlapping request', () => {
|
||||
expect(UserByIdPipe.COUNTER - pipeCounterBefore).toBe(
|
||||
OVERLAP_REQUEST_COUNT,
|
||||
);
|
||||
expect(Interceptor.COUNTER - interceptorCounterBefore).toBe(
|
||||
OVERLAP_REQUEST_COUNT,
|
||||
);
|
||||
expect(Guard.COUNTER - guardCounterBefore).toBe(OVERLAP_REQUEST_COUNT);
|
||||
expect(
|
||||
UserByIdPipe.REQUEST_SCOPED_DATA.slice(pipeDataLengthBefore),
|
||||
).toEqual(Array.from({ length: OVERLAP_REQUEST_COUNT }, () => 1));
|
||||
expect(
|
||||
Interceptor.REQUEST_SCOPED_DATA.slice(interceptorDataLengthBefore),
|
||||
).toEqual(Array.from({ length: OVERLAP_REQUEST_COUNT }, () => 1));
|
||||
expect(Guard.REQUEST_SCOPED_DATA.slice(guardDataLengthBefore)).toEqual(
|
||||
Array.from({ length: OVERLAP_REQUEST_COUNT }, () => 1),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { INestApplication, Injectable, Scope } from '@nestjs/common';
|
||||
import { createContextId } from '@nestjs/core';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import request from 'supertest';
|
||||
import { NestedTransientModule } from '../src/nested-transient/nested-transient.module.js';
|
||||
@@ -444,4 +445,148 @@ describe('Transient scope', () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when request-scoped providers have a deeply nested transient chain', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
class RequestDepthSixTransient {
|
||||
public static instanceCount = 0;
|
||||
public readonly instanceId: number;
|
||||
|
||||
constructor() {
|
||||
RequestDepthSixTransient.instanceCount++;
|
||||
this.instanceId = RequestDepthSixTransient.instanceCount;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
class RequestDepthFiveTransient {
|
||||
public static instanceCount = 0;
|
||||
public readonly instanceId: number;
|
||||
|
||||
constructor(public readonly next: RequestDepthSixTransient) {
|
||||
RequestDepthFiveTransient.instanceCount++;
|
||||
this.instanceId = RequestDepthFiveTransient.instanceCount;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
class RequestDepthFourTransient {
|
||||
constructor(public readonly next: RequestDepthFiveTransient) {}
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
class RequestDepthThreeTransient {
|
||||
constructor(public readonly next: RequestDepthFourTransient) {}
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
class RequestDepthTwoTransient {
|
||||
constructor(public readonly next: RequestDepthThreeTransient) {}
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
class RequestDepthOneTransient {
|
||||
constructor(public readonly next: RequestDepthTwoTransient) {}
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
class RequestScopedParentA {
|
||||
constructor(public readonly chain: RequestDepthOneTransient) {}
|
||||
}
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
class RequestScopedParentB {
|
||||
constructor(public readonly chain: RequestDepthOneTransient) {}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
RequestDepthFiveTransient.instanceCount = 0;
|
||||
RequestDepthSixTransient.instanceCount = 0;
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
RequestScopedParentA,
|
||||
RequestScopedParentB,
|
||||
RequestDepthOneTransient,
|
||||
RequestDepthTwoTransient,
|
||||
RequestDepthThreeTransient,
|
||||
RequestDepthFourTransient,
|
||||
RequestDepthFiveTransient,
|
||||
RequestDepthSixTransient,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('should create separate deep transient chains for different request-scoped parents in the same context', async () => {
|
||||
const contextId = createContextId();
|
||||
const [parentA, parentB] = await Promise.all([
|
||||
app.resolve(RequestScopedParentA, contextId),
|
||||
app.resolve(RequestScopedParentB, contextId),
|
||||
]);
|
||||
|
||||
expect(parentA).not.toBe(parentB);
|
||||
expect(parentA.chain).not.toBe(parentB.chain);
|
||||
expect(parentA.chain.next.next.next.next.instanceId).not.toBe(
|
||||
parentB.chain.next.next.next.next.instanceId,
|
||||
);
|
||||
expect(parentA.chain.next.next.next.next.next.instanceId).not.toBe(
|
||||
parentB.chain.next.next.next.next.next.instanceId,
|
||||
);
|
||||
}, 20000);
|
||||
|
||||
it('should isolate deep transient chains across overlapping request contexts', async () => {
|
||||
const contextIds = Array.from({ length: 200 }, () => createContextId());
|
||||
const parents = await Promise.all(
|
||||
contextIds.map(contextId =>
|
||||
app.resolve(RequestScopedParentA, contextId),
|
||||
),
|
||||
);
|
||||
|
||||
expect(new Set(parents).size).toBe(contextIds.length);
|
||||
expect(
|
||||
new Set(
|
||||
parents.map(parent => parent.chain.next.next.next.next.instanceId),
|
||||
).size,
|
||||
).toBe(contextIds.length);
|
||||
expect(
|
||||
new Set(
|
||||
parents.map(
|
||||
parent => parent.chain.next.next.next.next.next.instanceId,
|
||||
),
|
||||
).size,
|
||||
).toBe(contextIds.length);
|
||||
}, 20000);
|
||||
|
||||
it('should reuse the same request-scoped parent while preserving a single deep transient chain per context', async () => {
|
||||
const contextId = createContextId();
|
||||
const parents = await Promise.all(
|
||||
Array.from({ length: 200 }, () =>
|
||||
app.resolve(RequestScopedParentA, contextId),
|
||||
),
|
||||
);
|
||||
|
||||
expect(new Set(parents).size).toBe(1);
|
||||
expect(
|
||||
new Set(
|
||||
parents.map(parent => parent.chain.next.next.next.next.instanceId),
|
||||
).size,
|
||||
).toBe(1);
|
||||
expect(
|
||||
new Set(
|
||||
parents.map(
|
||||
parent => parent.chain.next.next.next.next.next.instanceId,
|
||||
),
|
||||
).size,
|
||||
).toBe(1);
|
||||
}, 20000);
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
Generated
+287
-392
File diff suppressed because it is too large.
Load diff
+22
-20
@@ -91,11 +91,11 @@
|
||||
"@fastify/multipart": "9.4.0",
|
||||
"@fastify/static": "9.0.0",
|
||||
"@fastify/view": "11.1.1",
|
||||
"@grpc/grpc-js": "1.14.3",
|
||||
"@grpc/proto-loader": "0.8.0",
|
||||
"@nats-io/transport-node": "^3.0.2",
|
||||
"@nestjs/apollo": "13.2.4",
|
||||
"@nestjs/graphql": "13.2.4",
|
||||
"@grpc/grpc-js": "1.14.4",
|
||||
"@grpc/proto-loader": "0.8.1",
|
||||
"@nats-io/transport-node": "^3.0.2",
|
||||
"@nestjs/apollo": "13.4.2",
|
||||
"@nestjs/graphql": "13.4.2",
|
||||
"@nestjs/mongoose": "11.0.4",
|
||||
"@nestjs/typeorm": "11.0.0",
|
||||
"@types/amqplib": "0.10.8",
|
||||
@@ -103,8 +103,8 @@
|
||||
"@types/express": "5.0.6",
|
||||
"@types/gulp": "4.0.18",
|
||||
"@types/http-errors": "2.0.5",
|
||||
"@types/node": "25.2.1",
|
||||
"@types/supertest": "6.0.3",
|
||||
"@types/node": "25.9.1",
|
||||
"@types/supertest": "7.2.0",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-istanbul": "4.1.2",
|
||||
"@vitest/coverage-v8": "4.1.2",
|
||||
@@ -135,15 +135,16 @@
|
||||
"lerna": "9.0.4",
|
||||
"lerna-changelog": "2.2.0",
|
||||
"light-my-request": "6.6.0",
|
||||
"lint-staged": "16.2.7",
|
||||
"lint-staged": "17.0.5",
|
||||
"markdown-table": "2.0.0",
|
||||
"mongoose": "9.1.6",
|
||||
"mqtt": "5.15.0",
|
||||
"multer": "2.0.2",
|
||||
"mysql2": "3.17.1",
|
||||
"oxlint": "1.58.0",
|
||||
"prettier": "3.7.4",
|
||||
"redis": "5.10.0",
|
||||
"mongoose": "9.6.2",
|
||||
"mqtt": "5.15.1",
|
||||
"multer": "2.1.1",
|
||||
"mysql2": "3.22.3",
|
||||
"nats": "2.29.3",
|
||||
"oxlint": "1.58.0",
|
||||
"prettier": "3.7.4",
|
||||
"redis": "5.12.1",
|
||||
"reusify": "1.1.0",
|
||||
"rxjs-compat": "6.6.7",
|
||||
"socket.io-client": "4.8.3",
|
||||
@@ -151,11 +152,12 @@
|
||||
"ts-morph": "27.0.2",
|
||||
"ts-node": "10.9.2",
|
||||
"tsx": "4.19.2",
|
||||
"typeorm": "0.3.28",
|
||||
"typescript": "6.0.2",
|
||||
"vite": "8.0.3",
|
||||
"vitest": "4.1.2",
|
||||
"ws": "8.19.0"
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.30",
|
||||
"typescript": "6.0.2",
|
||||
"vite": "8.0.3",
|
||||
"vitest": "4.1.2",
|
||||
"ws": "8.20.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
|
||||
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/common",
|
||||
"version": "11.1.20",
|
||||
"version": "11.1.23",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"homepage": "https://nestjs.com",
|
||||
|
||||
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -2,9 +2,17 @@ import { USING_INVALID_CLASS_AS_A_MODULE_MESSAGE } from '../messages.js';
|
||||
import { RuntimeException } from './runtime.exception.js';
|
||||
|
||||
export class InvalidClassModuleException extends RuntimeException {
|
||||
constructor(metatypeUsedAsAModule: any, scope: any[]) {
|
||||
constructor(
|
||||
metatypeUsedAsAModule: any,
|
||||
scope: any[],
|
||||
classKind: 'provider' | 'controller' | 'filter',
|
||||
) {
|
||||
super(
|
||||
USING_INVALID_CLASS_AS_A_MODULE_MESSAGE(metatypeUsedAsAModule, scope),
|
||||
USING_INVALID_CLASS_AS_A_MODULE_MESSAGE(
|
||||
metatypeUsedAsAModule,
|
||||
scope,
|
||||
classKind,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,12 @@ import { INVALID_MODULE_MESSAGE } from '../messages.js';
|
||||
import { RuntimeException } from './runtime.exception.js';
|
||||
|
||||
export class InvalidModuleException extends RuntimeException {
|
||||
constructor(parentModule: any, index: number, scope: any[]) {
|
||||
super(INVALID_MODULE_MESSAGE(parentModule, index, scope));
|
||||
constructor(
|
||||
parentModule: any,
|
||||
index: number,
|
||||
scope: any[],
|
||||
receivedValue: unknown,
|
||||
) {
|
||||
super(INVALID_MODULE_MESSAGE(parentModule, index, scope, receivedValue));
|
||||
}
|
||||
}
|
||||
@@ -157,11 +157,26 @@ export const INVALID_MODULE_MESSAGE = (
|
||||
parentModule: any,
|
||||
index: number,
|
||||
scope: any[],
|
||||
receivedValue: unknown,
|
||||
) => {
|
||||
const parentModuleName = parentModule?.name || 'module';
|
||||
|
||||
let formattedValue: string;
|
||||
let receivedType: string;
|
||||
if (receivedValue === null) {
|
||||
formattedValue = 'null';
|
||||
receivedType = 'null';
|
||||
} else if (typeof receivedValue === 'string') {
|
||||
formattedValue = `"${receivedValue}"`;
|
||||
receivedType = 'string';
|
||||
} else {
|
||||
formattedValue = String(receivedValue);
|
||||
receivedType = typeof receivedValue;
|
||||
}
|
||||
|
||||
return `Nest cannot create the ${parentModuleName} instance.
|
||||
Received an unexpected value at index [${index}] of the ${parentModuleName} "imports" array.
|
||||
The received value \`${formattedValue}\` is of type "${receivedType}".
|
||||
|
||||
Scope [${stringifyScope(scope)}]`;
|
||||
};
|
||||
@@ -169,11 +184,24 @@ Scope [${stringifyScope(scope)}]`;
|
||||
export const USING_INVALID_CLASS_AS_A_MODULE_MESSAGE = (
|
||||
metatypeUsedAsAModule: Type | ForwardReference,
|
||||
scope: any[],
|
||||
classKind: 'provider' | 'controller' | 'filter',
|
||||
) => {
|
||||
const metatypeNameQuote = `"${getInstanceName(metatypeUsedAsAModule)}"`;
|
||||
|
||||
return `Classes annotated with @Injectable(), @Catch(), and @Controller() decorators must not appear in the "imports" array of a module.
|
||||
Please remove ${metatypeNameQuote} (including forwarded occurrences, if any) from all of the "imports" arrays.
|
||||
let hint: string;
|
||||
switch (classKind) {
|
||||
case 'controller':
|
||||
hint = `${metatypeNameQuote} is decorated with @Controller() and cannot appear in the "imports" array of a module. Please move ${metatypeNameQuote} to the "controllers" array of the importing module instead.`;
|
||||
break;
|
||||
case 'provider':
|
||||
hint = `${metatypeNameQuote} is decorated with @Injectable() and cannot appear in the "imports" array of a module. Please move ${metatypeNameQuote} to the "providers" array of the importing module instead.`;
|
||||
break;
|
||||
case 'filter':
|
||||
hint = `${metatypeNameQuote} is decorated with @Catch() and cannot appear in the "imports" array of a module. Please move ${metatypeNameQuote} to the "providers" array (using the APP_FILTER token to apply it globally) or apply it via @UseFilters() instead.`;
|
||||
break;
|
||||
}
|
||||
|
||||
return `${hint}
|
||||
|
||||
Scope [${stringifyScope(scope)}]
|
||||
`;
|
||||
|
||||
@@ -205,6 +205,9 @@ export class Injector {
|
||||
localResolutionContext,
|
||||
resolutionContext.inquirer,
|
||||
);
|
||||
if (!instanceHost.isResolved) {
|
||||
settlementSignal.complete();
|
||||
}
|
||||
} catch (err) {
|
||||
wrapper.removeInstanceByContextId(
|
||||
this.getContextId(resolutionContext.contextId, wrapper),
|
||||
@@ -311,7 +314,10 @@ export class Injector {
|
||||
) {
|
||||
const metadata = wrapper.getCtorMetadata();
|
||||
|
||||
if (metadata && resolutionContext.contextId !== STATIC_CONTEXT) {
|
||||
if (
|
||||
resolutionContext.contextId !== STATIC_CONTEXT &&
|
||||
this.hasDenseCtorMetadata(wrapper, inject, metadata)
|
||||
) {
|
||||
const deps = await this.loadCtorMetadata(
|
||||
metadata,
|
||||
resolutionContext.contextId,
|
||||
@@ -461,21 +467,21 @@ export class Injector {
|
||||
];
|
||||
}
|
||||
|
||||
public reflectConstructorParams<T>(type: Type<T>): any[] {
|
||||
public reflectConstructorParams(type: Type<unknown> | Function): any[] {
|
||||
const paramtypes = [
|
||||
...(Reflect.getMetadata(PARAMTYPES_METADATA, type) || []),
|
||||
];
|
||||
const selfParams = this.reflectSelfParams<T>(type);
|
||||
const selfParams = this.reflectSelfParams(type);
|
||||
|
||||
selfParams.forEach(({ index, param }) => (paramtypes[index] = param));
|
||||
return Array.from(paramtypes);
|
||||
}
|
||||
|
||||
public reflectOptionalParams<T>(type: Type<T>): any[] {
|
||||
return Reflect.getOwnMetadata(OPTIONAL_DEPS_METADATA, type) || [];
|
||||
public reflectOptionalParams(type: Type<unknown> | Function): any[] {
|
||||
return Reflect.getMetadata(OPTIONAL_DEPS_METADATA, type) || [];
|
||||
}
|
||||
|
||||
public reflectSelfParams<T>(type: Type<T>): any[] {
|
||||
public reflectSelfParams(type: Type<unknown> | Function): any[] {
|
||||
return Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, type) || [];
|
||||
}
|
||||
|
||||
@@ -1043,7 +1049,6 @@ export class Injector {
|
||||
wrapper: InstanceWrapper<Injectable>,
|
||||
resolutionContext: ResolutionContext,
|
||||
): boolean {
|
||||
const isSnapshotGraphCompilation = !!this.options?.snapshot;
|
||||
const isStaticContext = resolutionContext.contextId === STATIC_CONTEXT;
|
||||
const hasNoInquirer = !resolutionContext.inquirer;
|
||||
const isTopLevelStaticTransientOrRequestProvider =
|
||||
@@ -1062,7 +1067,7 @@ export class Injector {
|
||||
(isTopLevelStaticTransientOrRequestProvider ||
|
||||
isStaticInquirerOutsideResolutionContext);
|
||||
|
||||
return !isSnapshotGraphCompilation && shouldSkipForStaticBootstrap;
|
||||
return shouldSkipForStaticBootstrap;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1149,6 +1154,37 @@ export class Injector {
|
||||
);
|
||||
}
|
||||
|
||||
private hasDenseCtorMetadata<T>(
|
||||
wrapper: InstanceWrapper<T>,
|
||||
inject: InjectorDependency[] | undefined,
|
||||
metadata: InstanceWrapper[] | undefined,
|
||||
): boolean {
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The fast path requires a fully populated metadata array.
|
||||
// While another request is still registering dependency metadata,
|
||||
// sparse entries here would feed request-scoped factories `undefined`.
|
||||
const expectedDepsLength = !isNil(inject)
|
||||
? inject.length
|
||||
: wrapper.metatype
|
||||
? this.reflectConstructorParams(wrapper.metatype).length
|
||||
: 0;
|
||||
|
||||
if (metadata.length !== expectedDepsLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < expectedDepsLength; index++) {
|
||||
if (metadata[index] === undefined) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private resolveScopedComponentHost(
|
||||
item: InstanceWrapper,
|
||||
contextId: ContextId,
|
||||
|
||||
@@ -61,6 +61,7 @@ export class NestApplication
|
||||
private readonly microservices: any[] = [];
|
||||
private httpServer: any;
|
||||
private isListening = false;
|
||||
private isWsModuleRegistered = false;
|
||||
|
||||
constructor(
|
||||
container: NestContainer,
|
||||
@@ -169,6 +170,7 @@ export class NestApplication
|
||||
this.appOptions,
|
||||
this.httpServer,
|
||||
);
|
||||
this.isWsModuleRegistered = true;
|
||||
}
|
||||
|
||||
public async init(): Promise<this> {
|
||||
@@ -476,6 +478,11 @@ export class NestApplication
|
||||
}
|
||||
|
||||
public useWebSocketAdapter(adapter: WebSocketAdapter): this {
|
||||
if (this.isWsModuleRegistered) {
|
||||
this.logger.warn(
|
||||
'useWebSocketAdapter() was called after WebSocket gateways were already initialized. The provided adapter will be stored but will NOT be applied to existing gateways — they remain bound to the previously installed adapter. To install a custom adapter, call app.useWebSocketAdapter(...) BEFORE app.init() (or app.listen()).',
|
||||
);
|
||||
}
|
||||
this.config.setIoAdapter(adapter);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/core",
|
||||
"version": "11.1.20",
|
||||
"version": "11.1.23",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@core)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -47,7 +47,7 @@
|
||||
"uid": "2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "11.1.20"
|
||||
"@nestjs/common": "11.1.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
|
||||
@@ -148,7 +148,12 @@ export class DependenciesScanner {
|
||||
throw new UndefinedModuleException(moduleDefinition, index, scope);
|
||||
}
|
||||
if (!innerModule) {
|
||||
throw new InvalidModuleException(moduleDefinition, index, scope);
|
||||
throw new InvalidModuleException(
|
||||
moduleDefinition,
|
||||
index,
|
||||
scope,
|
||||
innerModule,
|
||||
);
|
||||
}
|
||||
if (ctxRegistry.includes(innerModule)) {
|
||||
continue;
|
||||
@@ -186,12 +191,22 @@ export class DependenciesScanner {
|
||||
? moduleDefinition.forwardRef()
|
||||
: moduleDefinition;
|
||||
|
||||
if (
|
||||
this.isInjectable(moduleToAdd) ||
|
||||
this.isController(moduleToAdd) ||
|
||||
this.isExceptionFilter(moduleToAdd)
|
||||
) {
|
||||
throw new InvalidClassModuleException(moduleDefinition, scope);
|
||||
if (this.isInjectable(moduleToAdd)) {
|
||||
throw new InvalidClassModuleException(
|
||||
moduleDefinition,
|
||||
scope,
|
||||
'provider',
|
||||
);
|
||||
}
|
||||
if (this.isController(moduleToAdd)) {
|
||||
throw new InvalidClassModuleException(
|
||||
moduleDefinition,
|
||||
scope,
|
||||
'controller',
|
||||
);
|
||||
}
|
||||
if (this.isExceptionFilter(moduleToAdd)) {
|
||||
throw new InvalidClassModuleException(moduleDefinition, scope, 'filter');
|
||||
}
|
||||
|
||||
return this.container.addModule(moduleToAdd, scope);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
INVALID_MODULE_MESSAGE,
|
||||
UNDEFINED_MODULE_MESSAGE,
|
||||
UNKNOWN_EXPORT_MESSAGE,
|
||||
USING_INVALID_CLASS_AS_A_MODULE_MESSAGE,
|
||||
} from '../../../errors/messages.js';
|
||||
import { Module } from '../../../injector/module.js';
|
||||
import { stringCleaner } from '../../utils/string.cleaner.js';
|
||||
@@ -383,15 +384,116 @@ Scope [AppModule -> CatsModule]`);
|
||||
});
|
||||
|
||||
describe('INVALID_MODULE_MESSAGE', () => {
|
||||
it('should display the module name with the invalid index and scope', () => {
|
||||
it('should display the received `null` value and its type', () => {
|
||||
const expectedMessage =
|
||||
stringCleaner(`Nest cannot create the CatsModule instance.
|
||||
Received an unexpected value at index [0] of the CatsModule "imports" array.
|
||||
The received value \`null\` is of type "null".
|
||||
|
||||
Scope [AppModule -> CatsModule]`);
|
||||
|
||||
const actualMessage = stringCleaner(
|
||||
INVALID_MODULE_MESSAGE(CatsModule, 0, [AppModule, CatsModule]),
|
||||
INVALID_MODULE_MESSAGE(CatsModule, 0, [AppModule, CatsModule], null),
|
||||
);
|
||||
|
||||
expect(actualMessage).to.be.eq(expectedMessage);
|
||||
});
|
||||
|
||||
it('should display the received `false` value and its type', () => {
|
||||
const expectedMessage =
|
||||
stringCleaner(`Nest cannot create the CatsModule instance.
|
||||
Received an unexpected value at index [0] of the CatsModule "imports" array.
|
||||
The received value \`false\` is of type "boolean".
|
||||
|
||||
Scope [AppModule -> CatsModule]`);
|
||||
|
||||
const actualMessage = stringCleaner(
|
||||
INVALID_MODULE_MESSAGE(CatsModule, 0, [AppModule, CatsModule], false),
|
||||
);
|
||||
|
||||
expect(actualMessage).to.be.eq(expectedMessage);
|
||||
});
|
||||
|
||||
it('should display the received `0` value and its type', () => {
|
||||
const expectedMessage =
|
||||
stringCleaner(`Nest cannot create the CatsModule instance.
|
||||
Received an unexpected value at index [0] of the CatsModule "imports" array.
|
||||
The received value \`0\` is of type "number".
|
||||
|
||||
Scope [AppModule -> CatsModule]`);
|
||||
|
||||
const actualMessage = stringCleaner(
|
||||
INVALID_MODULE_MESSAGE(CatsModule, 0, [AppModule, CatsModule], 0),
|
||||
);
|
||||
|
||||
expect(actualMessage).to.be.eq(expectedMessage);
|
||||
});
|
||||
|
||||
it('should display the received empty string value and its type', () => {
|
||||
const expectedMessage =
|
||||
stringCleaner(`Nest cannot create the CatsModule instance.
|
||||
Received an unexpected value at index [0] of the CatsModule "imports" array.
|
||||
The received value \`""\` is of type "string".
|
||||
|
||||
Scope [AppModule -> CatsModule]`);
|
||||
|
||||
const actualMessage = stringCleaner(
|
||||
INVALID_MODULE_MESSAGE(CatsModule, 0, [AppModule, CatsModule], ''),
|
||||
);
|
||||
|
||||
expect(actualMessage).to.be.eq(expectedMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('USING_INVALID_CLASS_AS_A_MODULE_MESSAGE', () => {
|
||||
class FooClass {}
|
||||
|
||||
it('should identify a class decorated with @Controller() and direct it to the "controllers" array', () => {
|
||||
const expectedMessage =
|
||||
stringCleaner(`"FooClass" is decorated with @Controller() and cannot appear in the "imports" array of a module. Please move "FooClass" to the "controllers" array of the importing module instead.
|
||||
|
||||
Scope [AppModule]`);
|
||||
|
||||
const actualMessage = stringCleaner(
|
||||
USING_INVALID_CLASS_AS_A_MODULE_MESSAGE(
|
||||
FooClass,
|
||||
[AppModule],
|
||||
'controller',
|
||||
),
|
||||
);
|
||||
|
||||
expect(actualMessage).to.be.eq(expectedMessage);
|
||||
});
|
||||
|
||||
it('should identify a class decorated with @Injectable() and direct it to the "providers" array', () => {
|
||||
const expectedMessage =
|
||||
stringCleaner(`"FooClass" is decorated with @Injectable() and cannot appear in the "imports" array of a module. Please move "FooClass" to the "providers" array of the importing module instead.
|
||||
|
||||
Scope [AppModule]`);
|
||||
|
||||
const actualMessage = stringCleaner(
|
||||
USING_INVALID_CLASS_AS_A_MODULE_MESSAGE(
|
||||
FooClass,
|
||||
[AppModule],
|
||||
'provider',
|
||||
),
|
||||
);
|
||||
|
||||
expect(actualMessage).to.be.eq(expectedMessage);
|
||||
});
|
||||
|
||||
it('should identify a class decorated with @Catch() and direct it to the "providers" array or @UseFilters()', () => {
|
||||
const expectedMessage =
|
||||
stringCleaner(`"FooClass" is decorated with @Catch() and cannot appear in the "imports" array of a module. Please move "FooClass" to the "providers" array (using the APP_FILTER token to apply it globally) or apply it via @UseFilters() instead.
|
||||
|
||||
Scope [AppModule]`);
|
||||
|
||||
const actualMessage = stringCleaner(
|
||||
USING_INVALID_CLASS_AS_A_MODULE_MESSAGE(
|
||||
FooClass,
|
||||
[AppModule],
|
||||
'filter',
|
||||
),
|
||||
);
|
||||
|
||||
expect(actualMessage).toBe(expectedMessage);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Optional } from '@nestjs/common';
|
||||
import { Optional, Scope } from '@nestjs/common';
|
||||
import { PARAMTYPES_METADATA } from '@nestjs/common/constants.js';
|
||||
import { Inject } from '../../../common/decorators/core/inject.decorator.js';
|
||||
import { Injectable } from '../../../common/decorators/core/injectable.decorator.js';
|
||||
@@ -205,6 +205,33 @@ describe('Injector', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveConstructorParams', () => {
|
||||
it('should fall back to resolving each param when ctor metadata is sparse', async () => {
|
||||
const wrapper = new InstanceWrapper();
|
||||
const metadata = [new InstanceWrapper()];
|
||||
metadata.length = 2;
|
||||
|
||||
const getCtorMetadataSpy = vi
|
||||
.spyOn(wrapper, 'getCtorMetadata')
|
||||
.mockReturnValue(metadata);
|
||||
const loadCtorMetadataSpy = vi.spyOn(injector, 'loadCtorMetadata');
|
||||
await injector
|
||||
.resolveConstructorParams(
|
||||
wrapper,
|
||||
null!,
|
||||
['Provider1', 'Provider2'],
|
||||
() => {},
|
||||
{ contextId: { id: 2 } },
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
expect(loadCtorMetadataSpy).not.toHaveBeenCalled();
|
||||
|
||||
getCtorMetadataSpy.mockRestore();
|
||||
loadCtorMetadataSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadMiddleware', () => {
|
||||
let loadInstanceSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
@@ -294,6 +321,38 @@ describe('Injector', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadProvider', () => {
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
class TransientProvider {}
|
||||
|
||||
it('should not eagerly load a top-level transient provider during snapshot bootstrap', async () => {
|
||||
const snapshotInjector = new Injector({
|
||||
preview: false,
|
||||
snapshot: true,
|
||||
});
|
||||
const loadInstance = vi.spyOn(snapshotInjector, 'loadInstance');
|
||||
const loadEnhancersPerContext = vi.spyOn(
|
||||
snapshotInjector,
|
||||
'loadEnhancersPerContext',
|
||||
);
|
||||
|
||||
const wrapper = new InstanceWrapper({
|
||||
metatype: TransientProvider,
|
||||
instance: null,
|
||||
isResolved: false,
|
||||
scope: Scope.TRANSIENT,
|
||||
});
|
||||
const moduleRef = {
|
||||
providers: new Map([[TransientProvider, wrapper]]),
|
||||
} as any;
|
||||
|
||||
await snapshotInjector.loadProvider(wrapper as any, moduleRef);
|
||||
|
||||
expect(loadInstance).not.toHaveBeenCalled();
|
||||
expect(loadEnhancersPerContext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('lookupComponent', () => {
|
||||
let lookupComponentInImports: ReturnType<typeof vi.fn>;
|
||||
const metatype = { name: 'test', metatype: { name: 'test' } };
|
||||
|
||||
@@ -555,4 +555,44 @@ describe('NestApplicationContext', () => {
|
||||
expect(instances[2]).toBeInstanceOf(Service3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('snapshot bootstrap', () => {
|
||||
it('should not eagerly instantiate an unused transient provider', async () => {
|
||||
@Injectable({ scope: Scope.TRANSIENT })
|
||||
class UnusedTransient {
|
||||
static constructorCalls = 0;
|
||||
|
||||
constructor() {
|
||||
UnusedTransient.constructorCalls++;
|
||||
}
|
||||
}
|
||||
|
||||
const nestContainer = new NestContainer();
|
||||
const injector = new Injector({
|
||||
preview: false,
|
||||
snapshot: true,
|
||||
});
|
||||
const instanceLoader = new InstanceLoader(
|
||||
nestContainer,
|
||||
injector,
|
||||
new GraphInspector(nestContainer),
|
||||
);
|
||||
const { moduleRef } = (await nestContainer.addModule(class T {}, []))!;
|
||||
|
||||
nestContainer.addProvider(UnusedTransient, moduleRef.token);
|
||||
|
||||
const modules = nestContainer.getModules();
|
||||
await instanceLoader.createInstancesOfDependencies(modules);
|
||||
|
||||
expect(UnusedTransient.constructorCalls).to.equal(0);
|
||||
|
||||
const appCtx = new NestApplicationContext(nestContainer, {
|
||||
snapshot: true,
|
||||
});
|
||||
const instance = await appCtx.resolve(UnusedTransient);
|
||||
|
||||
expect(instance).to.be.instanceOf(UnusedTransient);
|
||||
expect(UnusedTransient.constructorCalls).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -151,4 +151,52 @@ describe('NestApplication', () => {
|
||||
expect((noopHttpAdapter as any).init).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
describe('useWebSocketAdapter', () => {
|
||||
function createInstance(): NestApplication {
|
||||
const noopHttpAdapter = new NoopHttpAdapter({});
|
||||
const applicationConfig = new ApplicationConfig();
|
||||
const container = new NestContainer(applicationConfig);
|
||||
container.setHttpAdapter(noopHttpAdapter);
|
||||
return new NestApplication(
|
||||
container,
|
||||
noopHttpAdapter,
|
||||
applicationConfig,
|
||||
new GraphInspector(container),
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
it('should not warn when called before WS module registration', () => {
|
||||
const instance = createInstance();
|
||||
const warnSpy = vi.spyOn((instance as any).logger, 'warn');
|
||||
|
||||
instance.useWebSocketAdapter({} as any);
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should warn when called after WS module registration', () => {
|
||||
const instance = createInstance();
|
||||
(instance as any).socketModule = { register: vi.fn() };
|
||||
instance.registerWsModule();
|
||||
const warnSpy = vi.spyOn((instance as any).logger, 'warn');
|
||||
|
||||
instance.useWebSocketAdapter({} as any);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledOnce();
|
||||
expect(warnSpy.mock.calls[0]?.[0]).toMatch(
|
||||
/useWebSocketAdapter\(\) was called after WebSocket gateways were already initialized/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should still set the adapter on ApplicationConfig even when called too late', () => {
|
||||
const instance = createInstance();
|
||||
instance.registerWsModule();
|
||||
const adapter = { create: () => undefined } as any;
|
||||
|
||||
instance.useWebSocketAdapter(adapter);
|
||||
|
||||
expect((instance as any).config.getIoAdapter()).toBe(adapter);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/microservices",
|
||||
"version": "11.1.20",
|
||||
"version": "11.1.23",
|
||||
"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.20",
|
||||
"@nestjs/core": "11.1.20"
|
||||
"@nestjs/common": "11.1.23",
|
||||
"@nestjs/core": "11.1.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@grpc/grpc-js": "*",
|
||||
|
||||
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/platform-express",
|
||||
"version": "11.1.20",
|
||||
"version": "11.1.23",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@platform-express)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
@@ -32,8 +32,8 @@
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/common": "11.1.20",
|
||||
"@nestjs/core": "11.1.20"
|
||||
"@nestjs/common": "11.1.23",
|
||||
"@nestjs/core": "11.1.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
|
||||
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/platform-fastify",
|
||||
"version": "11.1.20",
|
||||
"version": "11.1.23",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@platform-fastify)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/platform-socket.io",
|
||||
"version": "11.1.20",
|
||||
"version": "11.1.23",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@platform-socket.io)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/platform-ws",
|
||||
"version": "11.1.20",
|
||||
"version": "11.1.23",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@platform-ws)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/testing",
|
||||
"version": "11.1.20",
|
||||
"version": "11.1.23",
|
||||
"description": "Nest - modern, fast, powerful node.js web framework (@testing)",
|
||||
"author": "Kamil Mysliwiec",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -129,6 +129,7 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
<tr>
|
||||
<td align="center" valign="middle"><a href="https://solcellsforetag.se/" target="_blank"><img src="https://nestjs.com/img/logos/solcellsforetag-logo.svg" width="140" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://www.route4me.com/" target="_blank"><img src="https://nestjs.com/img/logos/route4me-logo.svg" width="100" valign="middle" /></a></td>
|
||||
<td align="center" valign="middle"><a href="https://memory2.co/" target="_blank"><img src="https://images.opencollective.com/memory-squared/bbe37f5/avatar/256.png?height=50" width="50" valign="middle" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nestjs/websockets",
|
||||
"version": "11.1.20",
|
||||
"version": "11.1.23",
|
||||
"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.20",
|
||||
"@nestjs/core": "11.1.20"
|
||||
"@nestjs/common": "11.1.23",
|
||||
"@nestjs/core": "11.1.23"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^11.0.0",
|
||||
|
||||
Generated
+12
-12
@@ -223,9 +223,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/common": {
|
||||
"version": "11.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.20.tgz",
|
||||
"integrity": "sha512-uivaQnTwnfnW1cam8rlh9Xpxvvq+9zKs5ywHWtHRHu1CSQ2FnsLvDewP9Ybhyuf6l075BXKgbixxyW9OKZGpmA==",
|
||||
"version": "11.1.23",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.23.tgz",
|
||||
"integrity": "sha512-qKgEqwQXHIVu8TwiISmgbTrGHAFBsseP86KNolBZwAiHQryinJ5FPiDpp0ZJBBryY+WEMnsqaCa4TSxVuQEWug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-type": "21.3.4",
|
||||
@@ -254,9 +254,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/core": {
|
||||
"version": "11.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.20.tgz",
|
||||
"integrity": "sha512-RlaeQctEtSNrc3SxwQDnAxv/R22ww2t5dbP8OMzaghXofUPRCe2dXhXZSlKMkCyCSEWX+tMhESlGRB5rcJhYTQ==",
|
||||
"version": "11.1.23",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.23.tgz",
|
||||
"integrity": "sha512-Yd+mVFUilw4A6PzV7tyfiW+zrG2wmRXnFZVmNQA+fl1N0k6km4bhhNboxjLu//dzl+XiZI5AsOHHOTegzvOgNQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -295,9 +295,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express": {
|
||||
"version": "11.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.20.tgz",
|
||||
"integrity": "sha512-HFJ/bf189AGziCp7o9DPdPEgj7LXvCWswR3hBNiVCIo3SwOwTIss1BsQWCCJVAtVYTmwIHOI0i0k9dMGXh3Hew==",
|
||||
"version": "11.1.23",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.23.tgz",
|
||||
"integrity": "sha512-7TQ9v2Z9lej8btTYK1VUVB3nZr0lXFNFiyI/iOc0jAOg31dHdpAWtnMMm9iUvyAfq/ssw7dsBhjCHOmEJJKr0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cors": "2.8.6",
|
||||
@@ -316,9 +316,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-fastify": {
|
||||
"version": "11.1.20",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-11.1.20.tgz",
|
||||
"integrity": "sha512-2dmSTnGisXEzzZ6NvdVwL43VRIfWe2v22tgyH/pD/6Qb6vj58Q8zp6nhU43uoxu2sgLOxq+9ZdS6izeUFJRcEQ==",
|
||||
"version": "11.1.23",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-11.1.23.tgz",
|
||||
"integrity": "sha512-cLSDwMu2wf0DB8HeUl2M7m7cIZRljaziwFFeXt9elHb863yQh31T8X4tR5I/a3QFXS0b7hJvdJ728G14K1oS2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "11.2.0",
|
||||
|
||||
Reference in new issue
Block a user