@nestjs/common 12.1.2 → 12.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants.d.ts +1 -0
- package/constants.js +1 -0
- package/decorators/core/inject.decorator.js +4 -1
- package/decorators/core/optional.decorator.js +1 -1
- package/decorators/http/create-route-param-metadata.decorator.js +25 -27
- package/decorators/http/sse.decorator.d.ts +41 -4
- package/decorators/http/sse.decorator.js +24 -5
- package/file-stream/streamable-file.js +11 -1
- package/interfaces/http/csrf-protection-options.interface.d.ts +7 -6
- package/interfaces/http/http-server.interface.d.ts +44 -6
- package/interfaces/middleware/middleware-config-proxy.interface.d.ts +5 -0
- package/interfaces/nest-application.interface.d.ts +8 -6
- package/internal.d.ts +1 -1
- package/module-utils/utils/get-injection-providers.util.js +4 -3
- package/package.json +3 -2
- package/pipes/default-value.pipe.d.ts +11 -0
- package/pipes/default-value.pipe.js +28 -2
- package/pipes/file/file-type.validator.d.ts +4 -0
- package/pipes/file/file-type.validator.js +9 -1
- package/pipes/parse-array.pipe.js +28 -5
- package/pipes/validation.pipe.d.ts +24 -2
- package/pipes/validation.pipe.js +104 -13
- package/serializer/class-serializer.interceptor.js +10 -2
- package/serializer/standard-schema-serializer.interceptor.js +10 -1
- package/services/console-logger.service.d.ts +14 -10
- package/services/console-logger.service.js +33 -5
- package/services/utils/redact.util.d.ts +5 -3
- package/services/utils/redact.util.js +22 -3
- package/utils/parameter-decorator-options.util.d.ts +3 -0
- package/utils/parameter-decorator-options.util.js +3 -0
- package/utils/strip-proto-keys.util.js +8 -4
package/constants.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export declare const HEADERS_METADATA = "__headers__";
|
|
|
35
35
|
export declare const REDIRECT_METADATA = "__redirect__";
|
|
36
36
|
export declare const RESPONSE_PASSTHROUGH_METADATA = "__responsePassthrough__";
|
|
37
37
|
export declare const SSE_METADATA = "__sse__";
|
|
38
|
+
export declare const SSE_OPTIONS_METADATA = "__sseOptions__";
|
|
38
39
|
export declare const VERSION_METADATA = "__version__";
|
|
39
40
|
export declare const INJECTABLE_WATERMARK = "__injectable__";
|
|
40
41
|
export declare const CONTROLLER_WATERMARK = "__controller__";
|
package/constants.js
CHANGED
|
@@ -34,6 +34,7 @@ export const HEADERS_METADATA = '__headers__';
|
|
|
34
34
|
export const REDIRECT_METADATA = '__redirect__';
|
|
35
35
|
export const RESPONSE_PASSTHROUGH_METADATA = '__responsePassthrough__';
|
|
36
36
|
export const SSE_METADATA = '__sse__';
|
|
37
|
+
export const SSE_OPTIONS_METADATA = '__sseOptions__';
|
|
37
38
|
export const VERSION_METADATA = '__version__';
|
|
38
39
|
export const INJECTABLE_WATERMARK = '__injectable__';
|
|
39
40
|
export const CONTROLLER_WATERMARK = '__controller__';
|
|
@@ -44,7 +44,10 @@ export function Inject(token) {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
let properties = Reflect.getMetadata(PROPERTY_DEPS_METADATA, target.constructor) || [];
|
|
47
|
-
properties = [
|
|
47
|
+
properties = [
|
|
48
|
+
...properties.filter(property => property.key !== key),
|
|
49
|
+
{ key, type },
|
|
50
|
+
];
|
|
48
51
|
Reflect.defineMetadata(PROPERTY_DEPS_METADATA, properties, target.constructor);
|
|
49
52
|
};
|
|
50
53
|
}
|
|
@@ -20,7 +20,7 @@ export function Optional() {
|
|
|
20
20
|
Reflect.defineMetadata(OPTIONAL_DEPS_METADATA, [...args, index], target);
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
-
const properties = Reflect.
|
|
23
|
+
const properties = Reflect.getMetadata(OPTIONAL_PROPERTY_DEPS_METADATA, target.constructor) || [];
|
|
24
24
|
Reflect.defineMetadata(OPTIONAL_PROPERTY_DEPS_METADATA, [...properties, key], target.constructor);
|
|
25
25
|
};
|
|
26
26
|
}
|
|
@@ -24,34 +24,32 @@ export function createParamDecorator(factory, enhancers = []) {
|
|
|
24
24
|
const hasParamData = isNil(data) || !isPipe(data);
|
|
25
25
|
const paramData = hasParamData ? data : undefined;
|
|
26
26
|
const paramPipes = hasParamData ? pipes : [data, ...pipes];
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
// Custom data may also contain schema or pipes properties, so only treat
|
|
28
|
+
// the first argument as options when those are its only keys and their
|
|
29
|
+
// values, if defined, are a Standard Schema and an array of pipes.
|
|
30
|
+
const isDataOptions = hasParamData &&
|
|
31
|
+
!isNil(data) &&
|
|
32
|
+
isParameterDecoratorOptions(data) &&
|
|
33
|
+
Object.keys(data).every(key => key === 'schema' || key === 'pipes') &&
|
|
34
|
+
(data.schema === undefined ||
|
|
35
|
+
isFunction(data.schema?.['~standard']?.validate)) &&
|
|
36
|
+
(data.pipes === undefined ||
|
|
37
|
+
(Array.isArray(data.pipes) &&
|
|
38
|
+
// Falsy entries are allowed, as the pipes context filters them out
|
|
39
|
+
data.pipes.every(pipe => isNil(pipe) || isPipe(pipe))));
|
|
40
|
+
const finalData = isDataOptions ? undefined : paramData;
|
|
33
41
|
let finalSchema;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
finalSchema = opts.schema;
|
|
46
|
-
finalPipes = [
|
|
47
|
-
...paramPipes.slice(0, -1),
|
|
48
|
-
...(opts.pipes ?? []),
|
|
49
|
-
];
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
finalData = paramData;
|
|
53
|
-
finalSchema = undefined;
|
|
54
|
-
finalPipes = paramPipes;
|
|
42
|
+
const finalPipes = [];
|
|
43
|
+
// Options objects may also appear among the pipes, at any position.
|
|
44
|
+
// Their pipes are merged in place, and the first schema found is used.
|
|
45
|
+
for (const arg of isDataOptions ? [data, ...paramPipes] : paramPipes) {
|
|
46
|
+
if (isParameterDecoratorOptions(arg)) {
|
|
47
|
+
finalSchema ??= arg.schema;
|
|
48
|
+
finalPipes.push(...(arg.pipes ?? []));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
finalPipes.push(arg);
|
|
52
|
+
}
|
|
55
53
|
}
|
|
56
54
|
Reflect.defineMetadata(ROUTE_ARGS_METADATA, assignCustomParameterMetadata(args, paramtype, index, factory, finalData, finalSchema, ...finalPipes), target.constructor, key);
|
|
57
55
|
enhancers.forEach(fn => fn(target, key, index));
|
|
@@ -1,10 +1,47 @@
|
|
|
1
|
-
import { METHOD_METADATA } from '../../constants.js';
|
|
2
1
|
import { RequestMethod } from '../../enums/request-method.enum.js';
|
|
2
|
+
/**
|
|
3
|
+
* Interface defining options that can be passed to `@Sse()` decorator
|
|
4
|
+
*
|
|
5
|
+
* @publicApi
|
|
6
|
+
*/
|
|
7
|
+
export interface SseOptions {
|
|
8
|
+
/**
|
|
9
|
+
* HTTP request method of the route (default: `RequestMethod.GET`).
|
|
10
|
+
*/
|
|
11
|
+
method?: RequestMethod;
|
|
12
|
+
/**
|
|
13
|
+
* Maximum number of characters of event data (`data` plus `comment`) that
|
|
14
|
+
* may wait for a client that is not reading its events (default:
|
|
15
|
+
* 4 * 1024 * 1024, i.e. 4M characters). Events wait once the stream and
|
|
16
|
+
* socket buffers are full, as the producer cannot be paused. Past this
|
|
17
|
+
* limit, the connection is dropped as if the client had disconnected, and
|
|
18
|
+
* a warning is logged. Set to 0 or `Infinity` to disable.
|
|
19
|
+
*/
|
|
20
|
+
maxPendingDataLength?: number;
|
|
21
|
+
}
|
|
3
22
|
/**
|
|
4
23
|
* Declares this route as a Server-Sent-Events endpoint
|
|
5
24
|
*
|
|
25
|
+
* Events wait in memory for a client that is not reading them. Once more than
|
|
26
|
+
* `maxPendingDataLength` characters of event data (4M by default) wait for a
|
|
27
|
+
* client whose stream is backed up, its connection is dropped. An
|
|
28
|
+
* `EventSource` then reconnects, sending the id of the last event it received.
|
|
29
|
+
* Raise the limit for a route that streams bulk data faster than its clients
|
|
30
|
+
* read, or set it to 0 or `Infinity` to lift it.
|
|
31
|
+
*
|
|
32
|
+
* @param path route path (default: `/`)
|
|
33
|
+
* @param options `SseOptions` object specifying:
|
|
34
|
+
* - `method` - HTTP request method of the route (default: `GET`).
|
|
35
|
+
* - `maxPendingDataLength` - maximum number of characters of event data that
|
|
36
|
+
* may wait for a client that is not reading them (default: 4M). 0 or
|
|
37
|
+
* `Infinity` lifts the limit.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* @Sse('logs', { maxPendingDataLength: 64 * 1024 * 1024 })
|
|
42
|
+
* logs(): Observable<MessageEvent> { ... }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
6
45
|
* @publicApi
|
|
7
46
|
*/
|
|
8
|
-
export declare function Sse(path?: string, options?:
|
|
9
|
-
[METHOD_METADATA]?: RequestMethod;
|
|
10
|
-
}): MethodDecorator;
|
|
47
|
+
export declare function Sse(path?: string, options?: SseOptions): MethodDecorator;
|
|
@@ -1,18 +1,37 @@
|
|
|
1
|
-
import { METHOD_METADATA, PATH_METADATA, SSE_METADATA, } from '../../constants.js';
|
|
1
|
+
import { METHOD_METADATA, PATH_METADATA, SSE_METADATA, SSE_OPTIONS_METADATA, } from '../../constants.js';
|
|
2
2
|
import { RequestMethod } from '../../enums/request-method.enum.js';
|
|
3
3
|
/**
|
|
4
4
|
* Declares this route as a Server-Sent-Events endpoint
|
|
5
5
|
*
|
|
6
|
+
* Events wait in memory for a client that is not reading them. Once more than
|
|
7
|
+
* `maxPendingDataLength` characters of event data (4M by default) wait for a
|
|
8
|
+
* client whose stream is backed up, its connection is dropped. An
|
|
9
|
+
* `EventSource` then reconnects, sending the id of the last event it received.
|
|
10
|
+
* Raise the limit for a route that streams bulk data faster than its clients
|
|
11
|
+
* read, or set it to 0 or `Infinity` to lift it.
|
|
12
|
+
*
|
|
13
|
+
* @param path route path (default: `/`)
|
|
14
|
+
* @param options `SseOptions` object specifying:
|
|
15
|
+
* - `method` - HTTP request method of the route (default: `GET`).
|
|
16
|
+
* - `maxPendingDataLength` - maximum number of characters of event data that
|
|
17
|
+
* may wait for a client that is not reading them (default: 4M). 0 or
|
|
18
|
+
* `Infinity` lifts the limit.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* @Sse('logs', { maxPendingDataLength: 64 * 1024 * 1024 })
|
|
23
|
+
* logs(): Observable<MessageEvent> { ... }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
6
26
|
* @publicApi
|
|
7
27
|
*/
|
|
8
|
-
export function Sse(path, options = {
|
|
9
|
-
[METHOD_METADATA]: RequestMethod.GET,
|
|
10
|
-
}) {
|
|
28
|
+
export function Sse(path, options = {}) {
|
|
11
29
|
return (target, key, descriptor) => {
|
|
12
30
|
path = path && path.length ? path : '/';
|
|
13
31
|
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
|
|
14
|
-
Reflect.defineMetadata(METHOD_METADATA, options[METHOD_METADATA], descriptor.value);
|
|
32
|
+
Reflect.defineMetadata(METHOD_METADATA, options[METHOD_METADATA] ?? RequestMethod.GET, descriptor.value);
|
|
15
33
|
Reflect.defineMetadata(SSE_METADATA, true, descriptor.value);
|
|
34
|
+
Reflect.defineMetadata(SSE_OPTIONS_METADATA, { maxPendingDataLength: options.maxPendingDataLength }, descriptor.value);
|
|
16
35
|
return descriptor;
|
|
17
36
|
};
|
|
18
37
|
}
|
|
@@ -20,8 +20,11 @@ export class StreamableFile {
|
|
|
20
20
|
res.end();
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
+
// The message may name the path the stream reads from, so it is
|
|
24
|
+
// logged rather than sent.
|
|
25
|
+
this.logError(err);
|
|
23
26
|
res.statusCode = HttpStatus.BAD_REQUEST;
|
|
24
|
-
res.send(
|
|
27
|
+
res.send('Bad Request');
|
|
25
28
|
};
|
|
26
29
|
logError = (err) => {
|
|
27
30
|
this.logger.error(err);
|
|
@@ -39,6 +42,13 @@ export class StreamableFile {
|
|
|
39
42
|
}
|
|
40
43
|
else if (bufferOrReadStream.pipe && isFunction(bufferOrReadStream.pipe)) {
|
|
41
44
|
this.stream = bufferOrReadStream;
|
|
45
|
+
// Without a listener, an 'error' emitted before the adapter attaches
|
|
46
|
+
// its own (e.g. ENOENT from `fs.createReadStream()` while an
|
|
47
|
+
// interceptor awaits), or by a file that is never sent, would bring
|
|
48
|
+
// the process down.
|
|
49
|
+
if (isFunction(this.stream.on)) {
|
|
50
|
+
this.stream.on('error', () => { });
|
|
51
|
+
}
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
54
|
getStream() {
|
|
@@ -36,12 +36,13 @@ export interface CsrfProtectionOptions<TRequest = any> {
|
|
|
36
36
|
* object. Only consulted for requests that would otherwise be rejected.
|
|
37
37
|
*
|
|
38
38
|
* Routes are declared like `MiddlewareConsumer.exclude()`: a path, or
|
|
39
|
-
* `{ path, method, version? }` to narrow by method
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* `{ path, method, version? }` to narrow by method and version (the
|
|
40
|
+
* version segment of the path with URI versioning, the version the request
|
|
41
|
+
* asks for otherwise), without the global prefix, which is added unless
|
|
42
|
+
* the route is excluded from it. Unlike `exclude()`, the match is exact:
|
|
43
|
+
* case-sensitive, without an optional trailing slash, and never for
|
|
44
|
+
* non-canonical request paths (`//`, dot segments, `;`, encoded `/` or
|
|
45
|
+
* `.`), which a router could resolve to a route that is not excluded.
|
|
45
46
|
*/
|
|
46
47
|
exclude?: (string | RouteInfo)[] | ((request: TRequest) => boolean);
|
|
47
48
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RequestMethod } from '../../enums/index.js';
|
|
2
|
+
import { RouteInfo } from '../middleware/middleware-configuration.interface.js';
|
|
2
3
|
import { NestApplicationOptions } from '../../interfaces/nest-application-options.interface.js';
|
|
3
4
|
import { VersionValue, VersioningOptions } from '../version-options.interface.js';
|
|
4
5
|
import { CookieSerializeOptions } from './cookie-options.interface.js';
|
|
@@ -51,6 +52,22 @@ export type SecurityRequestHook<TRequest = any> = (request: TRequest, response:
|
|
|
51
52
|
setHeader(name: string, value: string): unknown;
|
|
52
53
|
removeHeader(name: string): unknown;
|
|
53
54
|
}) => Error | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* How a router matches request paths against route paths, returned by
|
|
57
|
+
* {@link HttpServer.getRouteMatchingOptions}.
|
|
58
|
+
*
|
|
59
|
+
* @publicApi
|
|
60
|
+
*/
|
|
61
|
+
export interface RouteMatchingOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Whether `/Users` and `/users` are different paths.
|
|
64
|
+
*/
|
|
65
|
+
caseSensitive: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Whether `/users/` is served by the `/users` route.
|
|
68
|
+
*/
|
|
69
|
+
ignoreTrailingSlash: boolean;
|
|
70
|
+
}
|
|
54
71
|
/**
|
|
55
72
|
* Contract between the Nest core (`NestApplication`, the router, the
|
|
56
73
|
* middleware module and the exception layer) and an HTTP platform such as
|
|
@@ -419,8 +436,14 @@ export interface HttpServer<TRequest = any, TResponse = any, ServerInstance = an
|
|
|
419
436
|
*
|
|
420
437
|
* @param handler The `(req, res, next)` callback.
|
|
421
438
|
* @param prefix The global prefix (`app.setGlobalPrefix()`), when set.
|
|
439
|
+
* @param excludedRoutes The routes `app.setGlobalPrefix()` excluded from the
|
|
440
|
+
* prefix, when any. They live at the root, so an adapter that scopes the
|
|
441
|
+
* handler by `prefix` should also mount it on each of these paths, for
|
|
442
|
+
* every method: with `exclude: ['hello']` and only `GET /hello` declared,
|
|
443
|
+
* `POST /hello` is still an unmatched request for a route Nest owns. Paths
|
|
444
|
+
* arrive already converted to the framework's route syntax.
|
|
422
445
|
*/
|
|
423
|
-
setNotFoundHandler?(handler: Function, prefix?: string): any;
|
|
446
|
+
setNotFoundHandler?(handler: Function, prefix?: string, excludedRoutes?: RouteInfo[]): any;
|
|
424
447
|
/**
|
|
425
448
|
* Serves static files; pass-through for `app.useStaticAssets()`. The
|
|
426
449
|
* arguments are platform-specific. When not implemented,
|
|
@@ -517,11 +540,15 @@ export interface HttpServer<TRequest = any, TResponse = any, ServerInstance = an
|
|
|
517
540
|
* features is enabled. The hook must run for every request (matched routes,
|
|
518
541
|
* unmatched requests, routes outside the global prefix) before Nest
|
|
519
542
|
* middleware, guards and route handlers, and before body parsing when the
|
|
520
|
-
* platform allows it
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
543
|
+
* platform allows it. Registering a framework-level middleware at call time
|
|
544
|
+
* satisfies that on Express. An adapter whose {@link HttpServer.init} sets
|
|
545
|
+
* up what runs Nest middleware cannot register at call time, as
|
|
546
|
+
* `NestFactory.create()` calls `init()` first: `FastifyAdapter` adds its
|
|
547
|
+
* request hook in the constructor, and only stores the hook here.
|
|
548
|
+
*
|
|
549
|
+
* The hook receives the request and the Node.js `ServerResponse`
|
|
550
|
+
* (`reply.raw` on Fastify), on which it may set headers that route
|
|
551
|
+
* handlers, `@Header()` and exception filters can still override.
|
|
525
552
|
*
|
|
526
553
|
* When the hook returns an error, the adapter must not continue to the
|
|
527
554
|
* route: it hands the error to the exception layer installed through
|
|
@@ -626,4 +653,15 @@ export interface HttpServer<TRequest = any, TResponse = any, ServerInstance = an
|
|
|
626
653
|
* `(method, path)` registrations itself.
|
|
627
654
|
*/
|
|
628
655
|
isRouteOrderSensitive?(): boolean;
|
|
656
|
+
/**
|
|
657
|
+
* Tells the core how the underlying router matches request paths, so that
|
|
658
|
+
* the routes excluded with `MiddlewareConsumer.exclude()` are matched the
|
|
659
|
+
* same way: a request only skips the middleware when the router serves it
|
|
660
|
+
* with the excluded route. Called once per `forRoutes()` call, during
|
|
661
|
+
* `app.init()`. When not implemented, excluded routes are matched exactly:
|
|
662
|
+
* case-sensitively and without an optional trailing slash. With a more
|
|
663
|
+
* lenient router, a request for `/Users/login/` then runs the middleware
|
|
664
|
+
* excluded for `users/login` rather than skipping it.
|
|
665
|
+
*/
|
|
666
|
+
getRouteMatchingOptions?(): RouteMatchingOptions;
|
|
629
667
|
}
|
|
@@ -8,6 +8,11 @@ export interface MiddlewareConfigProxy {
|
|
|
8
8
|
/**
|
|
9
9
|
* Routes to exclude from the current middleware.
|
|
10
10
|
*
|
|
11
|
+
* A route given with a `version` is only excluded for the requests for that
|
|
12
|
+
* version: with URI versioning, through the version segment of its path,
|
|
13
|
+
* and with header, media type or custom versioning, by the version the
|
|
14
|
+
* request asks for.
|
|
15
|
+
*
|
|
11
16
|
* @param {(string | RouteInfo)[]} routes
|
|
12
17
|
* @returns {MiddlewareConfigProxy}
|
|
13
18
|
*/
|
|
@@ -40,9 +40,10 @@ export interface INestApplication<TServer = any> extends INestApplicationContext
|
|
|
40
40
|
*
|
|
41
41
|
* Must be called once, before `app.init()` / `app.listen()`. The check runs
|
|
42
42
|
* before Nest middleware, body parsing, guards and handlers. It shares one
|
|
43
|
-
* request hook with `app.useSecurityHeaders()
|
|
44
|
-
* of the two is called
|
|
45
|
-
* runs before the check.
|
|
43
|
+
* request hook with `app.useSecurityHeaders()`. On Express, the hook is
|
|
44
|
+
* registered where the first of the two is called, so middleware registered
|
|
45
|
+
* with `app.use()` before that runs before the check. On Fastify, the check
|
|
46
|
+
* runs before all middleware.
|
|
46
47
|
*
|
|
47
48
|
* @param {CsrfProtectionOptions} options
|
|
48
49
|
* @returns {this}
|
|
@@ -60,9 +61,10 @@ export interface INestApplication<TServer = any> extends INestApplicationContext
|
|
|
60
61
|
* route.
|
|
61
62
|
*
|
|
62
63
|
* Must be called once, before `app.init()` / `app.listen()`. It shares one
|
|
63
|
-
* request hook with `app.enableCsrfProtection()
|
|
64
|
-
* first of the two is called
|
|
65
|
-
* that runs first,
|
|
64
|
+
* request hook with `app.enableCsrfProtection()`. On Express, the hook is
|
|
65
|
+
* registered where the first of the two is called, so middleware registered
|
|
66
|
+
* with `app.use()` before that runs first, and responses it ends itself do
|
|
67
|
+
* not carry the headers. On Fastify, the hook runs before all middleware.
|
|
66
68
|
*
|
|
67
69
|
* @param {SecurityHeadersOptions} options
|
|
68
70
|
* @returns {this}
|
package/internal.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type { MiddlewareConfiguration, RouteInfo, } from './interfaces/middlewar
|
|
|
25
25
|
export type { MiddlewareConfigProxy } from './interfaces/middleware/middleware-config-proxy.interface.js';
|
|
26
26
|
export type { ModuleMetadata } from './interfaces/modules/module-metadata.interface.js';
|
|
27
27
|
export type { HttpArgumentsHost, RpcArgumentsHost, WsArgumentsHost, } from './interfaces/features/arguments-host.interface.js';
|
|
28
|
-
export type { RequestHandler, SecurityRequestHook, } from './interfaces/http/http-server.interface.js';
|
|
28
|
+
export type { RequestHandler, RouteMatchingOptions, SecurityRequestHook, } from './interfaces/http/http-server.interface.js';
|
|
29
29
|
export type { GetOrResolveOptions, SelectOptions, } from './interfaces/nest-application-context.interface.js';
|
|
30
30
|
export type { ShutdownHooksOptions } from './interfaces/shutdown-hooks-options.interface.js';
|
|
31
31
|
export { assignMetadata } from './decorators/http/route-params.decorator.js';
|
|
@@ -22,10 +22,11 @@ export function getInjectionProviders(providers, tokens) {
|
|
|
22
22
|
const match = (providers ?? []).filter(p => !result.includes(p) && // this prevents circular loops and duplication
|
|
23
23
|
(search.includes(p) || search.includes(p?.provide)));
|
|
24
24
|
result.push(...match);
|
|
25
|
-
//
|
|
25
|
+
// Follow factory dependencies and aliases to existing providers.
|
|
26
26
|
search = match
|
|
27
|
-
.
|
|
28
|
-
|
|
27
|
+
.flatMap(p => p?.useExisting !== undefined
|
|
28
|
+
? [p.useExisting]
|
|
29
|
+
: (p?.inject ?? []))
|
|
29
30
|
.map(mapInjectToTokens);
|
|
30
31
|
}
|
|
31
32
|
return result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs/common",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.2.0",
|
|
4
4
|
"description": "Nest - modern, fast, powerful node.js web framework (@common)",
|
|
5
5
|
"author": "Kamil Mysliwiec",
|
|
6
6
|
"homepage": "https://nestjs.com",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"main": "./index.js",
|
|
22
22
|
"exports": {
|
|
23
23
|
".": "./index.js",
|
|
24
|
+
"./package.json": "./package.json",
|
|
24
25
|
"./internal": "./internal.js",
|
|
25
26
|
"./*.js": "./*.js",
|
|
26
27
|
"./*": "./*.js"
|
|
@@ -47,5 +48,5 @@
|
|
|
47
48
|
"optional": true
|
|
48
49
|
}
|
|
49
50
|
},
|
|
50
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "af37111368de7fd2dfab0bdec2f530c7845510c2"
|
|
51
52
|
}
|
|
@@ -2,6 +2,11 @@ import { ArgumentMetadata, PipeTransform } from '../interfaces/features/pipe-tra
|
|
|
2
2
|
/**
|
|
3
3
|
* Defines the built-in DefaultValue Pipe
|
|
4
4
|
*
|
|
5
|
+
* A default value that is an array or a plain object is copied on every call
|
|
6
|
+
* (along with the arrays and plain objects nested in it), so that a handler
|
|
7
|
+
* that mutates its argument does not change it for subsequent requests. Any
|
|
8
|
+
* other object (e.g., a class instance, a `Date` or a `Map`) is returned as is.
|
|
9
|
+
*
|
|
5
10
|
* @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes)
|
|
6
11
|
*
|
|
7
12
|
* @publicApi
|
|
@@ -10,4 +15,10 @@ export declare class DefaultValuePipe<T = any, R = any> implements PipeTransform
|
|
|
10
15
|
protected readonly defaultValue: R;
|
|
11
16
|
constructor(defaultValue: R);
|
|
12
17
|
transform(value?: T, _metadata?: ArgumentMetadata): T | R;
|
|
18
|
+
/**
|
|
19
|
+
* Copies `value` if it is an array or a plain object, along with the arrays
|
|
20
|
+
* and plain objects nested in it. `copies` maps the objects copied so far
|
|
21
|
+
* to their copies, so that shared and circular references are preserved.
|
|
22
|
+
*/
|
|
23
|
+
protected copyDefaultValue<V>(value: V, copies?: Map<unknown, unknown>): V;
|
|
13
24
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
2
|
import { Injectable } from '../decorators/core/injectable.decorator.js';
|
|
3
|
-
import { isNil, isNumber } from '../utils/shared.utils.js';
|
|
3
|
+
import { isNil, isNumber, isPlainObject } from '../utils/shared.utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Defines the built-in DefaultValue Pipe
|
|
6
6
|
*
|
|
7
|
+
* A default value that is an array or a plain object is copied on every call
|
|
8
|
+
* (along with the arrays and plain objects nested in it), so that a handler
|
|
9
|
+
* that mutates its argument does not change it for subsequent requests. Any
|
|
10
|
+
* other object (e.g., a class instance, a `Date` or a `Map`) is returned as is.
|
|
11
|
+
*
|
|
7
12
|
* @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes)
|
|
8
13
|
*
|
|
9
14
|
* @publicApi
|
|
@@ -16,10 +21,31 @@ let DefaultValuePipe = class DefaultValuePipe {
|
|
|
16
21
|
transform(value, _metadata) {
|
|
17
22
|
if (isNil(value) ||
|
|
18
23
|
(isNumber(value) && isNaN(value))) {
|
|
19
|
-
return this.defaultValue;
|
|
24
|
+
return this.copyDefaultValue(this.defaultValue);
|
|
20
25
|
}
|
|
21
26
|
return value;
|
|
22
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Copies `value` if it is an array or a plain object, along with the arrays
|
|
30
|
+
* and plain objects nested in it. `copies` maps the objects copied so far
|
|
31
|
+
* to their copies, so that shared and circular references are preserved.
|
|
32
|
+
*/
|
|
33
|
+
copyDefaultValue(value, copies) {
|
|
34
|
+
const isArray = Array.isArray(value) && Object.getPrototypeOf(value) === Array.prototype;
|
|
35
|
+
if (!isArray && !isPlainObject(value)) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
copies ??= new Map();
|
|
39
|
+
if (copies.has(value)) {
|
|
40
|
+
return copies.get(value);
|
|
41
|
+
}
|
|
42
|
+
const copy = Object.assign(isArray ? [] : Object.create(Object.getPrototypeOf(value)), value);
|
|
43
|
+
copies.set(value, copy);
|
|
44
|
+
for (const key of Reflect.ownKeys(copy)) {
|
|
45
|
+
copy[key] = this.copyDefaultValue(copy[key], copies);
|
|
46
|
+
}
|
|
47
|
+
return copy;
|
|
48
|
+
}
|
|
23
49
|
};
|
|
24
50
|
DefaultValuePipe = __decorate([
|
|
25
51
|
Injectable(),
|
|
@@ -7,6 +7,10 @@ export type FileTypeValidatorOptions = {
|
|
|
7
7
|
* Expected file type(s) for validation. Can be a string (MIME type)
|
|
8
8
|
* or a regular expression to match multiple types.
|
|
9
9
|
*
|
|
10
|
+
* A string matches a MIME type that contains it, up to the end of a name:
|
|
11
|
+
* 'image/png' or 'png' matches 'image/png', and 'audio/wav' does not match
|
|
12
|
+
* 'audio/wavpack'.
|
|
13
|
+
*
|
|
10
14
|
* @example
|
|
11
15
|
* // Match a single MIME type
|
|
12
16
|
* fileType: 'image/png'
|
|
@@ -107,6 +107,14 @@ export class FileTypeValidator extends FileValidator {
|
|
|
107
107
|
if (typeof fileType === 'string' && mimetype === fileType) {
|
|
108
108
|
return true;
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
if (typeof fileType === 'string') {
|
|
111
|
+
// A string that ends in a letter or a digit must match up to the end
|
|
112
|
+
// of a name, or "audio/wav" would accept "audio/wavpack" and
|
|
113
|
+
// "font/woff" would accept "font/woff2".
|
|
114
|
+
return !!mimetype.match(/[a-z\d]$/i.test(fileType) ? `(?:${fileType})(?![\\w-])` : fileType);
|
|
115
|
+
}
|
|
116
|
+
// Stateful expressions must start at zero for each file, without changing
|
|
117
|
+
// the caller's lastIndex (the validator can be reused across uploads).
|
|
118
|
+
return !!mimetype.match(new RegExp(fileType));
|
|
111
119
|
}
|
|
112
120
|
}
|
|
@@ -66,7 +66,9 @@ let ParseArrayPipe = class ParseArrayPipe {
|
|
|
66
66
|
};
|
|
67
67
|
const isExpectedTypePrimitive = this.isExpectedTypePrimitive();
|
|
68
68
|
const toClassInstance = (item, index) => {
|
|
69
|
-
|
|
69
|
+
// `JSON.parse` stringifies its argument first, so an array holding a
|
|
70
|
+
// single item (`[1]` becomes `'1'`) would be parsed as that item.
|
|
71
|
+
if (this.options.items !== String && isString(item)) {
|
|
70
72
|
try {
|
|
71
73
|
item = JSON.parse(item);
|
|
72
74
|
}
|
|
@@ -82,8 +84,8 @@ let ParseArrayPipe = class ParseArrayPipe {
|
|
|
82
84
|
if (this.options.stopAtFirstError === false) {
|
|
83
85
|
// strict compare to "false" to make sure
|
|
84
86
|
// that this option is disabled by default
|
|
85
|
-
|
|
86
|
-
const targetArray = value;
|
|
87
|
+
const errors = [];
|
|
88
|
+
const targetArray = Array.from(value);
|
|
87
89
|
for (let i = 0; i < targetArray.length; i++) {
|
|
88
90
|
try {
|
|
89
91
|
targetArray[i] = await toClassInstance(targetArray[i]);
|
|
@@ -102,7 +104,17 @@ let ParseArrayPipe = class ParseArrayPipe {
|
|
|
102
104
|
else {
|
|
103
105
|
message = err;
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
// Appended in place, since `concat()` would copy all the errors so
|
|
108
|
+
// far for each invalid item, and one at a time, since one item can
|
|
109
|
+
// have more errors than `push(...message)` takes arguments.
|
|
110
|
+
if (Array.isArray(message)) {
|
|
111
|
+
for (const item of message) {
|
|
112
|
+
errors.push(item);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
errors.push(message);
|
|
117
|
+
}
|
|
106
118
|
}
|
|
107
119
|
}
|
|
108
120
|
if (errors.length > 0) {
|
|
@@ -111,7 +123,18 @@ let ParseArrayPipe = class ParseArrayPipe {
|
|
|
111
123
|
return targetArray;
|
|
112
124
|
}
|
|
113
125
|
else {
|
|
114
|
-
|
|
126
|
+
// One item at a time, up to the first that fails: validating them all
|
|
127
|
+
// at once kept the validation and the exception of every item alive
|
|
128
|
+
// together, and a string splits into up to one item per character.
|
|
129
|
+
const targetArray = value;
|
|
130
|
+
const items = [];
|
|
131
|
+
for (let i = 0; i < targetArray.length; i++) {
|
|
132
|
+
// As with `map()`, the holes of a sparse array are not validated.
|
|
133
|
+
items.push(i in targetArray
|
|
134
|
+
? await toClassInstance(targetArray[i], i)
|
|
135
|
+
: undefined);
|
|
136
|
+
}
|
|
137
|
+
value = items;
|
|
115
138
|
}
|
|
116
139
|
}
|
|
117
140
|
return value;
|
|
@@ -35,6 +35,16 @@ export interface ValidationPipeOptions extends ValidatorOptions {
|
|
|
35
35
|
* If you have exception filters or interceptors that assume `message` is always an array, they will need to be updated.
|
|
36
36
|
*/
|
|
37
37
|
errorFormat?: ValidationErrorFormat;
|
|
38
|
+
/**
|
|
39
|
+
* Maximum total length, in characters, of the messages in an error response
|
|
40
|
+
* of the built-in exception factory, counting the property path that comes
|
|
41
|
+
* with each message. The errors past it are left out, and a last message
|
|
42
|
+
* (in the 'grouped' format, a '...' entry) says so. Set it to `Infinity` to
|
|
43
|
+
* lift the limit.
|
|
44
|
+
*
|
|
45
|
+
* @default 1_000_000
|
|
46
|
+
*/
|
|
47
|
+
maxErrorMessagesLength?: number;
|
|
38
48
|
}
|
|
39
49
|
/**
|
|
40
50
|
* @see [Validation](https://docs.nestjs.com/techniques/validation)
|
|
@@ -51,19 +61,31 @@ export declare class ValidationPipe implements PipeTransform {
|
|
|
51
61
|
protected exceptionFactory: (errors: ValidationError[]) => any;
|
|
52
62
|
protected validateCustomDecorators: boolean;
|
|
53
63
|
protected errorFormat: ValidationErrorFormat;
|
|
64
|
+
protected maxErrorMessagesLength: number;
|
|
54
65
|
constructor(options?: ValidationPipeOptions);
|
|
55
66
|
protected loadValidator(validatorPackage?: ValidatorPackage): ValidatorPackage | Promise<ValidatorPackage>;
|
|
56
67
|
protected loadTransformer(transformerPackage?: TransformerPackage): TransformerPackage | Promise<TransformerPackage>;
|
|
57
68
|
transform(value: unknown, metadata: ArgumentMetadata): Promise<any>;
|
|
58
69
|
createExceptionFactory(): (validationErrors?: ValidationError[]) => unknown;
|
|
70
|
+
/**
|
|
71
|
+
* Leaves out the errors that come after the first "maxErrorMessagesLength"
|
|
72
|
+
* characters of messages. Each message counts with the path of its
|
|
73
|
+
* property, which is what makes the response to a deeply nested payload
|
|
74
|
+
* grow faster than the payload. The errors past the limit are not visited.
|
|
75
|
+
*/
|
|
76
|
+
protected limitValidationErrors(validationErrors: ValidationError[], budget: {
|
|
77
|
+
remaining: number;
|
|
78
|
+
isExceeded: boolean;
|
|
79
|
+
}, parentPathLength?: number): ValidationError[];
|
|
59
80
|
protected toValidate(metadata: ArgumentMetadata): boolean;
|
|
60
81
|
protected transformPrimitive(value: unknown, metadata: ArgumentMetadata): unknown;
|
|
61
82
|
protected toEmptyIfNil<T = any, R = T>(value: unknown, metatype: Type<unknown> | object): R | object | string;
|
|
62
83
|
protected isPrimitive(value: unknown): boolean;
|
|
63
84
|
protected stripProtoKeys(value: any): void;
|
|
64
85
|
protected validate(object: object, validatorOptions?: ValidatorOptions): Promise<ValidationError[]> | ValidationError[];
|
|
86
|
+
protected hasValidationRules(metatype: Type<unknown>): Promise<boolean>;
|
|
65
87
|
protected flattenValidationErrors(validationErrors: ValidationError[]): string[];
|
|
66
|
-
protected groupValidationErrors(validationErrors: ValidationError[], parentPath?: string): Record<string, string[]>;
|
|
67
|
-
protected mapChildrenToValidationErrors(error: ValidationError, parentPath?: string): ValidationError[];
|
|
88
|
+
protected groupValidationErrors(validationErrors: ValidationError[], parentPath?: string, result?: Record<string, string[]>): Record<string, string[]>;
|
|
89
|
+
protected mapChildrenToValidationErrors(error: ValidationError, parentPath?: string, validationErrors?: ValidationError[]): ValidationError[];
|
|
68
90
|
protected prependConstraintsWithParentProp(parentPath: string, error: ValidationError): ValidationError;
|
|
69
91
|
}
|
package/pipes/validation.pipe.js
CHANGED
|
@@ -7,6 +7,8 @@ import { HttpErrorByCode, } from '../utils/http-error-by-code.util.js';
|
|
|
7
7
|
import { loadPackage } from '../utils/load-package.util.js';
|
|
8
8
|
import { isNil, isUndefined } from '../utils/shared.utils.js';
|
|
9
9
|
import { stripProtoKeys } from '../utils/strip-proto-keys.util.js';
|
|
10
|
+
const DEFAULT_MAX_ERROR_MESSAGES_LENGTH = 1_000_000;
|
|
11
|
+
const OMITTED_ERRORS_MESSAGE = 'Further validation errors were omitted';
|
|
10
12
|
let classValidator = {};
|
|
11
13
|
let classTransformer = {};
|
|
12
14
|
/**
|
|
@@ -24,9 +26,10 @@ let ValidationPipe = class ValidationPipe {
|
|
|
24
26
|
exceptionFactory;
|
|
25
27
|
validateCustomDecorators;
|
|
26
28
|
errorFormat;
|
|
29
|
+
maxErrorMessagesLength;
|
|
27
30
|
constructor(options) {
|
|
28
31
|
options = options || {};
|
|
29
|
-
const { transform, disableErrorMessages, errorHttpStatusCode, expectedType, transformOptions, exceptionFactory, validatorPackage, transformerPackage, validateCustomDecorators, errorFormat, ...validatorOptions } = options;
|
|
32
|
+
const { transform, disableErrorMessages, errorHttpStatusCode, expectedType, transformOptions, exceptionFactory, validatorPackage, transformerPackage, validateCustomDecorators, errorFormat, maxErrorMessagesLength, ...validatorOptions } = options;
|
|
30
33
|
// @see [https://github.com/nestjs/nest/issues/10683#issuecomment-1413690508](https://github.com/nestjs/nest/issues/10683#issuecomment-1413690508)
|
|
31
34
|
this.validatorOptions = { forbidUnknownValues: false, ...validatorOptions };
|
|
32
35
|
this.isTransformEnabled = !!transform;
|
|
@@ -36,6 +39,8 @@ let ValidationPipe = class ValidationPipe {
|
|
|
36
39
|
this.errorHttpStatusCode = errorHttpStatusCode || HttpStatus.BAD_REQUEST;
|
|
37
40
|
this.expectedType = expectedType;
|
|
38
41
|
this.errorFormat = errorFormat || 'list';
|
|
42
|
+
this.maxErrorMessagesLength =
|
|
43
|
+
maxErrorMessagesLength ?? DEFAULT_MAX_ERROR_MESSAGES_LENGTH;
|
|
39
44
|
this.exceptionFactory = exceptionFactory || this.createExceptionFactory();
|
|
40
45
|
classValidator = this.loadValidator(validatorPackage);
|
|
41
46
|
classTransformer = this.loadTransformer(transformerPackage);
|
|
@@ -77,10 +82,33 @@ let ValidationPipe = class ValidationPipe {
|
|
|
77
82
|
// metatype defined inside the handler
|
|
78
83
|
entity = { constructor: metatype };
|
|
79
84
|
}
|
|
80
|
-
const errors = await this.validate(entity,
|
|
85
|
+
const errors = await this.validate(entity, {
|
|
86
|
+
...this.validatorOptions,
|
|
87
|
+
// The target of an error is the whole object it was found in, so an
|
|
88
|
+
// exception factory that sends the errors would repeat that object
|
|
89
|
+
// once per error.
|
|
90
|
+
validationError: {
|
|
91
|
+
target: false,
|
|
92
|
+
...this.validatorOptions.validationError,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
81
95
|
if (errors.length > 0) {
|
|
82
96
|
throw await this.exceptionFactory(errors);
|
|
83
97
|
}
|
|
98
|
+
if (isCtorNotEqual &&
|
|
99
|
+
(isPrimitive || Array.isArray(value)) &&
|
|
100
|
+
(await this.hasValidationRules(metatype))) {
|
|
101
|
+
// An array or a primitive is never an instance of the class: the
|
|
102
|
+
// validation above has only seen a stand-in without any property set,
|
|
103
|
+
// which passes when none of them is required.
|
|
104
|
+
throw await this.exceptionFactory([
|
|
105
|
+
{
|
|
106
|
+
property: metadata.data || metadata.type,
|
|
107
|
+
constraints: { isObject: 'Validation failed (object is expected)' },
|
|
108
|
+
children: [],
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
84
112
|
if (originalValue === undefined && originalEntity === '') {
|
|
85
113
|
// Since SWC requires empty string for validation (to avoid an error),
|
|
86
114
|
// a fallback is needed to revert to the original value (when undefined).
|
|
@@ -111,8 +139,16 @@ let ValidationPipe = class ValidationPipe {
|
|
|
111
139
|
if (this.isDetailedOutputDisabled) {
|
|
112
140
|
return new HttpErrorByCode[this.errorHttpStatusCode]();
|
|
113
141
|
}
|
|
142
|
+
const budget = {
|
|
143
|
+
remaining: this.maxErrorMessagesLength,
|
|
144
|
+
isExceeded: false,
|
|
145
|
+
};
|
|
146
|
+
validationErrors = this.limitValidationErrors(validationErrors, budget);
|
|
114
147
|
if (this.errorFormat === 'grouped') {
|
|
115
|
-
|
|
148
|
+
let errors = this.groupValidationErrors(validationErrors);
|
|
149
|
+
if (budget.isExceeded) {
|
|
150
|
+
errors = { ...errors, '...': [OMITTED_ERRORS_MESSAGE] };
|
|
151
|
+
}
|
|
116
152
|
// Custom (object) bodies are returned verbatim by
|
|
117
153
|
// `HttpException.createBody`, so add the standard envelope fields
|
|
118
154
|
// explicitly to match the shape of the "list" format.
|
|
@@ -123,10 +159,43 @@ let ValidationPipe = class ValidationPipe {
|
|
|
123
159
|
statusCode,
|
|
124
160
|
});
|
|
125
161
|
}
|
|
126
|
-
|
|
162
|
+
let errors = this.flattenValidationErrors(validationErrors);
|
|
163
|
+
if (budget.isExceeded) {
|
|
164
|
+
errors = [...errors, OMITTED_ERRORS_MESSAGE];
|
|
165
|
+
}
|
|
127
166
|
return new HttpErrorByCode[this.errorHttpStatusCode](errors);
|
|
128
167
|
};
|
|
129
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Leaves out the errors that come after the first "maxErrorMessagesLength"
|
|
171
|
+
* characters of messages. Each message counts with the path of its
|
|
172
|
+
* property, which is what makes the response to a deeply nested payload
|
|
173
|
+
* grow faster than the payload. The errors past the limit are not visited.
|
|
174
|
+
*/
|
|
175
|
+
limitValidationErrors(validationErrors, budget, parentPathLength = 0) {
|
|
176
|
+
const limited = [];
|
|
177
|
+
let isChanged = false;
|
|
178
|
+
for (const error of validationErrors) {
|
|
179
|
+
if (budget.remaining <= 0) {
|
|
180
|
+
budget.isExceeded = true;
|
|
181
|
+
return limited;
|
|
182
|
+
}
|
|
183
|
+
const pathLength = parentPathLength + String(error.property).length + 1;
|
|
184
|
+
for (const key in error.constraints) {
|
|
185
|
+
budget.remaining -= pathLength + String(error.constraints[key]).length;
|
|
186
|
+
}
|
|
187
|
+
const children = error.children &&
|
|
188
|
+
this.limitValidationErrors(error.children, budget, pathLength);
|
|
189
|
+
if (children === error.children) {
|
|
190
|
+
limited.push(error);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
limited.push({ ...error, children });
|
|
194
|
+
isChanged = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return isChanged ? limited : validationErrors;
|
|
198
|
+
}
|
|
130
199
|
toValidate(metadata) {
|
|
131
200
|
const { metatype, type } = metadata;
|
|
132
201
|
if (type === 'custom' && !this.validateCustomDecorators) {
|
|
@@ -191,6 +260,11 @@ let ValidationPipe = class ValidationPipe {
|
|
|
191
260
|
validate(object, validatorOptions) {
|
|
192
261
|
return classValidator.validate(object, validatorOptions);
|
|
193
262
|
}
|
|
263
|
+
async hasValidationRules(metatype) {
|
|
264
|
+
// "forbidUnknownValues" rejects a class without any rule that applies
|
|
265
|
+
const errors = await this.validate({ constructor: metatype }, { ...this.validatorOptions, forbidUnknownValues: true });
|
|
266
|
+
return !errors.some(error => error.constraints?.unknownValue);
|
|
267
|
+
}
|
|
194
268
|
flattenValidationErrors(validationErrors) {
|
|
195
269
|
return iterate(validationErrors)
|
|
196
270
|
.map(error => this.mapChildrenToValidationErrors(error))
|
|
@@ -200,8 +274,7 @@ let ValidationPipe = class ValidationPipe {
|
|
|
200
274
|
.flatten()
|
|
201
275
|
.toArray();
|
|
202
276
|
}
|
|
203
|
-
groupValidationErrors(validationErrors, parentPath) {
|
|
204
|
-
const result = {};
|
|
277
|
+
groupValidationErrors(validationErrors, parentPath, result = {}) {
|
|
205
278
|
for (const error of validationErrors) {
|
|
206
279
|
const path = parentPath
|
|
207
280
|
? `${parentPath}.${error.property}`
|
|
@@ -210,24 +283,42 @@ let ValidationPipe = class ValidationPipe {
|
|
|
210
283
|
result[path] = Object.values(error.constraints);
|
|
211
284
|
}
|
|
212
285
|
if (error.children && error.children.length) {
|
|
213
|
-
|
|
286
|
+
// Nested errors go into the same result: copying each level's result
|
|
287
|
+
// into its parent took as many insertions as depth times leaves.
|
|
288
|
+
const nested = this.groupValidationErrors(error.children, path, result);
|
|
289
|
+
if (nested !== result) {
|
|
290
|
+
// an override returned a result of its own
|
|
291
|
+
Object.assign(result, nested);
|
|
292
|
+
}
|
|
214
293
|
}
|
|
215
294
|
}
|
|
216
295
|
return result;
|
|
217
296
|
}
|
|
218
|
-
mapChildrenToValidationErrors(error, parentPath) {
|
|
297
|
+
mapChildrenToValidationErrors(error, parentPath, validationErrors = []) {
|
|
298
|
+
const currentError = parentPath
|
|
299
|
+
? this.prependConstraintsWithParentProp(parentPath, error)
|
|
300
|
+
: error;
|
|
219
301
|
if (!(error.children && error.children.length)) {
|
|
220
|
-
|
|
302
|
+
validationErrors.push(currentError);
|
|
303
|
+
return validationErrors;
|
|
304
|
+
}
|
|
305
|
+
if (error.constraints) {
|
|
306
|
+
validationErrors.push(currentError);
|
|
221
307
|
}
|
|
222
|
-
const validationErrors = [];
|
|
223
308
|
parentPath = parentPath
|
|
224
309
|
? `${parentPath}.${error.property}`
|
|
225
310
|
: error.property;
|
|
226
311
|
for (const item of error.children) {
|
|
227
|
-
|
|
228
|
-
|
|
312
|
+
// Nested errors go into the same list: copying each level's list into
|
|
313
|
+
// its parent took as many copies as depth times leaves, and spreading
|
|
314
|
+
// a long one into push() threw a RangeError.
|
|
315
|
+
const nested = this.mapChildrenToValidationErrors(item, parentPath, validationErrors);
|
|
316
|
+
if (nested !== validationErrors) {
|
|
317
|
+
// an override returned a list of its own
|
|
318
|
+
for (const nestedError of nested) {
|
|
319
|
+
validationErrors.push(nestedError);
|
|
320
|
+
}
|
|
229
321
|
}
|
|
230
|
-
validationErrors.push(this.prependConstraintsWithParentProp(parentPath, item));
|
|
231
322
|
}
|
|
232
323
|
return validationErrors;
|
|
233
324
|
}
|
|
@@ -6,6 +6,11 @@ import { loadPackage } from '../utils/load-package.util.js';
|
|
|
6
6
|
import { isObject } from '../utils/shared.utils.js';
|
|
7
7
|
import { CLASS_SERIALIZER_OPTIONS } from './class-serializer.constants.js';
|
|
8
8
|
let classTransformer = {};
|
|
9
|
+
// The package of the interceptor created last, which may still be loading.
|
|
10
|
+
// "intercept()" resolves it into "classTransformer", so that an interceptor
|
|
11
|
+
// created while responses are serialized (e.g. a request-scoped one) doesn't
|
|
12
|
+
// put a promise in place of the package they use.
|
|
13
|
+
let pendingClassTransformer;
|
|
9
14
|
// NOTE (external)
|
|
10
15
|
// We need to deduplicate them here due to the circular dependency
|
|
11
16
|
// between core and common packages
|
|
@@ -19,12 +24,15 @@ let ClassSerializerInterceptor = class ClassSerializerInterceptor {
|
|
|
19
24
|
constructor(reflector, defaultOptions = {}) {
|
|
20
25
|
this.reflector = reflector;
|
|
21
26
|
this.defaultOptions = defaultOptions;
|
|
22
|
-
|
|
27
|
+
pendingClassTransformer =
|
|
23
28
|
defaultOptions?.transformerPackage ??
|
|
24
29
|
loadPackage('class-transformer', 'ClassSerializerInterceptor', () => import('class-transformer'));
|
|
30
|
+
if (!(pendingClassTransformer instanceof Promise)) {
|
|
31
|
+
classTransformer = pendingClassTransformer;
|
|
32
|
+
}
|
|
25
33
|
}
|
|
26
34
|
async intercept(context, next) {
|
|
27
|
-
classTransformer = (await
|
|
35
|
+
classTransformer = (await pendingClassTransformer);
|
|
28
36
|
const contextOptions = this.getContextOptions(context);
|
|
29
37
|
const options = {
|
|
30
38
|
...this.defaultOptions,
|
|
@@ -60,10 +60,19 @@ let StandardSchemaSerializerInterceptor = class StandardSchemaSerializerIntercep
|
|
|
60
60
|
return result.value;
|
|
61
61
|
}
|
|
62
62
|
getContextOptions(context) {
|
|
63
|
-
|
|
63
|
+
const [handlerOptions, classOptions] = this.reflector.getAll(CLASS_SERIALIZER_OPTIONS, [
|
|
64
64
|
context.getHandler(),
|
|
65
65
|
context.getClass(),
|
|
66
66
|
]);
|
|
67
|
+
if (!handlerOptions || !classOptions) {
|
|
68
|
+
return handlerOptions ?? classOptions;
|
|
69
|
+
}
|
|
70
|
+
// Key by key, so that a handler that only sets "validateOptions" (or the
|
|
71
|
+
// options of another serializer) doesn't drop the schema of its class
|
|
72
|
+
return {
|
|
73
|
+
schema: handlerOptions.schema ?? classOptions.schema,
|
|
74
|
+
validateOptions: handlerOptions.validateOptions ?? classOptions.validateOptions,
|
|
75
|
+
};
|
|
67
76
|
}
|
|
68
77
|
};
|
|
69
78
|
StandardSchemaSerializerInterceptor = __decorate([
|
|
@@ -23,6 +23,9 @@ export interface ConsoleLoggerOptions {
|
|
|
23
23
|
prefix?: string;
|
|
24
24
|
/**
|
|
25
25
|
* If enabled, will print the log message in JSON format.
|
|
26
|
+
* Unless `colors` are enabled or `compact` is not true, a record is cut short
|
|
27
|
+
* at about 1,000,000 characters: what comes past that point is replaced with
|
|
28
|
+
* "[Truncated]".
|
|
26
29
|
*/
|
|
27
30
|
json?: boolean;
|
|
28
31
|
/**
|
|
@@ -49,28 +52,28 @@ export interface ConsoleLoggerOptions {
|
|
|
49
52
|
/**
|
|
50
53
|
* Specifies the maximum number of Array, TypedArray, Map, Set, WeakMap, and WeakSet elements to include when formatting.
|
|
51
54
|
* Set to null or Infinity to show all elements. Set to 0 or negative to show no elements.
|
|
52
|
-
*
|
|
55
|
+
* When `json` is enabled, colors are disabled, and `compact` is set to true, only applies to Map, Set and Error values, which are inspected; everything else is serialized as JSON.
|
|
53
56
|
* @default 100
|
|
54
57
|
*/
|
|
55
58
|
maxArrayLength?: number;
|
|
56
59
|
/**
|
|
57
60
|
* Specifies the maximum number of characters to include when formatting.
|
|
58
61
|
* Set to null or Infinity to show all elements. Set to 0 or negative to show no characters.
|
|
59
|
-
*
|
|
62
|
+
* When `json` is enabled, colors are disabled, and `compact` is set to true, only applies to Map, Set and Error values, which are inspected; everything else is serialized as JSON.
|
|
60
63
|
* @default 10000.
|
|
61
64
|
*/
|
|
62
65
|
maxStringLength?: number;
|
|
63
66
|
/**
|
|
64
67
|
* If enabled, will sort keys while formatting objects.
|
|
65
68
|
* Can also be a custom sorting function.
|
|
66
|
-
*
|
|
69
|
+
* When `json` is enabled, colors are disabled, and `compact` is set to true, only applies to Map, Set and Error values, which are inspected; everything else is serialized as JSON.
|
|
67
70
|
* @default false
|
|
68
71
|
*/
|
|
69
72
|
sorted?: boolean | ((a: string, b: string) => number);
|
|
70
73
|
/**
|
|
71
74
|
* Specifies the number of times to recurse while formatting object.
|
|
72
75
|
* This is useful for inspecting large objects. To recurse up to the maximum call stack size pass Infinity or null.
|
|
73
|
-
*
|
|
76
|
+
* When `json` is enabled, colors are disabled, and `compact` is set to true, only applies to Map, Set and Error values, which are inspected; everything else is serialized as JSON.
|
|
74
77
|
* @default 5
|
|
75
78
|
*/
|
|
76
79
|
depth?: number;
|
|
@@ -83,7 +86,7 @@ export interface ConsoleLoggerOptions {
|
|
|
83
86
|
/**
|
|
84
87
|
* The length at which input values are split across multiple lines. Set to Infinity to format the input as a single line (in combination with "compact" set to true).
|
|
85
88
|
* Default Infinity when "compact" is true, 80 otherwise.
|
|
86
|
-
*
|
|
89
|
+
* When `json` is enabled, colors are disabled, and `compact` is set to true, only applies to Map, Set and Error values, which are inspected; everything else is serialized as JSON.
|
|
87
90
|
*/
|
|
88
91
|
breakLength?: number;
|
|
89
92
|
/**
|
|
@@ -276,7 +279,8 @@ export declare class ConsoleLogger implements LoggerService {
|
|
|
276
279
|
protected getInspectOptions(): InspectOptions;
|
|
277
280
|
/**
|
|
278
281
|
* Serializes a JSON log object without ever throwing: circular references
|
|
279
|
-
* are replaced with "[Circular]",
|
|
282
|
+
* are replaced with "[Circular]", whatever comes past `MAX_JSON_LENGTH`
|
|
283
|
+
* characters with "[Truncated]", and a value that cannot be serialized
|
|
280
284
|
* (e.g. a throwing `toJSON()`) makes the whole record fall back to `inspect`.
|
|
281
285
|
*/
|
|
282
286
|
protected stringifyJsonLogObject(logObject: Record<string, unknown>): string;
|
|
@@ -292,19 +296,19 @@ export declare class ConsoleLogger implements LoggerService {
|
|
|
292
296
|
};
|
|
293
297
|
protected getContextAndStackAndMessagesToPrint(args: unknown[]): {
|
|
294
298
|
messages: unknown[];
|
|
295
|
-
stack: string;
|
|
296
299
|
context: string | undefined;
|
|
297
300
|
params?: undefined;
|
|
301
|
+
stack?: undefined;
|
|
298
302
|
} | {
|
|
299
303
|
messages: unknown[];
|
|
300
304
|
context: string | undefined;
|
|
301
|
-
params
|
|
305
|
+
params: any;
|
|
302
306
|
stack?: undefined;
|
|
303
307
|
} | {
|
|
304
308
|
messages: unknown[];
|
|
309
|
+
stack: string;
|
|
305
310
|
context: string | undefined;
|
|
306
|
-
params
|
|
307
|
-
stack?: undefined;
|
|
311
|
+
params?: undefined;
|
|
308
312
|
} | {
|
|
309
313
|
stack: string | undefined;
|
|
310
314
|
messages: unknown[];
|
|
@@ -13,6 +13,13 @@ const DEFAULT_DEPTH = 5;
|
|
|
13
13
|
* serialized in JSON mode.
|
|
14
14
|
*/
|
|
15
15
|
const MAX_ERROR_DEPTH = 5;
|
|
16
|
+
/**
|
|
17
|
+
* The approximate number of characters up to which a record is serialized in
|
|
18
|
+
* JSON mode. An object is written out once per reference to it, so a small
|
|
19
|
+
* value can expand to a huge record; past this length, objects and strings
|
|
20
|
+
* are replaced with "[Truncated]".
|
|
21
|
+
*/
|
|
22
|
+
const MAX_JSON_LENGTH = 1_000_000;
|
|
16
23
|
const DEFAULT_LOG_LEVELS = [
|
|
17
24
|
'log',
|
|
18
25
|
'error',
|
|
@@ -466,16 +473,32 @@ let ConsoleLogger = class ConsoleLogger {
|
|
|
466
473
|
}
|
|
467
474
|
/**
|
|
468
475
|
* Serializes a JSON log object without ever throwing: circular references
|
|
469
|
-
* are replaced with "[Circular]",
|
|
476
|
+
* are replaced with "[Circular]", whatever comes past `MAX_JSON_LENGTH`
|
|
477
|
+
* characters with "[Truncated]", and a value that cannot be serialized
|
|
470
478
|
* (e.g. a throwing `toJSON()`) makes the whole record fall back to `inspect`.
|
|
471
479
|
*/
|
|
472
480
|
stringifyJsonLogObject(logObject) {
|
|
473
|
-
const replacer = this.stringifyReplacer;
|
|
481
|
+
const replacer = this.stringifyReplacer.bind(this);
|
|
474
482
|
// Objects on the path from the root to the value being serialized.
|
|
475
483
|
const ancestors = [];
|
|
484
|
+
// Approximate length of the output so far (array indices aren't written).
|
|
485
|
+
let length = 0;
|
|
476
486
|
try {
|
|
477
487
|
return JSON.stringify(logObject, function (key, value) {
|
|
478
|
-
value = replacer
|
|
488
|
+
value = replacer(key, value);
|
|
489
|
+
length +=
|
|
490
|
+
(Array.isArray(this) ? 0 : key.length) +
|
|
491
|
+
(isString(value) || typeof value === 'number'
|
|
492
|
+
? String(value).length
|
|
493
|
+
: 1);
|
|
494
|
+
if (length > MAX_JSON_LENGTH) {
|
|
495
|
+
// A primitive in place of an object is not descended into, so the
|
|
496
|
+
// walk stops here on every branch past the limit.
|
|
497
|
+
return isString(value) ||
|
|
498
|
+
(typeof value === 'object' && value !== null)
|
|
499
|
+
? '[Truncated]'
|
|
500
|
+
: value;
|
|
501
|
+
}
|
|
479
502
|
if (typeof value !== 'object' || value === null) {
|
|
480
503
|
return value;
|
|
481
504
|
}
|
|
@@ -550,8 +573,11 @@ let ConsoleLogger = class ConsoleLogger {
|
|
|
550
573
|
return { messages, context, params };
|
|
551
574
|
}
|
|
552
575
|
getContextAndStackAndMessagesToPrint(args) {
|
|
576
|
+
if (args.length === 1) {
|
|
577
|
+
return { ...this.getContextAndMessagesToPrint(args) };
|
|
578
|
+
}
|
|
553
579
|
if (args.length === 2) {
|
|
554
|
-
if (this.isStackFormat(args[1])) {
|
|
580
|
+
if (this.isStackFormat(args[1]) || isUndefined(args[1])) {
|
|
555
581
|
return {
|
|
556
582
|
messages: [args[0]],
|
|
557
583
|
stack: args[1],
|
|
@@ -591,7 +617,9 @@ let ConsoleLogger = class ConsoleLogger {
|
|
|
591
617
|
if (!isString(stack) && !isUndefined(stack)) {
|
|
592
618
|
return false;
|
|
593
619
|
}
|
|
594
|
-
|
|
620
|
+
// `[ \t]+` rather than `\s+`, which also matches "\n" and would make this
|
|
621
|
+
// unanchored search backtrack quadratically on long runs of newlines.
|
|
622
|
+
return /\n[ \t]+at .+:\d+:\d+/.test(stack);
|
|
595
623
|
}
|
|
596
624
|
getColorByLogLevel(level) {
|
|
597
625
|
switch (level) {
|
|
@@ -18,9 +18,11 @@ export type Redactor = (value: unknown) => unknown;
|
|
|
18
18
|
* `AggregateError#errors`), `Map`s (string keys) and `Set`s are traversed.
|
|
19
19
|
* Only the objects on the way to a redacted property are copied; the rest is
|
|
20
20
|
* returned as is, and the input is never mutated. A circular reference is
|
|
21
|
-
* replaced with `"[Circular]"` (so the objects on the cycle are copied too).
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* replaced with `"[Circular]"` (so the objects on the cycle are copied too). An
|
|
22
|
+
* object that is referenced several times is traversed (and copied) once for
|
|
23
|
+
* all the references a dotted path can't tell apart. If the value can't be
|
|
24
|
+
* traversed (for example, a getter throws), the whole value is replaced with
|
|
25
|
+
* the censor.
|
|
24
26
|
*
|
|
25
27
|
* @returns `undefined` when there is nothing to redact.
|
|
26
28
|
*/
|
|
@@ -14,9 +14,11 @@ const CIRCULAR = '[Circular]';
|
|
|
14
14
|
* `AggregateError#errors`), `Map`s (string keys) and `Set`s are traversed.
|
|
15
15
|
* Only the objects on the way to a redacted property are copied; the rest is
|
|
16
16
|
* returned as is, and the input is never mutated. A circular reference is
|
|
17
|
-
* replaced with `"[Circular]"` (so the objects on the cycle are copied too).
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* replaced with `"[Circular]"` (so the objects on the cycle are copied too). An
|
|
18
|
+
* object that is referenced several times is traversed (and copied) once for
|
|
19
|
+
* all the references a dotted path can't tell apart. If the value can't be
|
|
20
|
+
* traversed (for example, a getter throws), the whole value is replaced with
|
|
21
|
+
* the censor.
|
|
20
22
|
*
|
|
21
23
|
* @returns `undefined` when there is nothing to redact.
|
|
22
24
|
*/
|
|
@@ -38,11 +40,18 @@ export function createRedactor(paths, censor = DEFAULT_REDACT_CENSOR) {
|
|
|
38
40
|
if (keys.size === 0 && dottedPaths.length === 0) {
|
|
39
41
|
return undefined;
|
|
40
42
|
}
|
|
43
|
+
// How many of the keys leading to an object the dotted paths can look at.
|
|
44
|
+
const contextLength = Math.max(0, ...dottedPaths.map(segments => segments.length - 1));
|
|
41
45
|
return (value) => {
|
|
42
46
|
// Lower-cased keys from the root to the value being visited.
|
|
43
47
|
const path = [];
|
|
44
48
|
// Objects on that path, to detect circular references.
|
|
45
49
|
const ancestors = [];
|
|
50
|
+
// What each visited object was replaced with, by the last keys leading to
|
|
51
|
+
// it (a dotted path can match under some keys and not under others).
|
|
52
|
+
// Otherwise, an object referenced many times would be traversed, and
|
|
53
|
+
// copied, once per reference.
|
|
54
|
+
const visited = new Map();
|
|
46
55
|
const isRedacted = (key) => {
|
|
47
56
|
if (keys.has(key)) {
|
|
48
57
|
return true;
|
|
@@ -82,6 +91,15 @@ export function createRedactor(paths, censor = DEFAULT_REDACT_CENSOR) {
|
|
|
82
91
|
if (ancestors.includes(current)) {
|
|
83
92
|
return CIRCULAR;
|
|
84
93
|
}
|
|
94
|
+
const context = contextLength > 0 ? JSON.stringify(path.slice(-contextLength)) : '';
|
|
95
|
+
let results = visited.get(context);
|
|
96
|
+
if (!results) {
|
|
97
|
+
results = new Map();
|
|
98
|
+
visited.set(context, results);
|
|
99
|
+
}
|
|
100
|
+
if (results.has(current)) {
|
|
101
|
+
return results.get(current);
|
|
102
|
+
}
|
|
85
103
|
let redacted;
|
|
86
104
|
ancestors.push(current);
|
|
87
105
|
if (Array.isArray(current)) {
|
|
@@ -103,6 +121,7 @@ export function createRedactor(paths, censor = DEFAULT_REDACT_CENSOR) {
|
|
|
103
121
|
redacted = redactObject(current);
|
|
104
122
|
}
|
|
105
123
|
ancestors.pop();
|
|
124
|
+
results.set(current, redacted);
|
|
106
125
|
return redacted;
|
|
107
126
|
};
|
|
108
127
|
const redactArray = (array) => {
|
|
@@ -8,6 +8,9 @@ import type { ParameterDecoratorOptions } from '../decorators/http/route-params.
|
|
|
8
8
|
* arguments identically: a pipe instance that happens to expose a `schema` or
|
|
9
9
|
* `pipes` property must never be mistaken for an options object.
|
|
10
10
|
*
|
|
11
|
+
* `createParamDecorator` applies a stricter check to its first argument, since
|
|
12
|
+
* that argument may also be arbitrary custom data.
|
|
13
|
+
*
|
|
11
14
|
* @internal
|
|
12
15
|
*/
|
|
13
16
|
export declare function isParameterDecoratorOptions(value: unknown): value is ParameterDecoratorOptions;
|
|
@@ -12,6 +12,9 @@ const isPipeLike = (value) => isFunction(value?.transform) ||
|
|
|
12
12
|
* arguments identically: a pipe instance that happens to expose a `schema` or
|
|
13
13
|
* `pipes` property must never be mistaken for an options object.
|
|
14
14
|
*
|
|
15
|
+
* `createParamDecorator` applies a stricter check to its first argument, since
|
|
16
|
+
* that argument may also be arbitrary custom data.
|
|
17
|
+
*
|
|
15
18
|
* @internal
|
|
16
19
|
*/
|
|
17
20
|
export function isParameterDecoratorOptions(value) {
|
|
@@ -17,7 +17,11 @@ export function stripProtoKeys(value) {
|
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
if (Array.isArray(value)) {
|
|
20
|
-
|
|
20
|
+
// Only the items that are present: the length of a sparse array (e.g.
|
|
21
|
+
// one built from a multipart field named `tags[4294967294]`) can be far
|
|
22
|
+
// beyond its number of items, and a loop over every index would block
|
|
23
|
+
// the event loop for as long. `Object.values()` skips the holes.
|
|
24
|
+
for (const v of Object.values(value)) {
|
|
21
25
|
stripProtoKeys(v);
|
|
22
26
|
}
|
|
23
27
|
return;
|
|
@@ -25,9 +29,9 @@ export function stripProtoKeys(value) {
|
|
|
25
29
|
// Delete dangerous prototype pollution keys
|
|
26
30
|
delete value.__proto__;
|
|
27
31
|
delete value.prototype;
|
|
28
|
-
// Only delete constructor if it's NOT a built-in type
|
|
29
|
-
|
|
30
|
-
if (
|
|
32
|
+
// Only delete constructor if it's NOT a built-in type, whatever its value:
|
|
33
|
+
// a falsy one (null, 0, false, "") shadows the prototype's all the same
|
|
34
|
+
if (!BUILT_IN_TYPES.includes(value.constructor)) {
|
|
31
35
|
delete value.constructor;
|
|
32
36
|
}
|
|
33
37
|
for (const key in value) {
|