@nestjs/common
Version:
Decorators, pipes, exceptions and utilities for NestJS applications: @Controller(), @Injectable(), @Body(), ValidationPipe, HttpException and more.
147 lines (146 loc) • 5.94 kB
JavaScript
import { __decorate, __metadata, __param } from "tslib";
import { Observable } from 'rxjs';
import { Inject, Injectable, Optional } from '../decorators/core/index.js';
import { StreamableFile } from '../file-stream/index.js';
import { isStreamingBody } from '../utils/is-streaming-body.util.js';
import { mapStreamingBody } from '../utils/map-streaming-body.util.js';
import { isObject } from '../utils/shared.utils.js';
import { CLASS_SERIALIZER_OPTIONS } from './class-serializer.constants.js';
// NOTE (external)
// We need to deduplicate them here due to the circular dependency
// between core and common packages
const REFLECTOR = 'Reflector';
/**
* `concatMap(project)` for a `project` that returns a promise: values are
* projected one at a time, in order. `concatMap()` `shift()`s each waiting
* value off an array of all the waiting ones, so a stream that emits many
* values at once (e.g. `from()` an array) took time quadratic in their
* number, without the event loop running in between. Here they wait in a
* linked list.
*/
function concatMapAsync(project) {
return source => new Observable(subscriber => {
let head;
let tail;
let isProjecting = false;
let isSourceComplete = false;
const projectNext = () => {
if (subscriber.closed) {
return;
}
if (!head) {
isProjecting = false;
if (isSourceComplete) {
subscriber.complete();
}
return;
}
const { value } = head;
head = head.next;
isProjecting = true;
project(value).then(result => {
subscriber.next(result);
projectNext();
}, err => subscriber.error(err));
};
subscriber.add(source.subscribe({
next: value => {
const waiting = { value };
tail = head ? (tail.next = waiting) : (head = waiting);
if (!isProjecting) {
projectNext();
}
},
error: err => subscriber.error(err),
complete: () => {
isSourceComplete = true;
if (!isProjecting) {
projectNext();
}
},
}));
});
}
/**
* 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
*/
let StandardSchemaSerializerInterceptor = class StandardSchemaSerializerInterceptor {
reflector;
defaultOptions;
constructor(reflector, defaultOptions = {}) {
this.reflector = reflector;
this.defaultOptions = defaultOptions;
}
intercept(context, next) {
const contextOptions = this.getContextOptions(context);
const schema = contextOptions?.schema ?? this.defaultOptions.schema;
const validateOptions = contextOptions?.validateOptions ?? this.defaultOptions.validateOptions;
// The router sends each object a stream yields as a line of JSON, so these
// are serialized as the stream yields them. Other transports leave what a
// stream yields to whatever consumes it (e.g. a GraphQL subscription).
const isHttp = context.getType?.() === 'http';
return next
.handle()
.pipe(concatMapAsync(async (res) => isHttp && schema && isStreamingBody(res)
? mapStreamingBody(res, chunk => this.serialize(chunk, schema, validateOptions))
: this.serialize(res, schema, validateOptions)));
}
/**
* Serializes responses that are non-null objects nor streams.
*/
serialize(response, schema, validateOptions) {
if (!schema ||
!isObject(response) ||
response instanceof StreamableFile ||
isStreamingBody(response)) {
return response;
}
return Array.isArray(response)
? Promise.all(response.map(item => this.transformToPlain(item, schema, validateOptions)))
: this.transformToPlain(response, schema, validateOptions);
}
async transformToPlain(plainOrClass, schema, validateOptions) {
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;
}
getContextOptions(context) {
const [handlerOptions, classOptions] = this.reflector.getAll(CLASS_SERIALIZER_OPTIONS, [
context.getHandler(),
context.getClass(),
]);
if (!handlerOptions || !classOptions) {
return handlerOptions ?? classOptions;
}
// Key by key, so that a handler that only sets "validateOptions" (or the
// options of another serializer) doesn't drop the schema of its class
return {
schema: handlerOptions.schema ?? classOptions.schema,
validateOptions: handlerOptions.validateOptions ?? classOptions.validateOptions,
};
}
};
StandardSchemaSerializerInterceptor = __decorate([
Injectable(),
__param(0, Inject(REFLECTOR)),
__param(1, Optional()),
__metadata("design:paramtypes", [Object, Object])
], StandardSchemaSerializerInterceptor);
export { StandardSchemaSerializerInterceptor };