UNPKG

@nestjs/common

Version:

Decorators, pipes, exceptions and utilities for NestJS applications: @Controller(), @Injectable(), @Body(), ValidationPipe, HttpException and more.

220 lines (219 loc) • 9.99 kB
import { __decorate, __metadata, __param } from "tslib"; 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 { HttpErrorByCode } from '../utils/http-error-by-code.util.js'; import { withoutArrayGaps } from '../utils/close-array-gaps.util.js'; import { isNil, isString, isUndefined } from '../utils/shared.utils.js'; import { OMITTED_ERRORS_MESSAGE, resolveMaxErrorMessagesLength, } from './validation-errors.constants.js'; import { ValidationPipe } from './validation.pipe.js'; const VALIDATION_ERROR_MESSAGE = 'Validation failed (parsable array expected)'; const DEFAULT_ARRAY_SEPARATOR = ','; /** * Defines the built-in ParseArray Pipe * * @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes) * * @publicApi */ let ParseArrayPipe = class ParseArrayPipe { options; validationPipe; exceptionFactory; constructor(options = {}) { this.options = options; this.validationPipe = new ValidationPipe({ transform: true, validateCustomDecorators: true, ...options, }); const { exceptionFactory, errorHttpStatusCode = HttpStatus.BAD_REQUEST } = options; this.exceptionFactory = exceptionFactory || (error => new HttpErrorByCode[errorHttpStatusCode](error)); } /** * Method that accesses and performs optional transformation on argument for * in-flight requests. * * @param value currently processed route argument * @param metadata contains metadata about the currently processed route argument */ async transform(value, metadata) { if (isNil(value) && !this.options.optional) { throw this.exceptionFactory(VALIDATION_ERROR_MESSAGE); } else if (isNil(value) && this.options.optional) { return value; } if (!Array.isArray(value)) { if (!isString(value)) { throw this.exceptionFactory(VALIDATION_ERROR_MESSAGE); } else { try { value = value .trim() .split(this.options.separator || DEFAULT_ARRAY_SEPARATOR); } catch { throw this.exceptionFactory(VALIDATION_ERROR_MESSAGE); } } } else { // Only the items that are present (`[, '1']` gives `['1']`), as // ValidationPipe does: the gaps of a sparse array would otherwise be // handed on unvalidated. The input array is left unchanged. value = withoutArrayGaps(value); } if (this.options.items) { const validationMetadata = { metatype: this.options.items, type: 'query', }; const isExpectedTypePrimitive = this.isExpectedTypePrimitive(); const toClassInstance = (item, index) => { // `JSON.parse` stringifies its argument first, so an array holding a // single item (`[1]` becomes `'1'`) would be parsed as that item. if (this.options.items !== String && isString(item)) { try { item = JSON.parse(item); } catch { // Do nothing } } if (isExpectedTypePrimitive) { return this.validatePrimitive(item, index); } return this.validationPipe.transform(item, validationMetadata); }; if (this.options.stopAtFirstError === false) { // strict compare to "false" to make sure // that this option is disabled by default const errors = []; // The validation pipe applies "maxErrorMessagesLength" to the // messages of one item, and a string splits into up to one item per // character, so the messages of all the items count against it here. // As in the validation pipe, a custom exception factory gets them all. let remainingLength = this.options.exceptionFactory ? Infinity : resolveMaxErrorMessagesLength(this.options.maxErrorMessagesLength); const targetArray = Array.from(value); for (let i = 0; i < targetArray.length; i++) { try { targetArray[i] = await toClassInstance(targetArray[i]); } catch (err) { if (err instanceof Error && !err.getResponse) { // Not a validation error, but a failure of the code that // validates (e.g. an async constraint or a `@Transform()` // function). It is left to the exception filters, as when the // pipe stops at the first error: in the list of messages, it // would be sent as it is and never logged. throw err; } if (remainingLength <= 0) { // The array is rejected either way, so the items that are left // are not validated. errors.push(OMITTED_ERRORS_MESSAGE); break; } let message; if (err.getResponse) { const response = err.getResponse(); if (Array.isArray(response.message)) { message = response.message.map((item) => `[${i}] ${item}`); } else if (response.message && typeof response.message === 'object') { // "grouped" error format: the messages of each property path message = Object.entries(response.message).flatMap(([path, messages]) => { // "..." says that errors were left out, it is not a path const prefix = path === '...' ? '' : `${path}: `; return [messages] .flat() .map(item => `[${i}] ${prefix}${item}`); }); } else { message = `[${i}] ${response.message}`; } } else { message = err; } // Appended in place, since `concat()` would copy all the errors so // far for each invalid item, and one at a time, since one item can // have more errors than `push(...message)` takes arguments. for (const item of Array.isArray(message) ? message : [message]) { errors.push(item); remainingLength -= isString(item) ? item.length : 1; } } } if (errors.length > 0) { throw this.exceptionFactory(errors); } return targetArray; } else { // One item at a time, up to the first that fails: validating them all // at once kept the validation and the exception of every item alive // together, and a string splits into up to one item per character. const targetArray = value; const items = []; for (let i = 0; i < targetArray.length; i++) { items.push(await toClassInstance(targetArray[i], i)); } value = items; } } return value; } isExpectedTypePrimitive() { return [Boolean, Number, String].includes(this.options.items); } validatePrimitive(originalValue, index) { if (this.options.items === Number) { if (!this.isNumeric(originalValue)) { throw this.exceptionFactory(`${isUndefined(index) ? '' : `[${index}] `}item must be a number`); } return Number(originalValue); } else if (this.options.items === String) { if (!isString(originalValue)) { return `${originalValue}`; } } else if (this.options.items === Boolean) { if (typeof originalValue !== 'boolean') { throw this.exceptionFactory(`${isUndefined(index) ? '' : `[${index}] `}item must be a boolean value`); } } return originalValue; } isNumeric(value) { if (typeof value === 'number') { return Number.isFinite(value); } // Booleans, arrays and blank strings coerce to numbers (`+true === 1`, // `+[2] === 2`, `+' ' === 0`), so only strings are converted. if (typeof value !== 'string' || value.trim() === '') { return false; } // Reject radix prefixes (`Number('0x10') === 16`), as ParseIntPipe and // ParseFloatPipe do. if (/^0[box]/i.test(value.trim())) { return false; } return Number.isFinite(Number(value)); } }; ParseArrayPipe = __decorate([ Injectable(), __param(0, Optional()), __metadata("design:paramtypes", [Object]) ], ParseArrayPipe); export { ParseArrayPipe };