UNPKG

@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
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 };