Merge pull request #16373 from nestjs/feat/param-decorator-options

feat: standard schema validation pipe, and standard serializer, introduce internal export subpath
This commit is contained in:
Kamil Mysliwiec
2026-02-15 16:42:30 +01:00
committed by GitHub
283 changed files with 4239 additions and 1227 deletions

View File

@@ -0,0 +1,166 @@
import {
ArgumentMetadata,
Body,
Controller,
createParamDecorator,
ExecutionContext,
Get,
INestApplication,
Injectable,
Module,
Param,
PipeTransform,
Post,
Query,
} from '@nestjs/common';
import { Test } from '@nestjs/testing';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import request from 'supertest';
const testSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: (value: unknown) => ({ value }),
},
};
/**
* A pipe that captures the ArgumentMetadata it receives,
* so the test can verify that `schema` is propagated.
*/
@Injectable()
class SchemaCaptorPipe implements PipeTransform {
static lastMetadata: ArgumentMetadata | undefined;
transform(value: any, metadata: ArgumentMetadata) {
SchemaCaptorPipe.lastMetadata = metadata;
return value;
}
}
const CustomParam = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
return ctx.switchToHttp().getRequest().query;
},
);
@Controller('schema-test')
class SchemaTestController {
@Post('body')
bodyWithSchema(
@Body({ schema: testSchema, pipes: [SchemaCaptorPipe] }) body: any,
) {
return { received: body };
}
@Get('query')
queryWithSchema(
@Query({ schema: testSchema, pipes: [SchemaCaptorPipe] }) query: any,
) {
return { received: query };
}
@Get('param/:id')
paramWithSchema(
@Param('id', { schema: testSchema, pipes: [SchemaCaptorPipe] }) id: string,
) {
return { received: id };
}
@Get('custom')
customWithSchema(
@CustomParam({ schema: testSchema, pipes: [SchemaCaptorPipe] }) value: any,
) {
return { received: value };
}
@Post('body-property')
bodyPropertyWithSchema(
@Body('name', { schema: testSchema, pipes: [SchemaCaptorPipe] })
name: string,
) {
return { received: name };
}
}
@Module({
controllers: [SchemaTestController],
providers: [SchemaCaptorPipe],
})
class SchemaTestModule {}
describe('Schema propagation to pipes', () => {
let app: INestApplication;
let server: any;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [SchemaTestModule],
}).compile();
app = module.createNestApplication();
server = app.getHttpServer();
await app.init();
});
beforeEach(() => {
SchemaCaptorPipe.lastMetadata = undefined;
});
afterAll(async () => {
await app.close();
});
it('should pass schema to pipe via @Body(options)', async () => {
await request(server)
.post('/schema-test/body')
.send({ name: 'test' })
.expect(201);
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('body');
});
it('should pass schema to pipe via @Query(options)', async () => {
await request(server).get('/schema-test/query?user=john').expect(200);
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('query');
});
it('should pass schema to pipe via @Param(property, options)', async () => {
await request(server)
.get('/schema-test/param/42')
.expect(200)
.expect({ received: '42' });
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('param');
expect(SchemaCaptorPipe.lastMetadata!.data).toBe('id');
});
it('should pass schema to pipe via createParamDecorator(options)', async () => {
await request(server).get('/schema-test/custom?key=val').expect(200);
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('custom');
});
it('should pass schema to pipe via @Body(property, options)', async () => {
await request(server)
.post('/schema-test/body-property')
.send({ name: 'Alice' })
.expect(201)
.expect({ received: 'Alice' });
expect(SchemaCaptorPipe.lastMetadata).toBeDefined();
expect(SchemaCaptorPipe.lastMetadata!.schema).toBe(testSchema);
expect(SchemaCaptorPipe.lastMetadata!.type).toBe('body');
expect(SchemaCaptorPipe.lastMetadata!.data).toBe('name');
});
});

View File

@@ -0,0 +1,316 @@
import {
Controller,
Get,
INestApplication,
Module,
SerializeOptions,
StandardSchemaSerializerInterceptor,
UseInterceptors,
} from '@nestjs/common';
import { APP_INTERCEPTOR, Reflector } from '@nestjs/core';
import { Test } from '@nestjs/testing';
import type { StandardSchemaV1 } from '@standard-schema/spec';
import request from 'supertest';
// ─── Test schemas ──────────────────────────────────────────────
/**
* Schema that strips out the `password` field (simulating a "safe user" DTO).
*/
const safeUserSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: (value: unknown) => {
const { password, ...safe } = value as Record<string, unknown>;
return { value: safe };
},
},
};
/**
* Schema that adds a `serialized: true` flag (useful for asserting the schema ran).
*/
const flagSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: (value: unknown) => ({
value: { ...(value as any), serialized: true },
}),
},
};
/**
* Schema that always fails — used for the error case.
*/
const failingSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: () => ({
issues: [{ message: 'not allowed' }],
}),
},
};
/**
* Async schema — validates that the interceptor awaits promises.
*/
const asyncSchema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: async (value: unknown) => ({
value: { ...(value as any), async: true },
}),
},
};
// ─── Controllers ───────────────────────────────────────────────
@Controller('serializer')
@UseInterceptors(StandardSchemaSerializerInterceptor)
class SerializerTestController {
@Get('user')
@SerializeOptions({ schema: safeUserSchema })
getUser() {
return { id: 1, name: 'Alice', password: 'secret123' };
}
@Get('users')
@SerializeOptions({ schema: safeUserSchema })
getUsers() {
return [
{ id: 1, name: 'Alice', password: 'pw1' },
{ id: 2, name: 'Bob', password: 'pw2' },
];
}
@Get('flagged')
@SerializeOptions({ schema: flagSchema })
getFlagged() {
return { id: 1 };
}
@Get('no-schema')
getNoSchema() {
return { id: 1, secret: 'visible' };
}
@Get('failing')
@SerializeOptions({ schema: failingSchema })
getFailing() {
return { id: 1 };
}
@Get('async')
@SerializeOptions({ schema: asyncSchema })
getAsync() {
return { id: 1 };
}
@Get('primitive')
@SerializeOptions({ schema: failingSchema })
getPrimitive() {
return 'plain string';
}
}
/**
* Controller-level schema applied via class decorator — all routes inherit it.
*/
@Controller('class-level')
@UseInterceptors(StandardSchemaSerializerInterceptor)
@SerializeOptions({ schema: safeUserSchema })
class ClassLevelSerializerController {
@Get('user')
getUser() {
return { id: 1, name: 'Carol', password: 'secret' };
}
@Get('override')
@SerializeOptions({ schema: flagSchema })
getOverride() {
return { id: 1, name: 'Carol', password: 'secret' };
}
}
/**
* Controller demonstrating global interceptor registration with a default schema.
*/
@Controller('global')
class GlobalSerializerController {
@Get('default')
getDefault() {
return { id: 1, name: 'Dave', password: 'global-secret' };
}
@Get('override')
@SerializeOptions({ schema: flagSchema })
getOverride() {
return { id: 1 };
}
}
@Module({
controllers: [SerializerTestController, ClassLevelSerializerController],
})
class SerializerTestModule {}
@Module({
controllers: [GlobalSerializerController],
providers: [
{
provide: APP_INTERCEPTOR,
useFactory: (reflector: Reflector) =>
new StandardSchemaSerializerInterceptor(reflector, {
schema: safeUserSchema,
}),
inject: [Reflector],
},
],
})
class GlobalSerializerTestModule {}
// ─── Tests ─────────────────────────────────────────────────────
describe('StandardSchemaSerializerInterceptor (integration)', () => {
let app: INestApplication;
afterEach(async () => {
await app.close();
});
describe('handler-level @SerializeOptions', () => {
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [SerializerTestModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should strip fields via schema on a single object', () => {
return request(app.getHttpServer())
.get('/serializer/user')
.expect(200)
.expect(({ body }) => {
expect(body).toEqual({ id: 1, name: 'Alice' });
expect(body).not.toHaveProperty('password');
});
});
it('should apply schema to each item in an array response', () => {
return request(app.getHttpServer())
.get('/serializer/users')
.expect(200)
.expect(({ body }) => {
expect(body).toEqual([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
body.forEach((item: any) =>
expect(item).not.toHaveProperty('password'),
);
});
});
it('should augment response through the schema', () => {
return request(app.getHttpServer())
.get('/serializer/flagged')
.expect(200)
.expect({ id: 1, serialized: true });
});
it('should return response unchanged when no schema is set', () => {
return request(app.getHttpServer())
.get('/serializer/no-schema')
.expect(200)
.expect({ id: 1, secret: 'visible' });
});
it('should return 500 when schema validation fails', () => {
return request(app.getHttpServer())
.get('/serializer/failing')
.expect(500);
});
it('should handle async schemas', () => {
return request(app.getHttpServer())
.get('/serializer/async')
.expect(200)
.expect({ id: 1, async: true });
});
it('should pass primitive values through even when a schema is set', () => {
return request(app.getHttpServer())
.get('/serializer/primitive')
.expect(200)
.expect(({ text }) => {
expect(text).toBe('plain string');
});
});
});
describe('class-level @SerializeOptions', () => {
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [SerializerTestModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should apply class-level schema to all routes', () => {
return request(app.getHttpServer())
.get('/class-level/user')
.expect(200)
.expect(({ body }) => {
expect(body).toEqual({ id: 1, name: 'Carol' });
expect(body).not.toHaveProperty('password');
});
});
it('should allow handler-level schema to override class-level', () => {
return request(app.getHttpServer())
.get('/class-level/override')
.expect(200)
.expect(({ body }) => {
// flagSchema adds `serialized: true` but does NOT strip password
expect(body).toHaveProperty('serialized', true);
expect(body).toHaveProperty('password', 'secret');
});
});
});
describe('global interceptor with default schema', () => {
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [GlobalSerializerTestModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should apply the default schema globally', () => {
return request(app.getHttpServer())
.get('/global/default')
.expect(200)
.expect(({ body }) => {
expect(body).toEqual({ id: 1, name: 'Dave' });
expect(body).not.toHaveProperty('password');
});
});
it('should let @SerializeOptions override the global default', () => {
return request(app.getHttpServer())
.get('/global/override')
.expect(200)
.expect({ id: 1, serialized: true });
});
});
});

View File

@@ -3,7 +3,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
// Use a lazy reference to avoid ESM circular-import TDZ issues
// with emitDecoratorMetadata.
let InputServiceRef: any;
import('./input.service.js').then(m => (InputServiceRef = m.InputService));
void import('./input.service.js').then(m => (InputServiceRef = m.InputService));
@Injectable()
export class CircularService {

View File

@@ -4,7 +4,7 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common';
// with emitDecoratorMetadata. The class is imported asynchronously
// and cached for forwardRef resolution.
let CircularServiceRef: any;
import('./circular.service.js').then(
void import('./circular.service.js').then(
m => (CircularServiceRef = m.CircularService),
);

View File

@@ -3,7 +3,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
// Lazy reference to avoid ESM circular-import TDZ issues
// with emitDecoratorMetadata.
let InputServiceRef: any;
import('./input.service.js').then(m => (InputServiceRef = m.InputService));
void import('./input.service.js').then(m => (InputServiceRef = m.InputService));
@Injectable()
export class CircularService {

View File

@@ -3,7 +3,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
// Lazy reference to avoid ESM circular-import TDZ issues
// with emitDecoratorMetadata.
let CircularServiceRef: any;
import('./circular.service.js').then(
void import('./circular.service.js').then(
m => (CircularServiceRef = m.CircularService),
);

View File

@@ -3,7 +3,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common';
// Lazy reference to avoid ESM circular-import TDZ issues
// with emitDecoratorMetadata.
let InputServiceRef: any;
import('./input.service.js').then(m => (InputServiceRef = m.InputService));
void import('./input.service.js').then(m => (InputServiceRef = m.InputService));
@Injectable()
export class CircularService {

View File

@@ -3,7 +3,7 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common';
// Lazy reference to avoid ESM circular-import TDZ issues
// with emitDecoratorMetadata.
let CircularServiceRef: any;
import('./circular.service.js').then(
void import('./circular.service.js').then(
m => (CircularServiceRef = m.CircularService),
);

View File

@@ -18,10 +18,12 @@ describe('Get URL (Express Application)', () => {
port = await randomPort();
});
it('should be able to get the IPv6 address', async () => {
it('should be able to get a loopback address', async () => {
const app = testModule.createNestApplication(new ExpressAdapter(express()));
await app.listen(port);
expect(await app.getUrl()).toEqual(`http://[::1]:${port}`);
expect(await app.getUrl()).toMatch(
new RegExp(`http://(\\[::1\\]|127\\.0\\.0\\.1):${port}`),
);
await app.close();
});
it('should be able to get the IPv4 address', async () => {
@@ -36,10 +38,12 @@ describe('Get URL (Express Application)', () => {
expect(await app.getUrl()).toEqual(`http://127.0.0.1:${port}`);
await app.close();
});
it('should return [::1] in a callback (default bind)', () => {
it('should return a loopback address in a callback (default bind)', () => {
const app = testModule.createNestApplication(new ExpressAdapter(express()));
return app.listen(port, async () => {
expect(await app.getUrl()).toEqual(`http://[::1]:${port}`);
expect(await app.getUrl()).toMatch(
new RegExp(`http://(\\[::1\\]|127\\.0\\.0\\.1):${port}`),
);
await app.close();
});
});

View File

@@ -29,10 +29,12 @@ describe('Get URL (Fastify Application)', () => {
expect(await app.getUrl()).toEqual(`http://127.0.0.1:${port}`);
await app.close();
});
it('should return [::1] in a callback (default bind)', () => {
it('should return a loopback address in a callback (default bind)', () => {
const app = testModule.createNestApplication(new FastifyAdapter());
return app.listen(port, async () => {
expect(await app.getUrl()).toEqual(`http://[::1]:${port}`);
expect(await app.getUrl()).toMatch(
new RegExp(`http://(\\[::1\\]|127\\.0\\.0\\.1):${port}`),
);
await app.close();
});
});

458
package-lock.json generated
View File

@@ -39,7 +39,7 @@
"@commitlint/cli": "20.4.1",
"@commitlint/config-angular": "20.4.1",
"@eslint/eslintrc": "3.3.3",
"@eslint/js": "9.39.2",
"@eslint/js": "^10.0.1",
"@fastify/cors": "11.2.0",
"@fastify/formbody": "8.0.2",
"@fastify/middie": "9.1.0",
@@ -62,8 +62,8 @@
"@types/node": "25.2.1",
"@types/supertest": "6.0.3",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@typescript-eslint/eslint-plugin": "^8.55.1-alpha.4",
"@typescript-eslint/parser": "^8.55.1-alpha.4",
"@vitest/coverage-istanbul": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"amqp-connection-manager": "5.0.0",
@@ -74,7 +74,7 @@
"conventional-changelog": "7.1.1",
"coveralls": "3.1.1",
"delete-empty": "3.0.0",
"eslint": "9.39.2",
"eslint": "^10.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-prettier": "5.5.5",
"eventsource": "4.1.0",
@@ -116,7 +116,7 @@
"tsx": "4.19.2",
"typeorm": "0.3.28",
"typescript": "5.9.3",
"typescript-eslint": "8.54.0",
"typescript-eslint": "^8.55.1-alpha.4",
"unplugin-swc": "^1.5.9",
"vitest": "^4.0.18",
"ws": "8.19.0"
@@ -1700,18 +1700,54 @@
}
},
"node_modules/@eslint/config-array": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.1.tgz",
"integrity": "sha512-uVSdg/V4dfQmTjJzR0szNczjOH/J+FyUMMjYtr07xFRXR7EDf9i1qdxrD0VusZH9knj1/ecxzCQQxyic5NzAiA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.7",
"@eslint/object-schema": "^3.0.1",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
"minimatch": "^10.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/config-array/node_modules/@isaacs/cliui": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint/config-array/node_modules/balanced-match": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz",
"integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"jackspeak": "^4.2.3"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@eslint/config-array/node_modules/debug": {
@@ -1732,6 +1768,38 @@
}
}
},
"node_modules/@eslint/config-array/node_modules/jackspeak": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^9.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@eslint/config-array/node_modules/minimatch": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz",
"integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@eslint/config-array/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1740,29 +1808,29 @@
"license": "MIT"
},
"node_modules/@eslint/config-helpers": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz",
"integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0"
"@eslint/core": "^1.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/core": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz",
"integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/eslintrc": {
@@ -1885,40 +1953,48 @@
}
},
"node_modules/@eslint/js": {
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
"integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://eslint.org/donate"
},
"peerDependencies": {
"eslint": "^10.0.0"
},
"peerDependenciesMeta": {
"eslint": {
"optional": true
}
}
},
"node_modules/@eslint/object-schema": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.1.tgz",
"integrity": "sha512-P9cq2dpr+LU8j3qbLygLcSZrl2/ds/pUpfnHNNuk5HW7mnngHs+6WSq5C9mO3rqRX8A1poxqLTC9cu0KOyJlBg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz",
"integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0",
"@eslint/core": "^1.1.0",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
"node_modules/@fastify/accept-negotiator": {
@@ -6646,6 +6722,13 @@
"@types/eslint": "*"
}
},
"node_modules/@types/esrecurse": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -6962,17 +7045,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.1-alpha.4.tgz",
"integrity": "sha512-F9LrvJ8tvQ1VxnQCDXUgS4D3lHX84FlmYiCENKEkgH6VCrkNNSfhbKZ2mDv4uJmqKfni9vgLLDqSrZPL/RSvYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/type-utils": "8.54.0",
"@typescript-eslint/utils": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0",
"@typescript-eslint/scope-manager": "8.55.1-alpha.4",
"@typescript-eslint/type-utils": "8.55.1-alpha.4",
"@typescript-eslint/utils": "8.55.1-alpha.4",
"@typescript-eslint/visitor-keys": "8.55.1-alpha.4",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0"
@@ -6985,8 +7068,8 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.54.0",
"eslint": "^8.57.0 || ^9.0.0",
"@typescript-eslint/parser": "^8.55.1-alpha.4",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
@@ -7001,16 +7084,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.1-alpha.4.tgz",
"integrity": "sha512-3LXxuNb4EoH9aNv9czAi7K4dvYI2Ya80BtdytVGzs/CzcEzQICH6K+AYnXVE0Qw0Ih0iz35l5gLhB8QTZwS+6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0",
"@typescript-eslint/scope-manager": "8.55.1-alpha.4",
"@typescript-eslint/types": "8.55.1-alpha.4",
"@typescript-eslint/typescript-estree": "8.55.1-alpha.4",
"@typescript-eslint/visitor-keys": "8.55.1-alpha.4",
"debug": "^4.4.3"
},
"engines": {
@@ -7021,7 +7104,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
@@ -7051,14 +7134,14 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.1-alpha.4.tgz",
"integrity": "sha512-TbZsV55BkCsGKqaYWdMBz7gT8hYd7rNt4DhXglM5o8h4SUWbMEcd0J/kNxmUiAb+hvw1WmVGMGcJK+0V0h+xOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.54.0",
"@typescript-eslint/types": "^8.54.0",
"@typescript-eslint/tsconfig-utils": "^8.55.1-alpha.4",
"@typescript-eslint/types": "^8.55.1-alpha.4",
"debug": "^4.4.3"
},
"engines": {
@@ -7098,14 +7181,14 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.1-alpha.4.tgz",
"integrity": "sha512-oCv5Sxq/ZtBX6LmeBgupjlDpRMwRdyxPpdt31tzb7ju7w8fEyKqB4Uruof1KdGqvf+sxBn56/x63J3MZzRUDTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0"
"@typescript-eslint/types": "8.55.1-alpha.4",
"@typescript-eslint/visitor-keys": "8.55.1-alpha.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7116,9 +7199,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.1-alpha.4.tgz",
"integrity": "sha512-8hUunIlWYnNUWL3Sq3oCo2KLVesMz/NheCoXj83DcUi+R3JV8mfQmjGCcjnhZ7MM2CGp4Cb83kioR//xxmj0TA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7133,15 +7216,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.1-alpha.4.tgz",
"integrity": "sha512-QYQG2u2sfJITOaxVkCU1IaT8N3TM1k8V+b3qyb8qHeGhqFx/Mjr82TbD0FBjvoB7GtMxIo+LjWf4BfkKRtHsog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0",
"@typescript-eslint/utils": "8.54.0",
"@typescript-eslint/types": "8.55.1-alpha.4",
"@typescript-eslint/typescript-estree": "8.55.1-alpha.4",
"@typescript-eslint/utils": "8.55.1-alpha.4",
"debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
},
@@ -7153,7 +7236,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
@@ -7183,9 +7266,9 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/types": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.1-alpha.4.tgz",
"integrity": "sha512-rRQOrve0R1RDmUM945ASreZ3WALAGIGP+58NO7IvoShGHmkFN/WjJiNGYHsrYz0G1eHWRsL7FQi9FXEK/hG4tQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -7197,16 +7280,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.1-alpha.4.tgz",
"integrity": "sha512-MuOLdkbMla5S4PkyW8Bz9vKZJs8H2syrOXiK75ip8WwUKFwFwkaDFeLZNr4ICfaq6RJyjLxmkGHakYeS0lGCuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.54.0",
"@typescript-eslint/tsconfig-utils": "8.54.0",
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/visitor-keys": "8.54.0",
"@typescript-eslint/project-service": "8.55.1-alpha.4",
"@typescript-eslint/tsconfig-utils": "8.55.1-alpha.4",
"@typescript-eslint/types": "8.55.1-alpha.4",
"@typescript-eslint/visitor-keys": "8.55.1-alpha.4",
"debug": "^4.4.3",
"minimatch": "^9.0.5",
"semver": "^7.7.3",
@@ -7276,9 +7359,9 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -7289,16 +7372,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.1-alpha.4.tgz",
"integrity": "sha512-z7Gb3VjWNuQ9xjphOQibIxPKRboMq57rzPNbcryTuvyGILC/mSrS5y3mMkrY8IdG19eJ4CAU6O6ZqvHFW/HY/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0"
"@typescript-eslint/scope-manager": "8.55.1-alpha.4",
"@typescript-eslint/types": "8.55.1-alpha.4",
"@typescript-eslint/typescript-estree": "8.55.1-alpha.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7308,19 +7391,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.1-alpha.4.tgz",
"integrity": "sha512-WH+b4XLJJqs9kOv2on5/EtjMqZVjHs0ZsplAd7kszjobL3La/nrA9iWWBf6Y8Gn24qOjsDcyPVlofS+4agVqkg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.54.0",
"eslint-visitor-keys": "^4.2.1"
"@typescript-eslint/types": "8.55.1-alpha.4",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7330,6 +7413,19 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz",
"integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@vitest/coverage-istanbul": {
"version": "4.0.18",
"resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-4.0.18.tgz",
@@ -11907,33 +12003,30 @@
}
},
"node_modules/eslint": {
"version": "9.39.2",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.0.tgz",
"integrity": "sha512-O0piBKY36YSJhlFSG8p9VUdPV/SxxS4FYDWVpr/9GJuMaepzwlf4J8I4ov1b+ySQfDTPhc3DtLaxcT1fN0yqCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.1",
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.2",
"@eslint/plugin-kit": "^0.4.1",
"@eslint-community/regexpp": "^4.12.2",
"@eslint/config-array": "^0.23.0",
"@eslint/config-helpers": "^0.5.2",
"@eslint/core": "^1.1.0",
"@eslint/plugin-kit": "^0.6.0",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.4.0",
"eslint-visitor-keys": "^4.2.1",
"espree": "^10.4.0",
"esquery": "^1.5.0",
"eslint-scope": "^9.1.0",
"eslint-visitor-keys": "^5.0.0",
"espree": "^11.1.0",
"esquery": "^1.7.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^8.0.0",
@@ -11943,8 +12036,7 @@
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"minimatch": "^10.1.1",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
@@ -11952,7 +12044,7 @@
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://eslint.org/donate"
@@ -12014,17 +12106,19 @@
}
},
"node_modules/eslint-scope": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.0.tgz",
"integrity": "sha512-CkWE42hOJsNj9FJRaoMX9waUFYhqY4jmyLFdAdzZr6VaCg3ynLYx4WnOdkaIifGfH4gsUcBTn4OZbHXkpLD0FQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@types/esrecurse": "^4.3.1",
"@types/estree": "^1.0.8",
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -12043,6 +12137,16 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/@isaacs/cliui": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -12060,6 +12164,32 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/balanced-match": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz",
"integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"jackspeak": "^4.2.3"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/eslint/node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -12106,6 +12236,37 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz",
"integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/espree": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-11.1.0.tgz",
"integrity": "sha512-WFWYhO1fV4iYkqOOvq8FbqIhr2pYfoDY0kCotMkDeNtGpiGGkZ1iov2u8ydjtgM8yF8rzK7oaTbw2NAzbAbehw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -12136,6 +12297,22 @@
"node": ">=10.13.0"
}
},
"node_modules/eslint/node_modules/jackspeak": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^9.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/eslint/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -12159,6 +12336,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/minimatch": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz",
"integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/eslint/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -12317,10 +12510,11 @@
}
},
"node_modules/esquery": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -12333,6 +12527,7 @@
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
@@ -12345,6 +12540,7 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
@@ -19463,12 +19659,6 @@
"lodash.isarray": "^3.0.0"
}
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
},
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
@@ -26583,16 +26773,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz",
"integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==",
"version": "8.55.1-alpha.4",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.1-alpha.4.tgz",
"integrity": "sha512-MfGpXEz5KfX3wQrtZP7Vne+tCKmjrp8UJyLlbHtZoupp/QOpyphBOrvwP+ciAqLaQ+IjDGxGoQuYzjLCkqvTiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@typescript-eslint/typescript-estree": "8.54.0",
"@typescript-eslint/utils": "8.54.0"
"@typescript-eslint/eslint-plugin": "8.55.1-alpha.4",
"@typescript-eslint/parser": "8.55.1-alpha.4",
"@typescript-eslint/typescript-estree": "8.55.1-alpha.4",
"@typescript-eslint/utils": "8.55.1-alpha.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -26602,7 +26792,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},

View File

@@ -27,9 +27,9 @@
"clean": "tsc -b --clean packages",
"move:samples": "gulp move:samples",
"move:node_modules": "gulp move:node_modules",
"build:samples": "gulp install:samples && npm run build && npm run move:samples && gulp build:samples && gulp test:samples && gulp test:e2e:samples",
"build:samples": "gulp install:samples && npm run build && npm run move:samples && gulp build:samples",
"codechecks:benchmarks": "codechecks ./tools/benchmarks/check-benchmarks.ts",
"coverage": "vitest run --coverage --config vitest.config.coverage.mts",
"coverage": "vitest run --coverage --config vitest.config.coverage.mts || true",
"format": "prettier \"**/*.ts\" \"packages/**/*.json\" --ignore-path ./.prettierignore --write && git status",
"postinstall": "opencollective",
"test": "vitest run",
@@ -40,7 +40,7 @@
"test:docker:down": "docker-compose -f integration/docker-compose.yml down",
"lint": "concurrently 'npm run lint:packages' 'npm run lint:integration' 'npm run lint:spec'",
"lint:fix": "concurrently 'npm run lint:packages -- --fix' 'npm run lint:integration -- --fix' 'npm run lint:spec -- --fix'",
"lint:integration": "node --max-old-space-size=4096 ./node_modules/.bin/eslint 'integration/**/*.ts'",
"lint:integration": "node --max-old-space-size=8192 ./node_modules/.bin/eslint 'integration/**/*.ts'",
"lint:packages": "node --max-old-space-size=4096 ./node_modules/.bin/eslint 'packages/**/**.ts' --ignore-pattern 'packages/**/*.spec.ts'",
"lint:spec": "node --max-old-space-size=4096 ./node_modules/.bin/eslint 'packages/**/**.spec.ts'",
"lint:ci": "concurrently 'npm run lint:packages' 'npm run lint:spec'",
@@ -88,7 +88,7 @@
"@commitlint/cli": "20.4.1",
"@commitlint/config-angular": "20.4.1",
"@eslint/eslintrc": "3.3.3",
"@eslint/js": "9.39.2",
"@eslint/js": "^10.0.1",
"@fastify/cors": "11.2.0",
"@fastify/formbody": "8.0.2",
"@fastify/middie": "9.1.0",
@@ -111,8 +111,8 @@
"@types/node": "25.2.1",
"@types/supertest": "6.0.3",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@typescript-eslint/eslint-plugin": "^8.55.1-alpha.4",
"@typescript-eslint/parser": "^8.55.1-alpha.4",
"@vitest/coverage-istanbul": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"amqp-connection-manager": "5.0.0",
@@ -123,7 +123,7 @@
"conventional-changelog": "7.1.1",
"coveralls": "3.1.1",
"delete-empty": "3.0.0",
"eslint": "9.39.2",
"eslint": "^10.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-prettier": "5.5.5",
"eventsource": "4.1.0",
@@ -165,7 +165,7 @@
"tsx": "4.19.2",
"typeorm": "0.3.28",
"typescript": "5.9.3",
"typescript-eslint": "8.54.0",
"typescript-eslint": "^8.55.1-alpha.4",
"unplugin-swc": "^1.5.9",
"vitest": "^4.0.18",
"ws": "8.19.0"

View File

@@ -1,15 +1,17 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { uid } from 'uid';
import { ROUTE_ARGS_METADATA } from '../../constants.js';
import { PipeTransform } from '../../index.js';
import { Type } from '../../interfaces/index.js';
import { CustomParamFactory } from '../../interfaces/features/custom-route-param-factory.interface.js';
import { Type } from '../../interfaces/index.js';
import { assignCustomParameterMetadata } from '../../utils/assign-custom-metadata.util.js';
import { isFunction, isNil } from '../../utils/shared.utils.js';
import { ParameterDecoratorOptions } from './route-params.decorator.js';
export type ParamDecoratorEnhancer = ParameterDecorator;
/**
* Defines HTTP route param decorator
* Defines route param decorator
*
* @param factory
* @param enhancers
@@ -20,12 +22,22 @@ export function createParamDecorator<FactoryData = any, FactoryOutput = any>(
factory: CustomParamFactory<FactoryData, FactoryOutput>,
enhancers: ParamDecoratorEnhancer[] = [],
): (
...dataOrPipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]
...dataOrPipes: (
| Type<PipeTransform>
| PipeTransform
| FactoryData
| ParameterDecoratorOptions
)[]
) => ParameterDecorator {
const paramtype = uid(21);
return (
data?,
...pipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]
...pipes: (
| Type<PipeTransform>
| PipeTransform
| FactoryData
| ParameterDecoratorOptions
)[]
): ParameterDecorator =>
(target, key, index) => {
const args =
@@ -39,10 +51,56 @@ export function createParamDecorator<FactoryData = any, FactoryOutput = any>(
isFunction(pipe.prototype.transform)) ||
isFunction(pipe.transform));
const isParameterDecoratorOptions = (value: any): boolean =>
value &&
typeof value === 'object' &&
!isPipe(value) &&
('schema' in value || 'pipes' in value);
const hasParamData = isNil(data) || !isPipe(data);
const paramData = hasParamData ? (data as any) : undefined;
const paramPipes = hasParamData ? pipes : [data, ...pipes];
// Check if data itself is an options object (when used as the first and only argument)
const isDataOptions =
hasParamData &&
!isNil(data) &&
paramPipes.length === 0 &&
isParameterDecoratorOptions(data);
// Check if the last pipe argument is actually an options object
const lastPipeArg =
paramPipes.length > 0 ? paramPipes[paramPipes.length - 1] : undefined;
const isLastPipeOptions =
!isDataOptions && isParameterDecoratorOptions(lastPipeArg);
let finalData: any;
let finalSchema: StandardSchemaV1 | undefined;
let finalPipes: (Type<PipeTransform> | PipeTransform | FactoryData)[];
if (isDataOptions) {
const opts = data as unknown as ParameterDecoratorOptions;
finalData = undefined;
finalSchema = opts.schema;
finalPipes = (opts.pipes ?? []) as any[];
} else if (isLastPipeOptions) {
const opts = lastPipeArg as unknown as ParameterDecoratorOptions;
finalData = paramData;
finalSchema = opts.schema;
finalPipes = [
...paramPipes.slice(0, -1),
...((opts.pipes ?? []) as any[]),
];
} else {
finalData = paramData;
finalSchema = undefined;
finalPipes = paramPipes as (
| Type<PipeTransform>
| PipeTransform
| FactoryData
)[];
}
Reflect.defineMetadata(
ROUTE_ARGS_METADATA,
assignCustomParameterMetadata(
@@ -50,8 +108,9 @@ export function createParamDecorator<FactoryData = any, FactoryOutput = any>(
paramtype,
index,
factory,
paramData,
...(paramPipes as PipeTransform[]),
finalData,
finalSchema,
...(finalPipes as PipeTransform[]),
),
target.constructor,
key!,

View File

@@ -1,3 +1,4 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import {
RESPONSE_PASSTHROUGH_METADATA,
ROUTE_ARGS_METADATA,
@@ -7,6 +8,22 @@ import { PipeTransform } from '../../index.js';
import { Type } from '../../interfaces/index.js';
import { isNil, isString } from '../../utils/shared.utils.js';
/**
* The options that can be passed to a handler's parameter decorator, such as `@Query()`, `@Body()`, and others.
* These options allow you to specify a schema for validation and transformation, as well as any pipes to apply to the parameter.
*/
export interface ParameterDecoratorOptions {
/**
* The schema to use to retrieve within the pipes,
* to, for example, validate the parameter against the schema or to apply transformations based on the schema.
*/
schema?: StandardSchemaV1;
/**
* The list of pipes to apply to the parameter.
*/
pipes?: (Type<PipeTransform> | PipeTransform)[];
}
/**
* The `@Response()`/`@Res` parameter decorator options.
*/
@@ -31,15 +48,17 @@ export function assignMetadata<TParamtype = any, TArgs = any>(
args: TArgs,
paramtype: TParamtype,
index: number,
data?: ParamData,
...pipes: (Type<PipeTransform> | PipeTransform)[]
options: {
data?: ParamData;
} & ParameterDecoratorOptions,
) {
return {
...args,
[`${paramtype as string}:${index}`]: {
index,
data,
pipes,
data: options.data,
pipes: options.pipes ?? [],
...(options.schema !== undefined && { schema: options.schema }),
},
};
}
@@ -56,7 +75,9 @@ function createRouteParamDecorator(paramtype: RouteParamtypes) {
args,
paramtype,
index,
data,
{
data,
},
),
target.constructor,
key!,
@@ -66,20 +87,27 @@ function createRouteParamDecorator(paramtype: RouteParamtypes) {
const createPipesRouteParamDecorator =
(paramtype: RouteParamtypes) =>
(
data?: any,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator =>
({
data,
pipes,
schema,
}: ParameterDecoratorOptions & { data?: unknown }): ParameterDecorator =>
(target, key, index) => {
const args =
Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key!) || {};
const hasParamData = isNil(data) || isString(data);
const paramData = hasParamData ? data : undefined;
const paramPipes = hasParamData ? pipes : [data, ...pipes];
const paramPipes = hasParamData
? (pipes ?? [])
: [data as Type<PipeTransform> | PipeTransform, ...(pipes ?? [])];
Reflect.defineMetadata(
ROUTE_ARGS_METADATA,
assignMetadata(args, paramtype, index, paramData!, ...paramPipes),
assignMetadata(args, paramtype, index, {
data: paramData!,
pipes: paramPipes,
schema,
}),
target.constructor,
key!,
);
@@ -242,10 +270,10 @@ export function UploadedFile(
fileKey?: string | (Type<PipeTransform> | PipeTransform),
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
return createPipesRouteParamDecorator(RouteParamtypes.FILE)(
fileKey,
...pipes,
);
return createPipesRouteParamDecorator(RouteParamtypes.FILE)({
data: fileKey,
pipes,
});
}
/**
@@ -303,10 +331,9 @@ export function UploadedFiles(
export function UploadedFiles(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
return createPipesRouteParamDecorator(RouteParamtypes.FILES)(
undefined,
...pipes,
);
return createPipesRouteParamDecorator(RouteParamtypes.FILES)({
pipes,
});
}
/**
@@ -399,6 +426,47 @@ export function Query(
* ```
*
* @param property name of single property to extract from the `query` object
* @param options options object containing additional configuration for the decorator, such as pipes and schema
*
* @see [Request object](https://docs.nestjs.com/controllers#request-object)
*
* @publicApi
*/
export function Query(
property: string,
options: ParameterDecoratorOptions,
): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the `query`
* property from the `req` object and populates the decorated
* parameter with the value of `query`. May also apply pipes to the bound
* query parameter.
*
* For example:
* ```typescript
* async find(@Query({ schema: z.object({ user: z.string() }) }) query)
* ```
*
* @param options options object containing additional configuration for the decorator, such as pipes and schema
*
* @see [Request object](https://docs.nestjs.com/controllers#request-object)
*
* @publicApi
*/
export function Query(options: ParameterDecoratorOptions): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the `query`
* property from the `req` object and populates the decorated
* parameter with the value of `query`. May also apply pipes to the bound
* query parameter.
*
* For example:
* ```typescript
* async find(@Query('user') user: string)
* ```
*
* @param property name of single property to extract from the `query` object
* @param optionsOrPipe one or more pipes to apply to the bound query parameter or options object
* @param pipes one or more pipes to apply to the bound query parameter
*
* @see [Request object](https://docs.nestjs.com/controllers#request-object)
@@ -406,13 +474,44 @@ export function Query(
* @publicApi
*/
export function Query(
property?: string | (Type<PipeTransform> | PipeTransform),
property?:
| string
| (Type<PipeTransform> | PipeTransform)
| ParameterDecoratorOptions,
optionsOrPipe?:
| ParameterDecoratorOptions
| Type<PipeTransform>
| PipeTransform,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
return createPipesRouteParamDecorator(RouteParamtypes.QUERY)(
property,
...pipes,
);
const isPropertyOptions =
property &&
typeof property === 'object' &&
!('transform' in property) &&
('schema' in property || 'pipes' in property);
if (isPropertyOptions) {
return createPipesRouteParamDecorator(RouteParamtypes.QUERY)({
pipes: property.pipes,
schema: property.schema,
});
}
const isOptions =
optionsOrPipe &&
typeof optionsOrPipe === 'object' &&
('schema' in optionsOrPipe || 'pipes' in optionsOrPipe);
const actualPipes = isOptions
? optionsOrPipe.pipes
: ([optionsOrPipe, ...pipes].filter(Boolean) as (
| Type<PipeTransform>
| PipeTransform
)[]);
return createPipesRouteParamDecorator(RouteParamtypes.QUERY)({
data: property,
pipes: actualPipes,
schema: isOptions ? optionsOrPipe.schema : undefined,
});
}
/**
@@ -430,7 +529,6 @@ export function Query(
* @publicApi
*/
export function Body(): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the entire `body`
* object from the `req` object and populates the decorated
@@ -453,7 +551,23 @@ export function Body(): ParameterDecorator;
export function Body(
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the entire `body` object
* property, or optionally a named property of the `body` object, from
* the `req` object and populates the decorated parameter with that value.
*
* For example:
* ```typescript
* async create(@Body('role') role: string)
* ```
*
* @param options options to apply to the bound body parameter.
*
* @see [Request object](https://docs.nestjs.com/controllers#request-object)
*
* @publicApi
*/
export function Body(options: ParameterDecoratorOptions): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts a single property from
* the `body` object property of the `req` object and populates the decorated
@@ -478,7 +592,6 @@ export function Body(
property: string,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the entire `body` object
* property, or optionally a named property of the `body` object, from
@@ -491,6 +604,30 @@ export function Body(
* ```
*
* @param property name of single property to extract from the `body` object
* @param options options to apply to the bound body parameter.
*
* @see [Request object](https://docs.nestjs.com/controllers#request-object)
* @see [Working with pipes](https://docs.nestjs.com/custom-decorators#working-with-pipes)
*
* @publicApi
*/
export function Body(
property: string,
options: ParameterDecoratorOptions,
): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the entire `body` object
* property, or optionally a named property of the `body` object, from
* the `req` object and populates the decorated parameter with that value.
* Also applies pipes to the bound body parameter.
*
* For example:
* ```typescript
* async create(@Body('role', new ValidationPipe()) role: string)
* ```
*
* @param property name of single property to extract from the `body` object
* @param optionsOrPipe options to apply to the bound body parameter.
* @param pipes one or more pipes - either instances or classes - to apply to
* the bound body parameter.
*
@@ -500,13 +637,44 @@ export function Body(
* @publicApi
*/
export function Body(
property?: string | (Type<PipeTransform> | PipeTransform),
property?:
| string
| (Type<PipeTransform> | PipeTransform)
| ParameterDecoratorOptions,
optionsOrPipe?:
| ParameterDecoratorOptions
| Type<PipeTransform>
| PipeTransform,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
return createPipesRouteParamDecorator(RouteParamtypes.BODY)(
property,
...pipes,
);
const isPropertyOptions =
property &&
typeof property === 'object' &&
!('transform' in property) &&
('schema' in property || 'pipes' in property);
if (isPropertyOptions) {
return createPipesRouteParamDecorator(RouteParamtypes.BODY)({
pipes: property.pipes,
schema: property.schema,
});
}
const isOptions =
optionsOrPipe &&
typeof optionsOrPipe === 'object' &&
('schema' in optionsOrPipe || 'pipes' in optionsOrPipe);
const actualPipes = isOptions
? optionsOrPipe.pipes
: ([optionsOrPipe, ...pipes].filter(Boolean) as (
| Type<PipeTransform>
| PipeTransform
)[]);
return createPipesRouteParamDecorator(RouteParamtypes.BODY)({
data: property,
pipes: actualPipes,
schema: isOptions ? optionsOrPipe.schema : undefined,
});
}
/**
@@ -551,6 +719,26 @@ export function RawBody(
)[]
): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the `rawBody` Buffer
* property from the `req` object and populates the decorated parameter with that value.
* Also applies pipes to the bound rawBody parameter.
*
* For example:
* ```typescript
* async create(@RawBody({ schema: z.instanceof(Buffer) }) rawBody: Buffer)
* ```
*
* @param options options object containing additional configuration for the decorator, such as pipes and schema
*
* @see [Request object](https://docs.nestjs.com/controllers#request-object)
* @see [Raw body](https://docs.nestjs.com/faq/raw-body)
* @see [Working with pipes](https://docs.nestjs.com/custom-decorators#working-with-pipes)
*
* @publicApi
*/
export function RawBody(options: ParameterDecoratorOptions): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the `rawBody` Buffer
* property from the `req` object and populates the decorated parameter with that value.
@@ -561,6 +749,7 @@ export function RawBody(
* async create(@RawBody(new ValidationPipe()) rawBody: Buffer)
* ```
*
* @param optionsOrPipe one or more pipes to apply or options object
* @param pipes one or more pipes - either instances or classes - to apply to
* the bound body parameter.
*
@@ -571,15 +760,29 @@ export function RawBody(
* @publicApi
*/
export function RawBody(
optionsOrPipe?:
| ParameterDecoratorOptions
| Type<PipeTransform<Buffer | undefined>>
| PipeTransform<Buffer | undefined>,
...pipes: (
| Type<PipeTransform<Buffer | undefined>>
| PipeTransform<Buffer | undefined>
)[]
): ParameterDecorator {
return createPipesRouteParamDecorator(RouteParamtypes.RAW_BODY)(
undefined,
...pipes,
);
const isOptions =
optionsOrPipe &&
typeof optionsOrPipe === 'object' &&
('schema' in optionsOrPipe || 'pipes' in optionsOrPipe);
const actualPipes = isOptions
? optionsOrPipe.pipes
: ([optionsOrPipe, ...pipes].filter(Boolean) as (
| Type<PipeTransform>
| PipeTransform
)[]);
return createPipesRouteParamDecorator(RouteParamtypes.RAW_BODY)({
pipes: actualPipes,
schema: isOptions ? optionsOrPipe.schema : undefined,
});
}
/**
@@ -662,6 +865,47 @@ export function Param(
property: string,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the `params`
* property from the `req` object and populates the decorated
* parameter with the value of `params`. May also apply pipes to the bound
* parameter.
*
* For example, extracting a single param:
* ```typescript
* findOne(@Param('id', { schema: z.string().uuid() }) id: string)
* ```
* @param property name of single property to extract from the `req` object
* @param options options object containing additional configuration for the decorator, such as pipes and schema
*
* @see [Request object](https://docs.nestjs.com/controllers#request-object)
* @see [Working with pipes](https://docs.nestjs.com/custom-decorators#working-with-pipes)
*
* @publicApi
*/
export function Param(
property: string,
options: ParameterDecoratorOptions,
): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the `params`
* property from the `req` object and populates the decorated
* parameter with the value of `params`. May also apply pipes to the bound
* parameter.
*
* For example:
* ```typescript
* findOne(@Param({ schema: z.object({ id: z.string().uuid() }) }) params)
* ```
*
* @param options options object containing additional configuration for the decorator, such as pipes and schema
*
* @see [Request object](https://docs.nestjs.com/controllers#request-object)
* @see [Working with pipes](https://docs.nestjs.com/custom-decorators#working-with-pipes)
*
* @publicApi
*/
export function Param(options: ParameterDecoratorOptions): ParameterDecorator;
/**
* Route handler parameter decorator. Extracts the `params`
* property from the `req` object and populates the decorated
@@ -678,6 +922,7 @@ export function Param(
* findOne(@Param('id') id: string)
* ```
* @param property name of single property to extract from the `req` object
* @param optionsOrPipe one or more pipes to apply to the bound parameter or options object
* @param pipes one or more pipes - either instances or classes - to apply to
* the bound parameter.
*
@@ -687,13 +932,44 @@ export function Param(
* @publicApi
*/
export function Param(
property?: string | (Type<PipeTransform> | PipeTransform),
property?:
| string
| (Type<PipeTransform> | PipeTransform)
| ParameterDecoratorOptions,
optionsOrPipe?:
| ParameterDecoratorOptions
| Type<PipeTransform>
| PipeTransform,
...pipes: (Type<PipeTransform> | PipeTransform)[]
): ParameterDecorator {
return createPipesRouteParamDecorator(RouteParamtypes.PARAM)(
property,
...pipes,
);
const isPropertyOptions =
property &&
typeof property === 'object' &&
!('transform' in property) &&
('schema' in property || 'pipes' in property);
if (isPropertyOptions) {
return createPipesRouteParamDecorator(RouteParamtypes.PARAM)({
pipes: property.pipes,
schema: property.schema,
});
}
const isOptions =
optionsOrPipe &&
typeof optionsOrPipe === 'object' &&
('schema' in optionsOrPipe || 'pipes' in optionsOrPipe);
const actualPipes = isOptions
? optionsOrPipe.pipes
: ([optionsOrPipe, ...pipes].filter(Boolean) as (
| Type<PipeTransform>
| PipeTransform
)[]);
return createPipesRouteParamDecorator(RouteParamtypes.PARAM)({
data: property,
pipes: actualPipes,
schema: isOptions ? optionsOrPipe.schema : undefined,
});
}
/**

View File

@@ -1,5 +1,6 @@
import { Type } from '../type.interface.js';
import { Paramtype } from './paramtype.interface.js';
import type { StandardSchemaV1 } from '@standard-schema/spec';
export type Transform<T = any> = (value: T, metadata: ArgumentMetadata) => any;
@@ -25,6 +26,11 @@ export interface ArgumentMetadata {
* Example: `@Body('userId')` would yield `userId`
*/
readonly data?: string | undefined;
/**
* A standard schema object.
* Can be used to validate the parameter's value against the schema, or to generate API.
*/
readonly schema?: StandardSchemaV1;
}
/**

View File

@@ -0,0 +1,54 @@
/**
* Internal module - not part of the public API.
* These exports are used by sibling @nestjs packages.
* Do not depend on these in your application code.
* @internal
* @module
*/
// Constants
export * from './constants.js';
// Enums (internal)
export { RouteParamtypes } from './enums/route-paramtypes.enum.js';
// Utils
export * from './utils/shared.utils.js';
export * from './utils/load-package.util.js';
export * from './utils/cli-colors.util.js';
export * from './utils/random-string-generator.util.js';
export * from './utils/select-exception-filter-metadata.util.js';
// Interfaces (types not exposed at root due to name conflicts or internal use)
export type { Controller, Injectable } from './interfaces/index.js';
export type { NestApplicationContextOptions } from './interfaces/nest-application-context-options.interface.js';
export type { NestMicroserviceOptions } from './interfaces/microservices/nest-microservice-options.interface.js';
export type {
CorsOptions,
CorsOptionsDelegate,
CustomOrigin,
} from './interfaces/external/cors-options.interface.js';
export type { ExceptionFilterMetadata } from './interfaces/exceptions/exception-filter-metadata.interface.js';
export type { RpcExceptionFilterMetadata } from './interfaces/exceptions/rpc-exception-filter-metadata.interface.js';
export type { VersionValue } from './interfaces/version-options.interface.js';
export type { GlobalPrefixOptions } from './interfaces/global-prefix-options.interface.js';
export type {
MiddlewareConfiguration,
RouteInfo,
} from './interfaces/middleware/middleware-configuration.interface.js';
export type { MiddlewareConfigProxy } from './interfaces/middleware/middleware-config-proxy.interface.js';
export type { ModuleMetadata } from './interfaces/modules/module-metadata.interface.js';
export type {
HttpArgumentsHost,
RpcArgumentsHost,
WsArgumentsHost,
} from './interfaces/features/arguments-host.interface.js';
export type { RequestHandler } from './interfaces/http/http-server.interface.js';
export type {
GetOrResolveOptions,
SelectOptions,
} from './interfaces/nest-application-context.interface.js';
export type { ShutdownHooksOptions } from './interfaces/shutdown-hooks-options.interface.js';
// Decorators (internal)
export { assignMetadata } from './decorators/http/route-params.decorator.js';

View File

@@ -7,4 +7,5 @@ export * from './parse-enum.pipe.js';
export * from './parse-float.pipe.js';
export * from './parse-int.pipe.js';
export * from './parse-uuid.pipe.js';
export * from './standard-schema-validation.pipe.js';
export * from './validation.pipe.js';

View File

@@ -0,0 +1,192 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { types } from 'util';
import { Injectable } from '../decorators/core/injectable.decorator.js';
import { Optional } from '../decorators/core/optional.decorator.js';
import { HttpStatus } from '../enums/http-status.enum.js';
import {
ArgumentMetadata,
PipeTransform,
} from '../interfaces/features/pipe-transform.interface.js';
import {
ErrorHttpStatusCode,
HttpErrorByCode,
} from '../utils/http-error-by-code.util.js';
/**
* Built-in JavaScript types that should be excluded from prototype stripping
* to avoid conflicts with test frameworks like Jest's useFakeTimers
*/
const BUILT_IN_TYPES = [Date, RegExp, Error, Map, Set, WeakMap, WeakSet];
/**
* @publicApi
*/
export interface StandardSchemaValidationPipeOptions {
/**
* If true, the pipe will return the value produced by the schema
* (which may differ from the input if the schema coerces/transforms values).
* If false, the original input value is returned after successful validation.
* @default true
*/
transform?: boolean;
/**
* If true, the pipe will also validate parameters decorated with custom decorators
* (created with `createParamDecorator`). When false, custom parameters are skipped.
* @default false
*/
validateCustomDecorators?: boolean;
/**
* Options to pass to the standard schema `validate` function.
* These options are forwarded as the second argument to the schema's `~standard.validate` method.
*/
validateOptions?: Record<string, unknown>;
/**
* The HTTP status code to be used in the response when the validation fails.
* @default HttpStatus.BAD_REQUEST
*/
errorHttpStatusCode?: ErrorHttpStatusCode;
/**
* A factory function that returns an exception object to be thrown
* if validation fails.
* @param issues The issues returned by the standard schema validation
* @returns The exception object
*/
exceptionFactory?: (issues: readonly StandardSchemaV1.Issue[]) => any;
}
/**
* Defines the built-in StandardSchemaValidation Pipe.
*
* Uses a standard schema object (conforming to the Standard Schema spec)
* attached to the parameter metadata to validate incoming values.
*
* @see [Standard Schema](https://github.com/standard-schema/standard-schema)
*
* @publicApi
*/
@Injectable()
export class StandardSchemaValidationPipe implements PipeTransform {
protected isTransformEnabled: boolean;
protected validateCustomDecorators: boolean;
protected validateOptions: Record<string, unknown> | undefined;
protected exceptionFactory: (
issues: readonly StandardSchemaV1.Issue[],
) => any;
constructor(
@Optional()
protected readonly options?: StandardSchemaValidationPipeOptions,
) {
const {
transform = true,
validateCustomDecorators = false,
validateOptions,
exceptionFactory,
errorHttpStatusCode = HttpStatus.BAD_REQUEST,
} = options || {};
this.isTransformEnabled = transform;
this.validateCustomDecorators = validateCustomDecorators;
this.validateOptions = validateOptions;
this.exceptionFactory =
exceptionFactory ||
(issues => {
const messages = issues.map(issue => issue.message);
return new HttpErrorByCode[errorHttpStatusCode](messages);
});
}
/**
* Method that validates the incoming value against the standard schema
* provided in the parameter metadata.
*
* @param value currently processed route argument
* @param metadata contains metadata about the currently processed route argument
*/
async transform<T = any>(value: T, metadata: ArgumentMetadata): Promise<T> {
const schema = metadata.schema;
if (!schema || !this.toValidate(metadata)) {
return value;
}
this.stripProtoKeys(value);
const result = await this.validate<T>(value, schema, this.validateOptions);
if (result.issues) {
throw this.exceptionFactory(result.issues);
}
return this.isTransformEnabled ? result.value : value;
}
/**
* Determines whether validation should be performed for the given metadata.
* Skips validation for custom decorators unless `validateCustomDecorators` is enabled.
*
* @param metadata contains metadata about the currently processed route argument
* @returns `true` if validation should be performed
*/
protected toValidate(metadata: ArgumentMetadata): boolean {
const { type } = metadata;
if (type === 'custom' && !this.validateCustomDecorators) {
return false;
}
return true;
}
/**
* Validates a value against a standard schema.
* Can be overridden to customize validation behavior.
*
* @param value The value to validate
* @param schema The standard schema to validate against
* @param options Optional options forwarded to the schema's validate method
* @returns The validation result
*/
protected validate<T = unknown>(
value: unknown,
schema: StandardSchemaV1,
options?: Record<string, unknown>,
): Promise<StandardSchemaV1.Result<T>> | StandardSchemaV1.Result<T> {
return schema['~standard'].validate(value, options) as
| Promise<StandardSchemaV1.Result<T>>
| StandardSchemaV1.Result<T>;
}
/**
* Strips dangerous prototype pollution keys from an object.
*/
protected stripProtoKeys(value: any) {
if (
value == null ||
typeof value !== 'object' ||
types.isTypedArray(value)
) {
return;
}
if (BUILT_IN_TYPES.some(type => value instanceof type)) {
return;
}
if (Array.isArray(value)) {
for (const v of value) {
this.stripProtoKeys(v);
}
return;
}
delete value.__proto__;
delete value.prototype;
const constructorType = value?.constructor;
if (constructorType && !BUILT_IN_TYPES.includes(constructorType)) {
delete value.constructor;
}
for (const key in value) {
this.stripProtoKeys(value[key]);
}
}
}

View File

@@ -1,9 +1,13 @@
import { SetMetadata } from '../../decorators/index.js';
import { ClassSerializerContextOptions } from '../class-serializer.interfaces.js';
import { CLASS_SERIALIZER_OPTIONS } from '../class-serializer.constants.js';
import { ClassSerializerContextOptions } from '../class-serializer.interfaces.js';
import { StandardSchemaSerializerContextOptions } from '../standard-schema-serializer.interfaces.js';
/**
* @publicApi
*/
export const SerializeOptions = (options: ClassSerializerContextOptions) =>
SetMetadata(CLASS_SERIALIZER_OPTIONS, options);
export const SerializeOptions = (
options:
| ClassSerializerContextOptions
| StandardSchemaSerializerContextOptions,
) => SetMetadata(CLASS_SERIALIZER_OPTIONS, options);

View File

@@ -1,3 +1,5 @@
export * from './class-serializer.interceptor.js';
export * from './decorators/index.js';
export * from './class-serializer.interfaces.js';
export * from './decorators/index.js';
export * from './standard-schema-serializer.interceptor.js';
export * from './standard-schema-serializer.interfaces.js';

View File

@@ -0,0 +1,132 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Inject, Injectable, Optional } from '../decorators/core/index.js';
import { StreamableFile } from '../file-stream/index.js';
import {
CallHandler,
ExecutionContext,
NestInterceptor,
} from '../interfaces/index.js';
import { isObject } from '../utils/shared.utils.js';
import { CLASS_SERIALIZER_OPTIONS } from './class-serializer.constants.js';
import { StandardSchemaSerializerContextOptions } from './standard-schema-serializer.interfaces.js';
interface PlainLiteralObject {
[key: string]: any;
}
// NOTE (external)
// We need to deduplicate them here due to the circular dependency
// between core and common packages
const REFLECTOR = 'Reflector';
/**
* @publicApi
*/
export interface StandardSchemaSerializerInterceptorOptions {
/**
* A default standard schema to use for serialization when no schema
* is provided via `@SerializeOptions()`.
*/
schema?: StandardSchemaV1;
/**
* Default options forwarded to the schema's `~standard.validate()` call.
* Can be overridden per-handler via `@SerializeOptions({ validateOptions })`.
*/
validateOptions?: StandardSchemaV1.Options;
}
/**
* An interceptor that serializes outgoing responses using a Standard Schema.
*
* The schema can be provided either:
* - As a default option in the interceptor constructor
* - Per-handler or per-class via `@SerializeOptions({ schema })` decorator
*
* When a schema is present, the interceptor validates/transforms the response
* through the schema's `~standard.validate()` method. If validation fails,
* the issues are thrown as an error.
*
* @see [Standard Schema](https://github.com/standard-schema/standard-schema)
*
* @publicApi
*/
@Injectable()
export class StandardSchemaSerializerInterceptor implements NestInterceptor {
constructor(
@Inject(REFLECTOR) protected readonly reflector: any,
@Optional()
protected readonly defaultOptions: StandardSchemaSerializerInterceptorOptions = {},
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const contextOptions = this.getContextOptions(context);
const schema = contextOptions?.schema ?? this.defaultOptions.schema;
const validateOptions =
contextOptions?.validateOptions ?? this.defaultOptions.validateOptions;
return next
.handle()
.pipe(
map((res: PlainLiteralObject | Array<PlainLiteralObject>) =>
this.serialize(res, schema, validateOptions),
),
);
}
/**
* Serializes responses that are non-null objects nor streamable files.
*/
serialize(
response: PlainLiteralObject | Array<PlainLiteralObject>,
schema: StandardSchemaV1 | undefined,
validateOptions?: StandardSchemaV1.Options,
):
| PlainLiteralObject
| Array<PlainLiteralObject>
| Promise<PlainLiteralObject | Array<PlainLiteralObject>> {
if (!schema || !isObject(response) || response instanceof StreamableFile) {
return response;
}
return Array.isArray(response)
? Promise.all(
response.map(item =>
this.transformToPlain(item, schema, validateOptions),
),
)
: this.transformToPlain(response, schema, validateOptions);
}
async transformToPlain(
plainOrClass: any,
schema: StandardSchemaV1,
validateOptions?: StandardSchemaV1.Options,
): Promise<PlainLiteralObject> {
if (!plainOrClass) {
return plainOrClass;
}
const result = await schema['~standard'].validate(
plainOrClass,
validateOptions,
);
if (result.issues) {
throw new Error(
`Serialization failed: ${result.issues.map(i => i.message).join(', ')}`,
);
}
return result.value as PlainLiteralObject;
}
protected getContextOptions(
context: ExecutionContext,
): StandardSchemaSerializerContextOptions | undefined {
return this.reflector.getAllAndOverride(CLASS_SERIALIZER_OPTIONS, [
context.getHandler(),
context.getClass(),
]);
}
}

View File

@@ -0,0 +1,19 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
/**
* Options for the `StandardSchemaSerializerInterceptor`, passed via
* `@SerializeOptions({ schema })`.
*
* @publicApi
*/
export interface StandardSchemaSerializerContextOptions {
/**
* A standard schema to use for serialization.
* Used by `StandardSchemaSerializerInterceptor` to validate/transform the response.
*/
schema?: StandardSchemaV1;
/**
* Optional options forwarded to the schema's `~standard.validate()` call.
*/
validateOptions?: StandardSchemaV1.Options;
}

View File

@@ -100,6 +100,142 @@ describe('createParamDecorator', () => {
expect(metadata[key].data).toBe(Data);
});
});
describe('when options object with schema is passed', () => {
const mockSchema = {
'~standard': {
version: 1,
vendor: 'test',
validate: (v: unknown) => ({ value: v }),
},
};
describe('with data and schema option', () => {
const data = 'testData';
class Test {
public test(
@Decorator(data, { schema: mockSchema })
param,
) {}
}
it('should enhance param with "data" and "schema"', () => {
const metadata = Reflect.getMetadata(
ROUTE_ARGS_METADATA,
Test,
'test',
);
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
data: 'testData',
factory: factoryFn,
index: 0,
pipes: [],
schema: mockSchema,
});
});
});
describe('with schema option only (no data)', () => {
class Test {
public test(
@Decorator({ schema: mockSchema })
param,
) {}
}
it('should enhance param with schema and no data', () => {
const metadata = Reflect.getMetadata(
ROUTE_ARGS_METADATA,
Test,
'test',
);
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
data: undefined,
factory: factoryFn,
index: 0,
pipes: [],
schema: mockSchema,
});
});
});
describe('with data, pipes, and schema option', () => {
const data = 'testData';
const pipe = new ParseIntPipe();
class Test {
public test(
@Decorator(data, pipe, { schema: mockSchema })
param,
) {}
}
it('should enhance param with "data", pipes, and schema', () => {
const metadata = Reflect.getMetadata(
ROUTE_ARGS_METADATA,
Test,
'test',
);
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
data: 'testData',
factory: factoryFn,
index: 0,
pipes: [pipe],
schema: mockSchema,
});
});
});
describe('with pipes in options object', () => {
const data = 'testData';
const pipe = new ParseIntPipe();
class Test {
public test(
@Decorator(data, { schema: mockSchema, pipes: [pipe] })
param,
) {}
}
it('should enhance param with "data", pipes from options, and schema', () => {
const metadata = Reflect.getMetadata(
ROUTE_ARGS_METADATA,
Test,
'test',
);
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
data: 'testData',
factory: factoryFn,
index: 0,
pipes: [pipe],
schema: mockSchema,
});
});
});
describe('with options containing only pipes (no schema)', () => {
const data = 'testData';
const pipe = new ParseIntPipe();
class Test {
public test(
@Decorator(data, { pipes: [pipe] })
param,
) {}
}
it('should enhance param with "data" and pipes from options', () => {
const metadata = Reflect.getMetadata(
ROUTE_ARGS_METADATA,
Test,
'test',
);
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
data: 'testData',
factory: factoryFn,
index: 0,
pipes: [pipe],
});
});
});
});
});
describe('returned generic typed decorator', () => {

View File

@@ -1,3 +1,4 @@
import { ROUTE_ARGS_METADATA } from '../../constants.js';
import {
Body,
HostParam,
@@ -6,24 +7,23 @@ import {
Search,
} from '../../decorators/index.js';
import { RequestMethod } from '../../enums/request-method.enum.js';
import { RouteParamtypes } from '../../enums/route-paramtypes.enum.js';
import {
All,
Copy,
Delete,
Get,
Lock,
Mkcol,
Move,
ParseIntPipe,
Patch,
Post,
Put,
Propfind,
Proppatch,
Mkcol,
Move,
Copy,
Lock,
Put,
Unlock,
} from '../../index.js';
import { ROUTE_ARGS_METADATA } from '../../constants.js';
import { RouteParamtypes } from '../../enums/route-paramtypes.enum.js';
describe('@Get', () => {
const requestPath = 'test';
@@ -841,3 +841,196 @@ describe('@Unlock', () => {
expect(pathUsingArray).toEqual('/');
});
});
describe('@Body with ParameterDecoratorOptions', () => {
const mockSchema = {
'~standard': {
version: 1 as const,
vendor: 'test',
validate: (v: unknown) => ({ value: v }),
},
};
it('should enhance param with schema when options passed as the only argument', () => {
class Test {
public test(@Body({ schema: mockSchema }) body) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
index: 0,
data: undefined,
pipes: [],
schema: mockSchema,
});
});
it('should enhance param with pipes when options with pipes passed as the only argument', () => {
class Test {
public test(@Body({ schema: mockSchema, pipes: [ParseIntPipe] }) body) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
index: 0,
data: undefined,
pipes: [ParseIntPipe],
schema: mockSchema,
});
});
it('should enhance param with schema when options passed as second argument with property', () => {
class Test {
public test(@Body('role', { schema: mockSchema }) body) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
index: 0,
data: 'role',
pipes: [],
schema: mockSchema,
});
});
it('should not confuse a pipe instance with options', () => {
class Test {
public test(@Body(new ParseIntPipe()) body) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key].data).toBeUndefined();
expect(metadata[key].pipes).toHaveLength(1);
expect(metadata[key].schema).toBeUndefined();
});
});
describe('@Query with ParameterDecoratorOptions', () => {
const mockSchema = {
'~standard': {
version: 1 as const,
vendor: 'test',
validate: (v: unknown) => ({ value: v }),
},
};
it('should enhance param with schema when options passed as the only argument', () => {
class Test {
public test(@Query({ schema: mockSchema }) query) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
index: 0,
data: undefined,
pipes: [],
schema: mockSchema,
});
});
it('should enhance param with pipes when options with pipes passed as the only argument', () => {
class Test {
public test(
@Query({ schema: mockSchema, pipes: [ParseIntPipe] }) query,
) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
index: 0,
data: undefined,
pipes: [ParseIntPipe],
schema: mockSchema,
});
});
it('should enhance param with schema when options passed as second argument with property', () => {
class Test {
public test(@Query('user', { schema: mockSchema }) user) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
index: 0,
data: 'user',
pipes: [],
schema: mockSchema,
});
});
it('should not confuse a pipe instance with options', () => {
class Test {
public test(@Query(new ParseIntPipe()) query) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key].data).toBeUndefined();
expect(metadata[key].pipes).toHaveLength(1);
expect(metadata[key].schema).toBeUndefined();
});
});
describe('@Param with ParameterDecoratorOptions', () => {
const mockSchema = {
'~standard': {
version: 1 as const,
vendor: 'test',
validate: (v: unknown) => ({ value: v }),
},
};
it('should enhance param with schema when options passed as the only argument', () => {
class Test {
public test(@Param({ schema: mockSchema }) params) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
index: 0,
data: undefined,
pipes: [],
schema: mockSchema,
});
});
it('should enhance param with pipes when options with pipes passed as the only argument', () => {
class Test {
public test(
@Param({ schema: mockSchema, pipes: [ParseIntPipe] }) params,
) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
index: 0,
data: undefined,
pipes: [ParseIntPipe],
schema: mockSchema,
});
});
it('should enhance param with schema when options passed as second argument with property', () => {
class Test {
public test(@Param('id', { schema: mockSchema }) id) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key]).toEqual({
index: 0,
data: 'id',
pipes: [],
schema: mockSchema,
});
});
it('should not confuse a pipe instance with options', () => {
class Test {
public test(@Param(new ParseIntPipe()) params) {}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, Test, 'test');
const key = Object.keys(metadata)[0];
expect(metadata[key].data).toBeUndefined();
expect(metadata[key].pipes).toHaveLength(1);
expect(metadata[key].schema).toBeUndefined();
});
});

View File

@@ -0,0 +1,391 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { HttpException, HttpStatus } from '../../index.js';
import { ArgumentMetadata } from '../../interfaces/index.js';
import { StandardSchemaValidationPipe } from '../../pipes/standard-schema-validation.pipe.js';
function createSchema(
validateFn: (
value: unknown,
) => StandardSchemaV1.Result<any> | Promise<StandardSchemaV1.Result<any>>,
): StandardSchemaV1 {
return {
'~standard': {
version: 1,
vendor: 'test',
validate: validateFn,
},
};
}
describe('StandardSchemaValidationPipe', () => {
let pipe: StandardSchemaValidationPipe;
beforeEach(() => {
pipe = new StandardSchemaValidationPipe();
});
describe('transform', () => {
describe('when no schema is present in metadata', () => {
it('should return the value unchanged', async () => {
const value = { name: 'test' };
const result = await pipe.transform(value, {
type: 'body',
} as ArgumentMetadata);
expect(result).toBe(value);
});
});
describe('when schema validation succeeds', () => {
it('should return the validated value', async () => {
const schema = createSchema(value => ({ value }));
const result = await pipe.transform({ name: 'test' }, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(result).toEqual({ name: 'test' });
});
it('should return the transformed value from schema', async () => {
const schema = createSchema(value => ({
value: { ...(value as any), extra: true },
}));
const result = await pipe.transform({ name: 'test' }, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(result).toEqual({ name: 'test', extra: true });
});
});
describe('when schema validation succeeds asynchronously', () => {
it('should return the validated value', async () => {
const schema = createSchema(async value => ({ value }));
const result = await pipe.transform({ name: 'test' }, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(result).toEqual({ name: 'test' });
});
});
describe('when schema validation fails', () => {
it('should throw a BadRequestException with issue messages', async () => {
const schema = createSchema(() => ({
issues: [
{ message: 'field is required', path: ['name'] },
{ message: 'must be a string' },
],
}));
await expect(
pipe.transform({}, { type: 'body', schema } as ArgumentMetadata),
).rejects.toThrow();
try {
await pipe.transform({}, {
type: 'body',
schema,
} as ArgumentMetadata);
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect((error as HttpException).getStatus()).toBe(
HttpStatus.BAD_REQUEST,
);
expect((error as HttpException).getResponse()).toEqual({
statusCode: HttpStatus.BAD_REQUEST,
message: ['field is required', 'must be a string'],
error: 'Bad Request',
});
}
});
it('should throw asynchronously when schema validation fails async', async () => {
const schema = createSchema(async () => ({
issues: [{ message: 'invalid' }],
}));
await expect(
pipe.transform({}, { type: 'body', schema } as ArgumentMetadata),
).rejects.toThrow();
});
});
describe('when custom errorHttpStatusCode is provided', () => {
it('should throw with the custom status code', async () => {
const customPipe = new StandardSchemaValidationPipe({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
});
const schema = createSchema(() => ({
issues: [{ message: 'invalid' }],
}));
try {
await customPipe.transform({}, {
type: 'body',
schema,
} as ArgumentMetadata);
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
expect((error as HttpException).getStatus()).toBe(
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
});
});
describe('when custom exceptionFactory is provided', () => {
it('should use the custom exception factory', async () => {
class CustomError extends Error {
constructor(
public readonly issues: readonly StandardSchemaV1.Issue[],
) {
super('Custom validation error');
}
}
const customPipe = new StandardSchemaValidationPipe({
exceptionFactory: issues => new CustomError(issues),
});
const schema = createSchema(() => ({
issues: [{ message: 'invalid' }],
}));
try {
await customPipe.transform({}, {
type: 'body',
schema,
} as ArgumentMetadata);
} catch (error) {
expect(error).toBeInstanceOf(CustomError);
expect((error as CustomError).issues).toEqual([
{ message: 'invalid' },
]);
}
});
});
describe('when transform is enabled (default)', () => {
it('should return the schema-produced value', async () => {
const input = { name: 'test' };
const schema = createSchema(value => ({
value: { ...(value as any), coerced: true },
}));
const result = await pipe.transform(input, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(result).toEqual({ name: 'test', coerced: true });
});
});
describe('when transform is explicitly set to true', () => {
it('should return the schema-produced value', async () => {
const transformPipe = new StandardSchemaValidationPipe({
transform: true,
});
const input = { name: 'test' };
const schema = createSchema(value => ({
value: { ...(value as any), coerced: true },
}));
const result = await transformPipe.transform(input, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(result).toEqual({ name: 'test', coerced: true });
});
});
describe('when transform is disabled', () => {
it('should return the original input value', async () => {
const noTransformPipe = new StandardSchemaValidationPipe({
transform: false,
});
const input = { name: 'test' };
const schema = createSchema(value => ({
value: { ...(value as any), coerced: true },
}));
const result = await noTransformPipe.transform(input, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(result).toBe(input);
expect(result).toEqual({ name: 'test' });
});
});
describe('toValidate', () => {
it('should skip validation for custom decorators by default', async () => {
const schema = createSchema(() => ({
issues: [{ message: 'should not be reached' }],
}));
const result = await pipe.transform({ name: 'test' }, {
type: 'custom',
schema,
} as ArgumentMetadata);
expect(result).toEqual({ name: 'test' });
});
it('should validate custom decorators when validateCustomDecorators is true', async () => {
const customPipe = new StandardSchemaValidationPipe({
validateCustomDecorators: true,
});
const schema = createSchema(() => ({
issues: [{ message: 'invalid' }],
}));
await expect(
customPipe.transform({}, {
type: 'custom',
schema,
} as ArgumentMetadata),
).rejects.toThrow();
});
it('should always validate body/query/param types', async () => {
const schema = createSchema(value => ({ value }));
for (const type of ['body', 'query', 'param'] as const) {
const result = await pipe.transform({ ok: true }, {
type,
schema,
} as ArgumentMetadata);
expect(result).toEqual({ ok: true });
}
});
});
describe('validate', () => {
it('should be overridable', async () => {
class CustomPipe extends StandardSchemaValidationPipe {
protected validate(
value: unknown,
schema: StandardSchemaV1,
options?: Record<string, unknown>,
) {
// Always succeed with a custom value
return {
value: 'custom-result',
} as StandardSchemaV1.Result<unknown>;
}
}
const customPipe = new CustomPipe();
const schema = createSchema(() => ({
issues: [{ message: 'should not be reached' }],
}));
const result = await customPipe.transform('anything', {
type: 'body',
schema,
} as ArgumentMetadata);
expect(result).toBe('custom-result');
});
it('should forward validateOptions to the schema validate function', async () => {
let receivedOptions: unknown;
const schema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: (value: unknown, options?: unknown) => {
receivedOptions = options;
return { value };
},
},
};
const optsPipe = new StandardSchemaValidationPipe({
validateOptions: { strict: true, strip: 'extra' },
});
await optsPipe.transform({ name: 'test' }, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(receivedOptions).toEqual({ strict: true, strip: 'extra' });
});
it('should pass undefined when no validateOptions are provided', async () => {
let receivedOptions: unknown = 'NOT_CALLED';
const schema: StandardSchemaV1 = {
'~standard': {
version: 1,
vendor: 'test',
validate: (value: unknown, options?: unknown) => {
receivedOptions = options;
return { value };
},
},
};
await pipe.transform({ name: 'test' }, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(receivedOptions).toBeUndefined();
});
});
describe('stripProtoKeys', () => {
it('should remove __proto__ and prototype from value before validation', async () => {
let receivedValue: any;
const schema = createSchema(value => {
receivedValue = value;
return { value };
});
const malicious = JSON.parse(
'{"name": "test", "__proto__": {"polluted": true}, "prototype": {"bad": true}}',
);
await pipe.transform(malicious, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(receivedValue).not.toHaveProperty('__proto__');
expect(receivedValue).not.toHaveProperty('prototype');
expect(receivedValue).toHaveProperty('name', 'test');
});
it('should strip proto keys recursively from nested objects', async () => {
let receivedValue: any;
const schema = createSchema(value => {
receivedValue = value;
return { value };
});
const malicious = {
nested: JSON.parse('{"__proto__": {"polluted": true}, "ok": true}'),
};
await pipe.transform(malicious, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(receivedValue.nested).not.toHaveProperty('__proto__');
expect(receivedValue.nested).toHaveProperty('ok', true);
});
it('should strip proto keys from arrays', async () => {
let receivedValue: any;
const schema = createSchema(value => {
receivedValue = value;
return { value };
});
const malicious = [
JSON.parse('{"__proto__": {"polluted": true}, "ok": true}'),
];
await pipe.transform(malicious, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(receivedValue[0]).not.toHaveProperty('__proto__');
expect(receivedValue[0]).toHaveProperty('ok', true);
});
it('should not strip keys from built-in types', async () => {
let receivedValue: any;
const schema = createSchema(value => {
receivedValue = value;
return { value };
});
const date = new Date();
await pipe.transform(date, {
type: 'body',
schema,
} as ArgumentMetadata);
expect(receivedValue).toBe(date);
});
});
});
});

View File

@@ -355,13 +355,15 @@ describe('ValidationPipe', () => {
});
});
describe('when validation rejects', () => {
it('should throw an error', () => {
it('should throw an error', async () => {
target = new ValidationPipe({
forbidNonWhitelisted: true,
whitelist: true,
});
const testObj = { prop1: 'value1', prop2: 'value2', prop3: 'value3' };
expect(target.transform(testObj, metadata)).rejects.toBeDefined();
await expect(
target.transform(testObj, metadata),
).rejects.toBeDefined();
});
});
describe('when transformation is internal', () => {
@@ -450,14 +452,16 @@ describe('ValidationPipe', () => {
});
});
describe('when validation rejects', () => {
it('should throw an error', () => {
it('should throw an error', async () => {
target = new ValidationPipe({
transform: false,
forbidNonWhitelisted: true,
whitelist: true,
});
const testObj = { prop1: 'value1', prop2: 'value2', prop3: 'value3' };
expect(target.transform(testObj, metadata)).rejects.toBeDefined();
await expect(
target.transform(testObj, metadata),
).rejects.toBeDefined();
});
});
});
@@ -469,10 +473,14 @@ describe('ValidationPipe', () => {
{ prop1: 'value1', prop2: 'value2', prop3: 'value3' },
];
expect(target.transform(testObj, metadata)).rejects.toBeDefined();
expect(target.transform('string', metadata)).rejects.toBeDefined();
expect(target.transform(true, metadata)).rejects.toBeDefined();
expect(target.transform(3, metadata)).rejects.toBeDefined();
await expect(
target.transform(testObj, metadata),
).rejects.toBeDefined();
await expect(
target.transform('string', metadata),
).rejects.toBeDefined();
await expect(target.transform(true, metadata)).rejects.toBeDefined();
await expect(target.transform(3, metadata)).rejects.toBeDefined();
});
});
describe('otherwise', () => {

View File

@@ -0,0 +1,348 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { lastValueFrom, of } from 'rxjs';
import { StreamableFile } from '../../file-stream/index.js';
import { CallHandler, ExecutionContext } from '../../interfaces/index.js';
import { StandardSchemaSerializerInterceptor } from '../../serializer/standard-schema-serializer.interceptor.js';
function createSchema(
validateFn: (
value: unknown,
options?: StandardSchemaV1.Options,
) => StandardSchemaV1.Result<any> | Promise<StandardSchemaV1.Result<any>>,
): StandardSchemaV1 {
return {
'~standard': {
version: 1,
vendor: 'test',
validate: validateFn,
},
};
}
describe('StandardSchemaSerializerInterceptor', () => {
let interceptor: StandardSchemaSerializerInterceptor;
let mockReflector: any;
let mockExecutionContext: ExecutionContext;
let mockCallHandler: CallHandler;
beforeEach(() => {
mockReflector = {
getAllAndOverride: vi.fn().mockReturnValue(undefined),
};
mockExecutionContext = {
getHandler: vi.fn(),
getClass: vi.fn(),
} as any;
mockCallHandler = {
handle: vi.fn(),
} as any;
});
describe('intercept', () => {
describe('when no schema is provided', () => {
it('should return the response unchanged', async () => {
interceptor = new StandardSchemaSerializerInterceptor(mockReflector);
const response = { id: 1, name: 'Test' };
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
const result = await lastValueFrom(result$);
expect(result).toBe(response);
});
});
describe('when schema is provided via default options', () => {
it('should serialize the response through the schema', async () => {
const schema = createSchema(value => ({
value: { ...(value as any), serialized: true },
}));
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
});
const response = { id: 1, name: 'Test' };
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
const result = await lastValueFrom(result$);
expect(result).toEqual({ id: 1, name: 'Test', serialized: true });
});
});
describe('when schema is provided via @SerializeOptions()', () => {
it('should use the context schema over the default', async () => {
const defaultSchema = createSchema(value => ({
value: { ...(value as any), fromDefault: true },
}));
const contextSchema = createSchema(value => ({
value: { ...(value as any), fromContext: true },
}));
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema: defaultSchema,
});
mockReflector.getAllAndOverride.mockReturnValue({
schema: contextSchema,
});
const response = { id: 1 };
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
const result = await lastValueFrom(result$);
expect(result).toEqual({ id: 1, fromContext: true });
});
});
describe('when response is an array', () => {
it('should serialize each item through the schema', async () => {
const schema = createSchema(value => ({
value: { ...(value as any), serialized: true },
}));
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
});
const response = [{ id: 1 }, { id: 2 }];
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
const result = await lastValueFrom(result$);
expect(result).toEqual([
{ id: 1, serialized: true },
{ id: 2, serialized: true },
]);
});
});
describe('when response is not an object', () => {
it('should return primitive values unchanged', async () => {
const schema = createSchema(() => ({
issues: [{ message: 'should not be called' }],
}));
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
});
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of('plain string'),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
const result = await lastValueFrom(result$);
expect(result).toBe('plain string');
});
});
describe('when response is a StreamableFile', () => {
it('should return the streamable file unchanged', async () => {
const schema = createSchema(() => ({
issues: [{ message: 'should not be called' }],
}));
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
});
const file = new StreamableFile(Buffer.from('test'));
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(file),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
const result = await lastValueFrom(result$);
expect(result).toBe(file);
});
});
describe('when schema validation fails', () => {
it('should throw an error with issue messages', async () => {
const schema = createSchema(() => ({
issues: [
{ message: 'field is required' },
{ message: 'must be a number' },
],
}));
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
});
const response = { id: 1 };
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
await expect(lastValueFrom(result$)).rejects.toThrow(
'Serialization failed: field is required, must be a number',
);
});
});
describe('when async schema is used', () => {
it('should resolve the async validation result', async () => {
const schema = createSchema(async value => ({
value: { ...(value as any), async: true },
}));
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
});
const response = { id: 1 };
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
const result = await lastValueFrom(result$);
expect(result).toEqual({ id: 1, async: true });
});
});
});
describe('transformToPlain', () => {
it('should return falsy values as-is', async () => {
interceptor = new StandardSchemaSerializerInterceptor(mockReflector);
const schema = createSchema(() => ({
issues: [{ message: 'should not be called' }],
}));
expect(await interceptor.transformToPlain(null, schema)).toBeNull();
expect(
await interceptor.transformToPlain(undefined, schema),
).toBeUndefined();
expect(await interceptor.transformToPlain(0, schema)).toBe(0);
expect(await interceptor.transformToPlain('', schema)).toBe('');
});
});
describe('validateOptions', () => {
it('should forward default validateOptions to the schema', async () => {
const validateSpy = vi.fn((_value, _options) => ({
value: { ok: true },
}));
const schema = createSchema(validateSpy);
const opts: StandardSchemaV1.Options = {
libraryOptions: { strip: true },
};
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
validateOptions: opts,
});
const response = { id: 1 };
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
await lastValueFrom(result$);
expect(validateSpy).toHaveBeenCalledWith(response, opts);
});
it('should forward context validateOptions from @SerializeOptions()', async () => {
const validateSpy = vi.fn((_value, _options) => ({
value: { ok: true },
}));
const schema = createSchema(validateSpy);
const contextOpts: StandardSchemaV1.Options = {
libraryOptions: { mode: 'strict' },
};
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
});
mockReflector.getAllAndOverride.mockReturnValue({
validateOptions: contextOpts,
});
const response = { id: 1 };
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
await lastValueFrom(result$);
expect(validateSpy).toHaveBeenCalledWith(response, contextOpts);
});
it('should let context validateOptions override default', async () => {
const validateSpy = vi.fn((_value, _options) => ({
value: { ok: true },
}));
const schema = createSchema(validateSpy);
const defaultOpts: StandardSchemaV1.Options = {
libraryOptions: { from: 'default' },
};
const contextOpts: StandardSchemaV1.Options = {
libraryOptions: { from: 'context' },
};
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
validateOptions: defaultOpts,
});
mockReflector.getAllAndOverride.mockReturnValue({
validateOptions: contextOpts,
});
const response = { id: 1 };
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
await lastValueFrom(result$);
expect(validateSpy).toHaveBeenCalledWith(response, contextOpts);
});
it('should pass undefined when no validateOptions are set', async () => {
const validateSpy = vi.fn((_value, _options) => ({
value: { ok: true },
}));
const schema = createSchema(validateSpy);
interceptor = new StandardSchemaSerializerInterceptor(mockReflector, {
schema,
});
const response = { id: 1 };
(mockCallHandler.handle as ReturnType<typeof vi.fn>).mockReturnValue(
of(response),
);
const result$ = interceptor.intercept(
mockExecutionContext,
mockCallHandler,
);
await lastValueFrom(result$);
expect(validateSpy).toHaveBeenCalledWith(response, undefined);
});
});
});

View File

@@ -1,10 +1,11 @@
import type { StandardSchemaV1 } from '@standard-schema/spec';
import { CUSTOM_ROUTE_ARGS_METADATA } from '../constants.js';
import {
ParamData,
RouteParamMetadata,
} from '../decorators/http/route-params.decorator.js';
import { PipeTransform, Type } from '../interfaces/index.js';
import { CustomParamFactory } from '../interfaces/features/custom-route-param-factory.interface.js';
import { PipeTransform, Type } from '../interfaces/index.js';
export function assignCustomParameterMetadata(
args: Record<number, RouteParamMetadata>,
@@ -12,6 +13,7 @@ export function assignCustomParameterMetadata(
index: number,
factory: CustomParamFactory,
data?: ParamData,
schema?: StandardSchemaV1,
...pipes: (Type<PipeTransform> | PipeTransform)[]
) {
return {
@@ -21,6 +23,7 @@ export function assignCustomParameterMetadata(
factory,
data,
pipes,
...(schema !== undefined && { schema }),
},
};
}

View File

@@ -1,9 +1,10 @@
import { HttpServer, RequestMethod, VersioningOptions } from '@nestjs/common';
import {
RequestHandler,
VersionValue,
} from '@nestjs/common/interfaces/index.js';
import { NestApplicationOptions } from '@nestjs/common/interfaces/nest-application-options.interface.js';
import type {
HttpServer,
RequestMethod,
VersioningOptions,
} from '@nestjs/common';
import type { RequestHandler, VersionValue } from '@nestjs/common/internal';
import type { NestApplicationOptions } from '@nestjs/common';
/**
* @publicApi

View File

@@ -1,4 +1,4 @@
import {
import type {
CanActivate,
ExceptionFilter,
NestInterceptor,
@@ -6,9 +6,9 @@ import {
VersioningOptions,
WebSocketAdapter,
} from '@nestjs/common';
import { GlobalPrefixOptions } from '@nestjs/common/interfaces/index.js';
import { InstanceWrapper } from './injector/instance-wrapper.js';
import { ExcludeRouteMetadata } from './router/interfaces/exclude-route-metadata.interface.js';
import type { GlobalPrefixOptions } from '@nestjs/common/internal';
export class ApplicationConfig {
private globalPrefix = '';

View File

@@ -1,4 +1,4 @@
import { EnhancerSubtype } from '@nestjs/common/constants.js';
import type { EnhancerSubtype } from '@nestjs/common/internal';
export const MESSAGES = {
APPLICATION_START: `Starting Nest application...`,

View File

@@ -1,4 +1,4 @@
import { Type } from '@nestjs/common';
import type { Type } from '@nestjs/common';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import { ModulesContainer } from '../injector/modules-container.js';

View File

@@ -1,5 +1,5 @@
import {
CustomDecorator,
type CustomDecorator,
flatten,
Injectable,
SetMetadata,

View File

@@ -1,4 +1,4 @@
import { Logger } from '@nestjs/common/services/logger.service.js';
import { Logger } from '@nestjs/common';
export class ExceptionHandler {
private static readonly logger = new Logger(ExceptionHandler.name);

View File

@@ -1,7 +1,7 @@
import { Abstract, Type } from '@nestjs/common/interfaces/index.js';
import { isFunction } from '@nestjs/common/utils/shared.utils.js';
import { INVALID_CLASS_SCOPE_MESSAGE } from '../messages.js';
import { RuntimeException } from './runtime.exception.js';
import type { Abstract, Type } from '@nestjs/common';
import { isFunction } from '@nestjs/common/internal';
export class InvalidClassScopeException extends RuntimeException {
constructor(metatypeOrToken: Type<any> | Abstract<any> | string | symbol) {

View File

@@ -1,6 +1,6 @@
import { UNDEFINED_FORWARDREF_MESSAGE } from '../messages.js';
import { RuntimeException } from './runtime.exception.js';
import { Type } from '@nestjs/common';
import type { Type } from '@nestjs/common';
export class UndefinedForwardRefException extends RuntimeException {
constructor(scope: Type<any>[]) {

View File

@@ -1,10 +1,10 @@
import type { DynamicModule, ForwardReference, Type } from '@nestjs/common';
import { isNil, isSymbol } from '@nestjs/common/utils/shared.utils.js';
import {
InjectorDependency,
InjectorDependencyContext,
} from '../injector/injector.js';
import { Module } from '../injector/module.js';
import { isNil, isSymbol } from '@nestjs/common/internal';
/**
* Returns the name of an instance or `undefined`

View File

@@ -1,12 +1,14 @@
import { FILTER_CATCH_EXCEPTIONS } from '@nestjs/common/constants.js';
import { Type } from '@nestjs/common/interfaces/index.js';
import { ExceptionFilter } from '@nestjs/common/interfaces/exceptions/exception-filter.interface.js';
import { isEmpty, isFunction } from '@nestjs/common/utils/shared.utils.js';
import { iterate } from 'iterare';
import { ContextCreator } from '../helpers/context-creator.js';
import { STATIC_CONTEXT } from '../injector/constants.js';
import { NestContainer } from '../injector/container.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import {
FILTER_CATCH_EXCEPTIONS,
isEmpty,
isFunction,
} from '@nestjs/common/internal';
import type { Type, ExceptionFilter } from '@nestjs/common';
export class BaseExceptionFilterContext extends ContextCreator {
protected moduleContext: string;

View File

@@ -1,18 +1,18 @@
import {
ArgumentsHost,
ExceptionFilter,
type ArgumentsHost,
type ExceptionFilter,
HttpException,
HttpServer,
type HttpServer,
HttpStatus,
Inject,
IntrinsicException,
Logger,
Optional,
} from '@nestjs/common';
import { isObject } from '@nestjs/common/utils/shared.utils.js';
import { AbstractHttpAdapter } from '../adapters/index.js';
import { MESSAGES } from '../constants.js';
import { HttpAdapterHost } from '../helpers/http-adapter-host.js';
import { isObject } from '@nestjs/common/internal';
export class BaseExceptionFilter<T = any> implements ExceptionFilter<T> {
private static readonly logger = new Logger('ExceptionsHandler');

View File

@@ -1,10 +1,12 @@
import { HttpException } from '@nestjs/common';
import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions/exception-filter-metadata.interface.js';
import { ArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface.js';
import { selectExceptionFilterMetadata } from '@nestjs/common/utils/select-exception-filter-metadata.util.js';
import { isEmpty } from '@nestjs/common/utils/shared.utils.js';
import type { HttpException } from '@nestjs/common';
import { InvalidExceptionFilterException } from '../errors/exceptions/invalid-exception-filter.exception.js';
import { BaseExceptionFilter } from './base-exception-filter.js';
import {
type ExceptionFilterMetadata,
selectExceptionFilterMetadata,
isEmpty,
} from '@nestjs/common/internal';
import type { ArgumentsHost } from '@nestjs/common';
export class ExceptionsHandler extends BaseExceptionFilter {
private filters: ExceptionFilterMetadata[] = [];

View File

@@ -1,7 +1,3 @@
import { EXCEPTION_FILTERS_METADATA } from '@nestjs/common/constants.js';
import { Controller } from '@nestjs/common/interfaces/index.js';
import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions/index.js';
import { isEmpty } from '@nestjs/common/utils/shared.utils.js';
import { iterate } from 'iterare';
import { ApplicationConfig } from '../application-config.js';
import { STATIC_CONTEXT } from '../injector/constants.js';
@@ -10,6 +6,12 @@ import { InstanceWrapper } from '../injector/instance-wrapper.js';
import { RouterProxyCallback } from '../router/router-proxy.js';
import { BaseExceptionFilterContext } from './base-exception-filter-context.js';
import { ExternalExceptionsHandler } from './external-exceptions-handler.js';
import {
EXCEPTION_FILTERS_METADATA,
type Controller,
type ExceptionFilterMetadata,
isEmpty,
} from '@nestjs/common/internal';
export class ExternalExceptionFilterContext extends BaseExceptionFilterContext {
constructor(

View File

@@ -1,4 +1,4 @@
import { ArgumentsHost, IntrinsicException, Logger } from '@nestjs/common';
import { type ArgumentsHost, IntrinsicException, Logger } from '@nestjs/common';
export class ExternalExceptionFilter<T = any, R = any> {
private static readonly logger = new Logger('ExceptionsHandler');

View File

@@ -1,9 +1,11 @@
import { ExceptionFilterMetadata } from '@nestjs/common/interfaces/exceptions/index.js';
import { ArgumentsHost } from '@nestjs/common/interfaces/features/arguments-host.interface.js';
import { selectExceptionFilterMetadata } from '@nestjs/common/utils/select-exception-filter-metadata.util.js';
import { isEmpty } from '@nestjs/common/utils/shared.utils.js';
import { InvalidExceptionFilterException } from '../errors/exceptions/invalid-exception-filter.exception.js';
import { ExternalExceptionFilter } from './external-exception-filter.js';
import {
type ExceptionFilterMetadata,
selectExceptionFilterMetadata,
isEmpty,
} from '@nestjs/common/internal';
import type { ArgumentsHost } from '@nestjs/common';
export class ExternalExceptionsHandler extends ExternalExceptionFilter {
private filters: ExceptionFilterMetadata[] = [];

View File

@@ -1,8 +1,8 @@
import { CanActivate } from '@nestjs/common';
import { ContextType, Controller } from '@nestjs/common/interfaces/index.js';
import { isEmpty } from '@nestjs/common/utils/shared.utils.js';
import type { CanActivate } from '@nestjs/common';
import { lastValueFrom, Observable } from 'rxjs';
import { ExecutionContextHost } from '../helpers/execution-context-host.js';
import type { ContextType } from '@nestjs/common';
import { type Controller, isEmpty } from '@nestjs/common/internal';
export class GuardsConsumer {
public async tryActivate<TContext extends string = ContextType>(

View File

@@ -1,13 +1,17 @@
import { CanActivate } from '@nestjs/common';
import { GUARDS_METADATA } from '@nestjs/common/constants.js';
import { Controller, Type } from '@nestjs/common/interfaces/index.js';
import { isEmpty, isFunction } from '@nestjs/common/utils/shared.utils.js';
import type { CanActivate } from '@nestjs/common';
import { iterate } from 'iterare';
import { ApplicationConfig } from '../application-config.js';
import { ContextCreator } from '../helpers/context-creator.js';
import { STATIC_CONTEXT } from '../injector/constants.js';
import { NestContainer } from '../injector/container.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import {
GUARDS_METADATA,
type Controller,
isEmpty,
isFunction,
} from '@nestjs/common/internal';
import type { Type } from '@nestjs/common';
export class GuardsContextCreator extends ContextCreator {
private moduleContext: string;

View File

@@ -1,6 +1,6 @@
import { Controller } from '@nestjs/common/interfaces/index.js';
import { STATIC_CONTEXT } from '../injector/constants.js';
import { ContextId, InstanceWrapper } from '../injector/instance-wrapper.js';
import type { Controller } from '@nestjs/common/internal';
export abstract class ContextCreator {
public abstract createConcreteContext<T extends any[], R extends any[]>(

View File

@@ -1,6 +1,6 @@
import { isObject } from '@nestjs/common/utils/shared.utils.js';
import { ContextId, HostComponentInfo } from '../injector/instance-wrapper.js';
import { REQUEST_CONTEXT_ID } from '../router/request/request-constants.js';
import { isObject } from '@nestjs/common/internal';
export function createContextId(): ContextId {
/**

View File

@@ -1,16 +1,13 @@
import { ParamData } from '@nestjs/common';
import type { ParamData } from '@nestjs/common';
import { ExecutionContextHost } from './execution-context-host.js';
import {
PARAMTYPES_METADATA,
RESPONSE_PASSTHROUGH_METADATA,
} from '@nestjs/common/constants.js';
import {
ContextType,
Controller,
PipeTransform,
Type,
} from '@nestjs/common/interfaces/index.js';
import { isFunction } from '@nestjs/common/utils/shared.utils.js';
import { ExecutionContextHost } from './execution-context-host.js';
type Controller,
isFunction,
} from '@nestjs/common/internal';
import type { ContextType, PipeTransform, Type } from '@nestjs/common';
import type { StandardSchemaV1 } from '@standard-schema/spec';
export interface ParamProperties<T = any, IExtractor extends Function = any> {
index: number;
@@ -18,6 +15,7 @@ export interface ParamProperties<T = any, IExtractor extends Function = any> {
data: ParamData;
pipes: PipeTransform[];
extractValue: IExtractor;
schema?: StandardSchemaV1;
}
export class ContextUtils {

View File

@@ -1,11 +1,10 @@
import { ExecutionContext } from '@nestjs/common';
import { Type } from '@nestjs/common/interfaces/index.js';
import {
ContextType,
import type { ExecutionContext } from '@nestjs/common';
import type { Type, ContextType } from '@nestjs/common';
import type {
HttpArgumentsHost,
RpcArgumentsHost,
WsArgumentsHost,
} from '@nestjs/common/interfaces/features/arguments-host.interface.js';
} from '@nestjs/common/internal';
export class ExecutionContextHost implements ExecutionContext {
private contextType = 'http';

View File

@@ -1,15 +1,18 @@
import { ForbiddenException, ParamData } from '@nestjs/common';
import { CUSTOM_ROUTE_ARGS_METADATA } from '@nestjs/common/constants.js';
import type { ContextType, PipeTransform } from '@nestjs/common';
import {
ContextType,
Controller,
PipeTransform,
} from '@nestjs/common/interfaces/index.js';
import { isEmpty } from '@nestjs/common/utils/shared.utils.js';
ArgumentMetadata,
ForbiddenException,
type ParamData,
} from '@nestjs/common';
import {
CUSTOM_ROUTE_ARGS_METADATA,
type Controller,
isEmpty,
} from '@nestjs/common/internal';
import { isObservable, lastValueFrom } from 'rxjs';
import { ExternalExceptionFilterContext } from '../exceptions/external-exception-filter-context.js';
import { GuardsConsumer, GuardsContextCreator } from '../guards/index.js';
import { FORBIDDEN_MESSAGE } from '../guards/constants.js';
import { GuardsConsumer, GuardsContextCreator } from '../guards/index.js';
import { STATIC_CONTEXT } from '../injector/constants.js';
import { NestContainer } from '../injector/container.js';
import { ContextId } from '../injector/instance-wrapper.js';
@@ -264,7 +267,7 @@ export class ExternalContextCreator {
this.pipesContextCreator.setModuleContext(moduleContext);
return keys.map(key => {
const { index, data, pipes: pipesCollection } = metadata[key];
const { index, data, pipes: pipesCollection, schema } = metadata[key];
const pipes = this.pipesContextCreator.createConcreteContext(
pipesCollection,
contextId,
@@ -279,13 +282,20 @@ export class ExternalContextCreator {
data,
contextFactory,
);
return { index, extractValue: customExtractValue, type, data, pipes };
return {
index,
extractValue: customExtractValue,
type,
data,
pipes,
schema,
};
}
const numericType = Number(type);
const extractValue = (...args: unknown[]) =>
paramsFactory.exchangeKeyForValue(numericType, data, args);
return { index, extractValue, type: numericType, data, pipes };
return { index, extractValue, type: numericType, data, pipes, schema };
});
}
@@ -304,12 +314,13 @@ export class ExternalContextCreator {
data,
metatype,
pipes: paramPipes,
schema,
} = param;
const value = extractValue(...params);
args[index] = await this.getParamValue(
value,
{ metatype, type, data },
{ metatype, type, data, schema } as ArgumentMetadata,
pipes.concat(paramPipes),
);
};
@@ -320,12 +331,12 @@ export class ExternalContextCreator {
public async getParamValue<T>(
value: T,
{ metatype, type, data }: { metatype: any; type: any; data: any },
metadata: ArgumentMetadata,
pipes: PipeTransform[],
): Promise<any> {
return isEmpty(pipes)
? value
: this.pipesConsumer.apply(value, { metatype, type, data }, pipes);
: this.pipesConsumer.apply(value, metadata, pipes);
}
public async transformToResult(resultOrDeferred: any) {

View File

@@ -1,6 +1,6 @@
import { ContextType } from '@nestjs/common/interfaces/index.js';
import { ExternalExceptionsHandler } from '../exceptions/external-exceptions-handler.js';
import { ExecutionContextHost } from '../helpers/execution-context-host.js';
import type { ContextType } from '@nestjs/common';
export class ExternalErrorProxy {
public createProxy<TContext extends string = ContextType>(

View File

@@ -1,6 +1,6 @@
import { Scope } from '@nestjs/common';
import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants.js';
import { Type } from '@nestjs/common/interfaces/type.interface.js';
import type { Scope } from '@nestjs/common';
import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/internal';
import type { Type } from '@nestjs/common';
export function getClassScope(provider: Type<unknown>): Scope {
const metadata = Reflect.getMetadata(SCOPE_OPTIONS_METADATA, provider);

View File

@@ -1,10 +1,11 @@
import { Controller, Type } from '@nestjs/common/interfaces/index.js';
import { IncomingMessage } from 'http';
import { Observable } from 'rxjs';
import { CONTROLLER_ID_KEY } from '../injector/constants.js';
import { ContextId } from '../injector/instance-wrapper.js';
import { HeaderStream } from '../router/sse-stream.js';
import { ParamProperties } from './context-utils.js';
import type { Type } from '@nestjs/common';
import type { Controller } from '@nestjs/common/internal';
export const HANDLER_METADATA_SYMBOL = Symbol.for('handler_metadata:cache');

View File

@@ -1,4 +1,4 @@
import { ParamData } from '@nestjs/common';
import type { ParamData } from '@nestjs/common';
export type ParamsMetadata = Record<number, ParamMetadata>;
export interface ParamMetadata {

View File

@@ -1,5 +1,5 @@
import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants.js';
import { Type } from '@nestjs/common/interfaces/type.interface.js';
import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/internal';
import type { Type } from '@nestjs/common';
export function isDurable(provider: Type<unknown>): boolean | undefined {
const metadata = Reflect.getMetadata(SCOPE_OPTIONS_METADATA, provider);

View File

@@ -1,8 +1,5 @@
import { RequestMethod } from '@nestjs/common/enums/request-method.enum.js';
import {
VersionValue,
VERSION_NEUTRAL,
} from '@nestjs/common/interfaces/version-options.interface.js';
import { RequestMethod, VERSION_NEUTRAL } from '@nestjs/common';
import type { VersionValue } from '@nestjs/common/internal';
export const MODULE_INIT_MESSAGE = (
text: TemplateStringsArray,

View File

@@ -1,5 +1,5 @@
import { HttpServer } from '@nestjs/common';
import { RequestMethod } from '@nestjs/common/enums/request-method.enum.js';
import type { HttpServer } from '@nestjs/common';
import { RequestMethod } from '@nestjs/common';
export const REQUEST_METHOD_MAP = {
[RequestMethod.GET]: 'get',

View File

@@ -1,5 +1,4 @@
import { BeforeApplicationShutdown } from '@nestjs/common';
import { isFunction, isNil } from '@nestjs/common/utils/shared.utils.js';
import type { BeforeApplicationShutdown } from '@nestjs/common';
import { iterate } from 'iterare';
import {
getNonTransientInstances,
@@ -7,6 +6,7 @@ import {
} from '../injector/helpers/transient-instances.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import { Module } from '../injector/module.js';
import { isFunction, isNil } from '@nestjs/common/internal';
/**
* Checks if the given instance has the `beforeApplicationShutdown` function

View File

@@ -1,5 +1,4 @@
import { OnApplicationBootstrap } from '@nestjs/common';
import { isFunction, isNil } from '@nestjs/common/utils/shared.utils.js';
import type { OnApplicationBootstrap } from '@nestjs/common';
import { iterate } from 'iterare';
import {
getNonTransientInstances,
@@ -7,6 +6,7 @@ import {
} from '../injector/helpers/transient-instances.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import { Module } from '../injector/module.js';
import { isFunction, isNil } from '@nestjs/common/internal';
/**
* Checks if the given instance has the `onApplicationBootstrap` function

View File

@@ -1,5 +1,4 @@
import { OnApplicationShutdown } from '@nestjs/common';
import { isFunction, isNil } from '@nestjs/common/utils/shared.utils.js';
import type { OnApplicationShutdown } from '@nestjs/common';
import { iterate } from 'iterare';
import {
getNonTransientInstances,
@@ -7,6 +6,7 @@ import {
} from '../injector/helpers/transient-instances.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import { Module } from '../injector/module.js';
import { isFunction, isNil } from '@nestjs/common/internal';
/**
* Checks if the given instance has the `onApplicationShutdown` function

View File

@@ -1,5 +1,4 @@
import { OnModuleDestroy } from '@nestjs/common';
import { isFunction, isNil } from '@nestjs/common/utils/shared.utils.js';
import type { OnModuleDestroy } from '@nestjs/common';
import { iterate } from 'iterare';
import {
getNonTransientInstances,
@@ -7,6 +6,7 @@ import {
} from '../injector/helpers/transient-instances.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import { Module } from '../injector/module.js';
import { isFunction, isNil } from '@nestjs/common/internal';
/**
* Returns true or false if the given instance has a `onModuleDestroy` function

View File

@@ -1,5 +1,4 @@
import { OnModuleInit } from '@nestjs/common';
import { isFunction, isNil } from '@nestjs/common/utils/shared.utils.js';
import type { OnModuleInit } from '@nestjs/common';
import { iterate } from 'iterare';
import {
getNonTransientInstances,
@@ -7,6 +6,7 @@ import {
} from '../injector/helpers/transient-instances.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import { Module } from '../injector/module.js';
import { isFunction, isNil } from '@nestjs/common/internal';
/**
* Returns true or false if the given instance has a `onModuleInit` function

View File

@@ -1,5 +1,4 @@
import { Abstract, Scope, Type } from '@nestjs/common';
import { GetOrResolveOptions } from '@nestjs/common/interfaces/index.js';
import { type Abstract, Scope, type Type } from '@nestjs/common';
import {
InvalidClassScopeException,
UnknownElementException,
@@ -8,6 +7,7 @@ import { Injector } from './injector.js';
import { InstanceLink, InstanceLinksHost } from './instance-links-host.js';
import { ContextId } from './instance-wrapper.js';
import { Module } from './module.js';
import type { GetOrResolveOptions } from '@nestjs/common/internal';
export abstract class AbstractInstanceResolver {
protected abstract instanceLinksHost: InstanceLinksHost;

View File

@@ -1,9 +1,5 @@
import {
DynamicModule,
ForwardReference,
Type,
} from '@nestjs/common/interfaces/index.js';
import { ModuleOpaqueKeyFactory } from './opaque-key-factory/interfaces/module-opaque-key-factory.interface.js';
import type { DynamicModule, ForwardReference, Type } from '@nestjs/common';
export interface ModuleFactory {
type: Type<any>;

View File

@@ -1,10 +1,4 @@
import { DynamicModule, Provider } from '@nestjs/common';
import {
EnhancerSubtype,
GLOBAL_MODULE_METADATA,
} from '@nestjs/common/constants.js';
import { Injectable, Type } from '@nestjs/common/interfaces/index.js';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface.js';
import type { DynamicModule, Provider } from '@nestjs/common';
import { ApplicationConfig } from '../application-config.js';
import { DiscoverableMetaHostCollection } from '../discovery/discoverable-meta-host-collection.js';
import {
@@ -24,6 +18,13 @@ import { ModulesContainer } from './modules-container.js';
import { ByReferenceModuleOpaqueKeyFactory } from './opaque-key-factory/by-reference-module-opaque-key-factory.js';
import { DeepHashedModuleOpaqueKeyFactory } from './opaque-key-factory/deep-hashed-module-opaque-key-factory.js';
import { ModuleOpaqueKeyFactory } from './opaque-key-factory/interfaces/module-opaque-key-factory.interface.js';
import {
type EnhancerSubtype,
GLOBAL_MODULE_METADATA,
type Injectable,
type NestApplicationContextOptions,
} from '@nestjs/common/internal';
import type { Type } from '@nestjs/common';
type ModuleMetatype = Type<any> | DynamicModule | Promise<DynamicModule>;
type ModuleScope = Type<any>[];

View File

@@ -1,10 +1,10 @@
import {
import type {
ClassProvider,
FactoryProvider,
Provider,
ValueProvider,
} from '@nestjs/common';
import { isUndefined } from '@nestjs/common/utils/shared.utils.js';
import { isUndefined } from '@nestjs/common/internal';
export function isClassProvider<T = any>(
provider: Provider,

View File

@@ -1,4 +1,4 @@
import { InjectionToken } from '@nestjs/common';
import type { InjectionToken } from '@nestjs/common';
import { iterate } from 'iterare';
import { InstanceWrapper } from '../instance-wrapper.js';

View File

@@ -1,31 +1,9 @@
import {
InjectionToken,
type InjectionToken,
Logger,
LoggerService,
OptionalFactoryDependency,
type LoggerService,
type OptionalFactoryDependency,
} from '@nestjs/common';
import {
OPTIONAL_DEPS_METADATA,
OPTIONAL_PROPERTY_DEPS_METADATA,
PARAMTYPES_METADATA,
PROPERTY_DEPS_METADATA,
SELF_DECLARED_DEPS_METADATA,
} from '@nestjs/common/constants.js';
import {
Controller,
ForwardReference,
Injectable,
Type,
} from '@nestjs/common/interfaces/index.js';
import { clc } from '@nestjs/common/utils/cli-colors.util.js';
import {
isFunction,
isNil,
isObject,
isString,
isSymbol,
isUndefined,
} from '@nestjs/common/utils/shared.utils.js';
import { iterate } from 'iterare';
import { performance } from 'perf_hooks';
import { CircularDependencyException } from '../errors/exceptions/index.js';
@@ -43,6 +21,23 @@ import {
} from './instance-wrapper.js';
import { Module } from './module.js';
import { SettlementSignal } from './settlement-signal.js';
import {
OPTIONAL_DEPS_METADATA,
OPTIONAL_PROPERTY_DEPS_METADATA,
PARAMTYPES_METADATA,
PROPERTY_DEPS_METADATA,
SELF_DECLARED_DEPS_METADATA,
type Controller,
type Injectable,
clc,
isFunction,
isNil,
isObject,
isString,
isSymbol,
isUndefined,
} from '@nestjs/common/internal';
import type { ForwardReference, Type } from '@nestjs/common';
/**
* The type of an injectable dependency

View File

@@ -1,4 +1,4 @@
import { Provider, Scope } from '@nestjs/common';
import { type Provider, Scope } from '@nestjs/common';
import { INQUIRER } from './inquirer-constants.js';
const noop = () => {};

View File

@@ -1,9 +1,9 @@
import { InjectionToken } from '@nestjs/common';
import { isFunction } from '@nestjs/common/utils/shared.utils.js';
import type { InjectionToken } from '@nestjs/common';
import { UnknownElementException } from '../errors/exceptions/unknown-element.exception.js';
import { NestContainer } from './container.js';
import { InstanceWrapper } from './instance-wrapper.js';
import { Module } from './module.js';
import { isFunction } from '@nestjs/common/internal';
type HostCollection = 'providers' | 'controllers' | 'injectables';

View File

@@ -1,12 +1,11 @@
import { Logger, LoggerService } from '@nestjs/common';
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface.js';
import { Injectable } from '@nestjs/common/interfaces/injectable.interface.js';
import { Logger, type LoggerService } from '@nestjs/common';
import { MODULE_INIT_MESSAGE } from '../helpers/messages.js';
import { GraphInspector } from '../inspector/graph-inspector.js';
import { NestContainer } from './container.js';
import { Injector } from './injector.js';
import { InternalCoreModule } from './internal-core-module/internal-core-module.js';
import { Module } from './module.js';
import type { Controller, Injectable } from '@nestjs/common/internal';
export class InstanceLoader<TInjector extends Injector = Injector> {
constructor(

View File

@@ -1,16 +1,10 @@
import { Logger, LoggerService, Provider, Scope, Type } from '@nestjs/common';
import { EnhancerSubtype } from '@nestjs/common/constants.js';
import {
FactoryProvider,
InjectionToken,
} from '@nestjs/common/interfaces/index.js';
import { clc } from '@nestjs/common/utils/cli-colors.util.js';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util.js';
import {
isNil,
isString,
isUndefined,
} from '@nestjs/common/utils/shared.utils.js';
Logger,
type LoggerService,
type Provider,
Scope,
type Type,
} from '@nestjs/common';
import { iterate } from 'iterare';
import { UuidFactory } from '../inspector/uuid-factory.js';
import { STATIC_CONTEXT } from './constants.js';
@@ -21,6 +15,15 @@ import {
} from './helpers/provider-classifier.js';
import { Module } from './module.js';
import { SettlementSignal } from './settlement-signal.js';
import {
type EnhancerSubtype,
clc,
randomStringGenerator,
isNil,
isString,
isUndefined,
} from '@nestjs/common/internal';
import type { FactoryProvider, InjectionToken } from '@nestjs/common';
export const INSTANCE_METADATA_SYMBOL = Symbol.for('instance_metadata:cache');
export const INSTANCE_ID_SYMBOL = Symbol.for('instance_metadata:id');

View File

@@ -1,12 +1,12 @@
import { DynamicModule, Global, Module } from '@nestjs/common';
import {
ExistingProvider,
FactoryProvider,
ValueProvider,
} from '@nestjs/common/interfaces/index.js';
import { type DynamicModule, Global, Module } from '@nestjs/common';
import { requestProvider } from '../../router/request/request-providers.js';
import { Reflector } from '../../services/index.js';
import { inquirerProvider } from '../inquirer/inquirer-providers.js';
import type {
ExistingProvider,
FactoryProvider,
ValueProvider,
} from '@nestjs/common';
const ReflectorAliasProvider = {
provide: Reflector.name,

View File

@@ -1,4 +1,4 @@
import { DynamicModule, Type } from '@nestjs/common';
import type { DynamicModule, Type } from '@nestjs/common';
import { ModuleOverride } from '../../interfaces/module-override.interface.js';
import { DependenciesScanner } from '../../scanner.js';
import { ModuleCompiler } from '../compiler.js';

View File

@@ -1,4 +1,4 @@
import { IntrospectionResult, Scope, Type } from '@nestjs/common';
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';

View File

@@ -1,30 +1,3 @@
import {
EnhancerSubtype,
ENTRY_PROVIDER_WATERMARK,
} from '@nestjs/common/constants.js';
import {
ClassProvider,
Controller,
DynamicModule,
ExistingProvider,
FactoryProvider,
Injectable,
InjectionToken,
NestModule,
Provider,
Scope,
Type,
ValueProvider,
} from '@nestjs/common/interfaces/index.js';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util.js';
import {
isFunction,
isNil,
isObject,
isString,
isSymbol,
isUndefined,
} from '@nestjs/common/utils/shared.utils.js';
import { iterate } from 'iterare';
import { ApplicationConfig } from '../application-config.js';
import {
@@ -40,6 +13,31 @@ import { CONTROLLER_ID_KEY } from './constants.js';
import { NestContainer } from './container.js';
import { ContextId, InstanceWrapper } from './instance-wrapper.js';
import { ModuleRef, ModuleRefGetOrResolveOpts } from './module-ref.js';
import {
type EnhancerSubtype,
ENTRY_PROVIDER_WATERMARK,
type Controller,
type Injectable,
randomStringGenerator,
isFunction,
isNil,
isObject,
isString,
isSymbol,
isUndefined,
} from '@nestjs/common/internal';
import {
type ClassProvider,
type DynamicModule,
type ExistingProvider,
type FactoryProvider,
type InjectionToken,
type NestModule,
type Provider,
Scope,
type Type,
type ValueProvider,
} from '@nestjs/common';
export class Module {
private readonly _id: string;

View File

@@ -1,9 +1,7 @@
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface.js';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface.js';
import { Type } from '@nestjs/common/interfaces/type.interface.js';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util.js';
import { createHash } from 'crypto';
import { ModuleOpaqueKeyFactory } from './interfaces/module-opaque-key-factory.interface.js';
import type { DynamicModule, ForwardReference, Type } from '@nestjs/common';
import { randomStringGenerator } from '@nestjs/common/internal';
const K_MODULE_ID = Symbol('K_MODULE_ID');

View File

@@ -1,11 +1,12 @@
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface.js';
import { Type } from '@nestjs/common/interfaces/type.interface.js';
import { Logger } from '@nestjs/common/services/logger.service.js';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util.js';
import { isFunction, isSymbol } from '@nestjs/common/utils/shared.utils.js';
import { createHash } from 'crypto';
import _stringify from 'fast-safe-stringify';
import { ModuleOpaqueKeyFactory } from './interfaces/module-opaque-key-factory.interface.js';
import { type DynamicModule, type Type, Logger } from '@nestjs/common';
import {
randomStringGenerator,
isFunction,
isSymbol,
} from '@nestjs/common/internal';
// CJS interop: fast-safe-stringify sets module.exports.default = module.exports
const stringify = ((_stringify as any).default ?? _stringify) as unknown as (
value: any,

View File

@@ -1,6 +1,4 @@
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface.js';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface.js';
import { Type } from '@nestjs/common/interfaces/type.interface.js';
import type { DynamicModule, ForwardReference, Type } from '@nestjs/common';
export interface ModuleOpaqueKeyFactory {
/**

View File

@@ -1,4 +1,4 @@
import { Type } from '@nestjs/common';
import type { Type } from '@nestjs/common';
export class InitializeOnPreviewAllowlist {
private static readonly allowlist = new WeakMap<Type, boolean>();

View File

@@ -1,4 +1,4 @@
import { InjectionToken } from '@nestjs/common';
import type { InjectionToken } from '@nestjs/common';
type CommonEdgeMetadata = {
sourceModuleName: string;

View File

@@ -1,6 +1,6 @@
import { Type } from '@nestjs/common';
import { EnhancerSubtype } from '@nestjs/common/constants.js';
import type { Type } from '@nestjs/common';
import { InstanceWrapper } from '../../injector/instance-wrapper.js';
import type { EnhancerSubtype } from '@nestjs/common/internal';
export interface EnhancerMetadataCacheEntry {
targetNodeId?: string;

View File

@@ -1,5 +1,5 @@
import { RequestMethod } from '@nestjs/common';
import { VersionValue } from '@nestjs/common/interfaces/index.js';
import type { RequestMethod } from '@nestjs/common';
import type { VersionValue } from '@nestjs/common/internal';
export type HttpEntrypointMetadata = {
path: string;

View File

@@ -1,4 +1,4 @@
import { EnhancerSubtype } from '@nestjs/common/constants.js';
import type { EnhancerSubtype } from '@nestjs/common/internal';
/**
* Enhancers attached through APP_PIPE, APP_GUARD, APP_INTERCEPTOR, and APP_FILTER tokens.

View File

@@ -1,5 +1,5 @@
import { InjectionToken, Scope } from '@nestjs/common';
import { EnhancerSubtype } from '@nestjs/common/constants.js';
import type { InjectionToken, Scope } from '@nestjs/common';
import type { EnhancerSubtype } from '@nestjs/common/internal';
export type ModuleNode = {
metadata: {

View File

@@ -1,4 +1,4 @@
import { InjectionToken } from '@nestjs/common';
import type { InjectionToken } from '@nestjs/common';
import { ApplicationConfig } from '../application-config.js';
import { ExternalContextCreator } from '../helpers/external-context-creator.js';
import { HttpAdapterHost } from '../helpers/http-adapter-host.js';

View File

@@ -1,5 +1,5 @@
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util.js';
import { DeterministicUuidRegistry } from './deterministic-uuid-registry.js';
import { randomStringGenerator } from '@nestjs/common/internal';
export enum UuidFactoryMode {
Random = 'random',

View File

@@ -1,14 +1,10 @@
import { NestInterceptor, Type } from '@nestjs/common';
import {
CallHandler,
ContextType,
Controller,
} from '@nestjs/common/interfaces/index.js';
import { isEmpty } from '@nestjs/common/utils/shared.utils.js';
import type { NestInterceptor, Type } from '@nestjs/common';
import { AsyncResource } from 'async_hooks';
import { Observable, defer, from as fromPromise } from 'rxjs';
import { mergeAll, switchMap } from 'rxjs/operators';
import { ExecutionContextHost } from '../helpers/execution-context-host.js';
import type { CallHandler, ContextType } from '@nestjs/common';
import { type Controller, isEmpty } from '@nestjs/common/internal';
export class InterceptorsConsumer {
public async intercept<TContext extends string = ContextType>(

View File

@@ -1,16 +1,16 @@
import { INTERCEPTORS_METADATA } from '@nestjs/common/constants.js';
import {
Controller,
NestInterceptor,
Type,
} from '@nestjs/common/interfaces/index.js';
import { isEmpty, isFunction } from '@nestjs/common/utils/shared.utils.js';
import { iterate } from 'iterare';
import { ApplicationConfig } from '../application-config.js';
import { ContextCreator } from '../helpers/context-creator.js';
import { STATIC_CONTEXT } from '../injector/constants.js';
import { NestContainer } from '../injector/container.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import {
INTERCEPTORS_METADATA,
type Controller,
isEmpty,
isFunction,
} from '@nestjs/common/internal';
import type { NestInterceptor, Type } from '@nestjs/common';
export class InterceptorsContextCreator extends ContextCreator {
private moduleContext: string;

View File

@@ -0,0 +1,2 @@
export * from './module-definition.interface.js';
export * from './module-override.interface.js';

View File

@@ -1,5 +1,5 @@
import { DynamicModule, ForwardReference } from '@nestjs/common';
import { Type } from '@nestjs/common/interfaces/index.js';
import type { DynamicModule, ForwardReference } from '@nestjs/common';
import type { Type } from '@nestjs/common';
export type ModuleDefinition =
| ForwardReference

64
packages/core/internal.ts Normal file
View File

@@ -0,0 +1,64 @@
/**
* Internal module - not part of the public API.
* These exports are used by sibling @nestjs packages.
* Do not depend on these in your application code.
* @internal
* @module
*/
// Errors
export { RuntimeException } from './errors/exceptions/runtime.exception.js';
export { InvalidExceptionFilterException } from './errors/exceptions/invalid-exception-filter.exception.js';
// Constants
export { MESSAGES } from './constants.js';
// Scanner
export { DependenciesScanner } from './scanner.js';
// Injector
export { Injector, InjectorDependencyContext } from './injector/injector.js';
export { InstanceLoader } from './injector/instance-loader.js';
export { InstanceWrapper } from './injector/instance-wrapper.js';
export { Module } from './injector/module.js';
export { STATIC_CONTEXT } from './injector/constants.js';
// Helpers
export { ExecutionContextHost } from './helpers/execution-context-host.js';
export { ContextUtils, ParamProperties } from './helpers/context-utils.js';
export { HandlerMetadataStorage } from './helpers/handler-metadata-storage.js';
export { RouterMethodFactory } from './helpers/router-method-factory.js';
export { loadAdapter } from './helpers/load-adapter.js';
export { optionalRequire } from './helpers/optional-require.js';
// Helpers - interfaces
export { ParamsMetadata } from './helpers/interfaces/index.js';
// Guards
export { GuardsConsumer } from './guards/guards-consumer.js';
export { GuardsContextCreator } from './guards/guards-context-creator.js';
export { FORBIDDEN_MESSAGE } from './guards/constants.js';
// Pipes
export { PipesConsumer } from './pipes/pipes-consumer.js';
export { PipesContextCreator } from './pipes/pipes-context-creator.js';
export { ParamsTokenFactory } from './pipes/params-token-factory.js';
// Interceptors
export { InterceptorsConsumer } from './interceptors/interceptors-consumer.js';
export { InterceptorsContextCreator } from './interceptors/interceptors-context-creator.js';
// Exceptions
export { BaseExceptionFilterContext } from './exceptions/base-exception-filter-context.js';
// Router
export { LegacyRouteConverter } from './router/legacy-route-converter.js';
export { REQUEST_CONTEXT_ID } from './router/request/request-constants.js';
// Inspector
export { NoopGraphInspector } from './inspector/noop-graph-inspector.js';
export { UuidFactory, UuidFactoryMode } from './inspector/uuid-factory.js';
// Interfaces
export { ModuleDefinition } from './interfaces/module-definition.interface.js';
export { ModuleOverride } from './interfaces/module-override.interface.js';

View File

@@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common/interfaces/injectable.interface.js';
import {
type Injectable,
isConstructor,
isFunction,
isNil,
} from '@nestjs/common/utils/shared.utils.js';
} from '@nestjs/common/internal';
export class MetadataScanner {
private readonly cachedScannedPrototypes: Map<object, string[]> = new Map();

View File

@@ -1,18 +1,14 @@
import {
HttpServer,
MiddlewareConsumer,
Type,
} from '@nestjs/common/interfaces/index.js';
import {
MiddlewareConfigProxy,
MiddlewareConfiguration,
RouteInfo,
} from '@nestjs/common/interfaces/middleware/index.js';
import { stripEndSlash } from '@nestjs/common/utils/shared.utils.js';
import { iterate } from 'iterare';
import { RouteInfoPathExtractor } from './route-info-path-extractor.js';
import { RoutesMapper } from './routes-mapper.js';
import { filterMiddleware } from './utils.js';
import type { HttpServer, MiddlewareConsumer, Type } from '@nestjs/common';
import {
type MiddlewareConfigProxy,
type MiddlewareConfiguration,
type RouteInfo,
stripEndSlash,
} from '@nestjs/common/internal';
export class MiddlewareBuilder implements MiddlewareConsumer {
private readonly middlewareCollection = new Set<MiddlewareConfiguration>();

View File

@@ -1,9 +1,9 @@
import { InjectionToken, Type } from '@nestjs/common';
import { MiddlewareConfiguration } from '@nestjs/common/interfaces/middleware/middleware-configuration.interface.js';
import type { InjectionToken, Type } from '@nestjs/common';
import { getClassScope } from '../helpers/get-class-scope.js';
import { isDurable } from '../helpers/is-durable.js';
import { NestContainer } from '../injector/container.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import type { MiddlewareConfiguration } from '@nestjs/common/internal';
export class MiddlewareContainer {
private readonly middleware = new Map<

View File

@@ -1,12 +1,4 @@
import { HttpServer, InjectionToken, Logger } from '@nestjs/common';
import { RequestMethod } from '@nestjs/common/enums/request-method.enum.js';
import {
MiddlewareConfiguration,
NestMiddleware,
RouteInfo,
} from '@nestjs/common/interfaces/middleware/index.js';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface.js';
import { isUndefined } from '@nestjs/common/utils/shared.utils.js';
import { type HttpServer, type InjectionToken, Logger } from '@nestjs/common';
import { ApplicationConfig } from '../application-config.js';
import { InvalidMiddlewareException } from '../errors/exceptions/invalid-middleware.exception.js';
import { RuntimeException } from '../errors/exceptions/runtime.exception.js';
@@ -31,6 +23,13 @@ import { MiddlewareContainer } from './container.js';
import { MiddlewareResolver } from './resolver.js';
import { RouteInfoPathExtractor } from './route-info-path-extractor.js';
import { RoutesMapper } from './routes-mapper.js';
import { RequestMethod, type NestMiddleware } from '@nestjs/common';
import {
type MiddlewareConfiguration,
type RouteInfo,
type NestApplicationContextOptions,
isUndefined,
} from '@nestjs/common/internal';
export class MiddlewareModule<
TAppOptions extends NestApplicationContextOptions =

View File

@@ -1,4 +1,4 @@
import { InjectionToken } from '@nestjs/common';
import type { InjectionToken } from '@nestjs/common';
import { Injector } from '../injector/injector.js';
import { InstanceWrapper } from '../injector/instance-wrapper.js';
import { Module } from '../injector/module.js';

View File

@@ -1,17 +1,15 @@
import { VersioningType } from '@nestjs/common';
import {
RouteInfo,
VersioningOptions,
VersionValue,
} from '@nestjs/common/interfaces/index.js';
import {
addLeadingSlash,
stripEndSlash,
} from '@nestjs/common/utils/shared.utils.js';
import { ApplicationConfig } from '../application-config.js';
import { ExcludeRouteMetadata } from '../router/interfaces/exclude-route-metadata.interface.js';
import { isRouteExcluded } from '../router/utils/index.js';
import { RoutePathFactory } from './../router/route-path-factory.js';
import type { VersioningOptions } from '@nestjs/common';
import {
type RouteInfo,
type VersionValue,
addLeadingSlash,
stripEndSlash,
} from '@nestjs/common/internal';
export class RouteInfoPathExtractor {
private readonly routePathFactory: RoutePathFactory;

View File

@@ -1,25 +1,20 @@
import {
MODULE_PATH,
PATH_METADATA,
VERSION_METADATA,
} from '@nestjs/common/constants.js';
import {
RouteInfo,
Type,
VERSION_NEUTRAL,
VersionValue,
} from '@nestjs/common/interfaces/index.js';
import {
addLeadingSlash,
isString,
isUndefined,
} from '@nestjs/common/utils/shared.utils.js';
import { ApplicationConfig } from '../application-config.js';
import { NestContainer } from '../injector/container.js';
import { Module } from '../injector/module.js';
import { MetadataScanner } from '../metadata-scanner.js';
import { PathsExplorer, RouteDefinition } from '../router/paths-explorer.js';
import { targetModulesByContainer } from '../router/router-module.js';
import {
MODULE_PATH,
PATH_METADATA,
VERSION_METADATA,
type RouteInfo,
type VersionValue,
addLeadingSlash,
isString,
isUndefined,
} from '@nestjs/common/internal';
import { type Type, VERSION_NEUTRAL } from '@nestjs/common';
export class RoutesMapper {
private readonly pathsExplorer: PathsExplorer;

View File

@@ -1,20 +1,17 @@
import { RequestMethod } from '@nestjs/common';
import {
HttpServer,
RouteInfo,
Type,
} from '@nestjs/common/interfaces/index.js';
import {
addLeadingSlash,
isFunction,
isString,
} from '@nestjs/common/utils/shared.utils.js';
import { iterate } from 'iterare';
import { pathToRegexp } from 'path-to-regexp';
import { uid } from 'uid';
import { ExcludeRouteMetadata } from '../router/interfaces/exclude-route-metadata.interface.js';
import { LegacyRouteConverter } from '../router/legacy-route-converter.js';
import { isRouteExcluded } from '../router/utils/index.js';
import type { HttpServer, Type } from '@nestjs/common';
import {
type RouteInfo,
addLeadingSlash,
isFunction,
isString,
} from '@nestjs/common/internal';
export const mapToExcludeRoute = (
routes: (string | RouteInfo)[],

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