"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ValidationPipe = void 0;
const tslib_1 = require("tslib");
const iterare_1 = require("iterare");
const util_1 = require("util");
const decorators_1 = require("../decorators");
const core_1 = require("../decorators/core");
const http_status_enum_1 = require("../enums/http-status.enum");
const http_error_by_code_util_1 = require("../utils/http-error-by-code.util");
const load_package_util_1 = require("../utils/load-package.util");
const shared_utils_1 = require("../utils/shared.utils");
let classValidator = {};
let classTransformer = {};
/**
 * 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];
/**
 * @see [Validation](https://docs.nestjs.com/techniques/validation)
 *
 * @publicApi
 */
let ValidationPipe = class ValidationPipe {
    constructor(options) {
        options = options || {};
        const { transform, disableErrorMessages, errorHttpStatusCode, expectedType, transformOptions, validateCustomDecorators, ...validatorOptions } = options;
        // @see [https://github.com/nestjs/nest/issues/10683#issuecomment-1413690508](https://github.com/nestjs/nest/issues/10683#issuecomment-1413690508)
        this.validatorOptions = { forbidUnknownValues: false, ...validatorOptions };
        this.isTransformEnabled = !!transform;
        this.transformOptions = transformOptions;
        this.isDetailedOutputDisabled = disableErrorMessages;
        this.validateCustomDecorators = validateCustomDecorators || false;
        this.errorHttpStatusCode = errorHttpStatusCode || http_status_enum_1.HttpStatus.BAD_REQUEST;
        this.expectedType = expectedType;
        this.exceptionFactory =
            options.exceptionFactory || this.createExceptionFactory();
        classValidator = this.loadValidator(options.validatorPackage);
        classTransformer = this.loadTransformer(options.transformerPackage);
    }
    loadValidator(validatorPackage) {
        return (validatorPackage ??
            (0, load_package_util_1.loadPackage)('class-validator', 'ValidationPipe', () => require('class-validator')));
    }
    loadTransformer(transformerPackage) {
        return (transformerPackage ??
            (0, load_package_util_1.loadPackage)('class-transformer', 'ValidationPipe', () => require('class-transformer')));
    }
    async transform(value, metadata) {
        if (this.expectedType) {
            metadata = { ...metadata, metatype: this.expectedType };
        }
        const metatype = metadata.metatype;
        if (!metatype || !this.toValidate(metadata)) {
            return this.isTransformEnabled
                ? this.transformPrimitive(value, metadata)
                : value;
        }
        const originalValue = value;
        value = this.toEmptyIfNil(value, metatype);
        const isNil = value !== originalValue;
        const isPrimitive = this.isPrimitive(value);
        this.stripProtoKeys(value);
        let entity = classTransformer.plainToInstance(metatype, value, this.transformOptions);
        const originalEntity = entity;
        const isCtorNotEqual = entity.constructor !== metatype;
        if (isCtorNotEqual && !isPrimitive) {
            entity.constructor = metatype;
        }
        else if (isCtorNotEqual) {
            // when "entity" is a primitive value, we have to temporarily
            // replace the entity to perform the validation against the original
            // metatype defined inside the handler
            entity = { constructor: metatype };
        }
        const errors = await this.validate(entity, this.validatorOptions);
        if (errors.length > 0) {
            throw await this.exceptionFactory(errors);
        }
        if (originalValue === undefined && originalEntity === '') {
            // Since SWC requires empty string for validation (to avoid an error),
            // a fallback is needed to revert to the original value (when undefined).
            // @see [https://github.com/nestjs/nest/issues/14430](https://github.com/nestjs/nest/issues/14430)
            return originalValue;
        }
        if (isPrimitive) {
            // if the value is a primitive value and the validation process has been successfully completed
            // we have to revert the original value passed through the pipe
            entity = originalEntity;
        }
        if (this.isTransformEnabled) {
            return entity;
        }
        if (isNil) {
            // if the value was originally undefined or null, revert it back
            return originalValue;
        }
        // we check if the number of keys of the "validatorOptions" is higher than 1 (instead of 0)
        // because the "forbidUnknownValues" now fallbacks to "false" (in case it wasn't explicitly specified)
        const shouldTransformToPlain = Object.keys(this.validatorOptions).length > 1;
        return shouldTransformToPlain
            ? classTransformer.classToPlain(entity, this.transformOptions)
            : value;
    }
    createExceptionFactory() {
        return (validationErrors = []) => {
            if (this.isDetailedOutputDisabled) {
                return new http_error_by_code_util_1.HttpErrorByCode[this.errorHttpStatusCode]();
            }
            const errors = this.flattenValidationErrors(validationErrors);
            return new http_error_by_code_util_1.HttpErrorByCode[this.errorHttpStatusCode](errors);
        };
    }
    toValidate(metadata) {
        const { metatype, type } = metadata;
        if (type === 'custom' && !this.validateCustomDecorators) {
            return false;
        }
        const types = [String, Boolean, Number, Array, Object, Buffer, Date];
        return !types.some(t => metatype === t) && !(0, shared_utils_1.isNil)(metatype);
    }
    transformPrimitive(value, metadata) {
        if (!metadata.data) {
            // leave top-level query/param objects unmodified
            return value;
        }
        const { type, metatype } = metadata;
        if (type !== 'param' && type !== 'query') {
            return value;
        }
        if (metatype === Boolean) {
            if ((0, shared_utils_1.isUndefined)(value)) {
                // This is an workaround to deal with optional boolean values since
                // optional booleans shouldn't be parsed to a valid boolean when
                // they were not defined
                return undefined;
            }
            // Any fasly value but `undefined` will be parsed to `false`
            return value === true || value === 'true';
        }
        if (metatype === Number) {
            if ((0, shared_utils_1.isUndefined)(value)) {
                // This is a workaround to deal with optional numeric values since
                // optional numerics shouldn't be parsed to a valid number when
                // they were not defined
                return undefined;
            }
            return +value;
        }
        if (metatype === String && !(0, shared_utils_1.isUndefined)(value)) {
            return String(value);
        }
        return value;
    }
    toEmptyIfNil(value, metatype) {
        if (!(0, shared_utils_1.isNil)(value)) {
            return value;
        }
        if (typeof metatype === 'function' ||
            (metatype && 'prototype' in metatype && metatype.prototype?.constructor)) {
            return {};
        }
        // SWC requires empty string to be returned instead of an empty object
        // when the value is nil and the metatype is not a class instance, but a plain object (enum, for example).
        // Otherwise, the error will be thrown.
        // @see [https://github.com/nestjs/nest/issues/12680](https://github.com/nestjs/nest/issues/12680)
        return '';
    }
    stripProtoKeys(value) {
        if (value == null ||
            typeof value !== 'object' ||
            util_1.types.isTypedArray(value)) {
            return;
        }
        // Skip built-in JavaScript primitives to avoid Jest useFakeTimers conflicts
        if (BUILT_IN_TYPES.some(type => value instanceof type)) {
            return;
        }
        if (Array.isArray(value)) {
            for (const v of value) {
                this.stripProtoKeys(v);
            }
            return;
        }
        // Delete dangerous prototype pollution keys
        delete value.__proto__;
        delete value.prototype;
        // Only delete constructor if it's NOT a built-in type
        const constructorType = value?.constructor;
        if (constructorType && !BUILT_IN_TYPES.includes(constructorType)) {
            delete value.constructor;
        }
        for (const key in value) {
            this.stripProtoKeys(value[key]);
        }
    }
    isPrimitive(value) {
        return ['number', 'boolean', 'string'].includes(typeof value);
    }
    validate(object, validatorOptions) {
        return classValidator.validate(object, validatorOptions);
    }
    flattenValidationErrors(validationErrors) {
        return (0, iterare_1.iterate)(validationErrors)
            .map(error => this.mapChildrenToValidationErrors(error))
            .flatten()
            .filter(item => !!item.constraints)
            .map(item => Object.values(item.constraints))
            .flatten()
            .toArray();
    }
    mapChildrenToValidationErrors(error, parentPath) {
        if (!(error.children && error.children.length)) {
            return [error];
        }
        const validationErrors = [];
        parentPath = parentPath
            ? `${parentPath}.${error.property}`
            : error.property;
        for (const item of error.children) {
            if (item.children && item.children.length) {
                validationErrors.push(...this.mapChildrenToValidationErrors(item, parentPath));
            }
            validationErrors.push(this.prependConstraintsWithParentProp(parentPath, item));
        }
        return validationErrors;
    }
    prependConstraintsWithParentProp(parentPath, error) {
        const constraints = {};
        for (const key in error.constraints) {
            constraints[key] = `${parentPath}.${error.constraints[key]}`;
        }
        return {
            ...error,
            constraints,
        };
    }
};
exports.ValidationPipe = ValidationPipe;
exports.ValidationPipe = ValidationPipe = tslib_1.__decorate([
    (0, core_1.Injectable)(),
    tslib_1.__param(0, (0, decorators_1.Optional)()),
    tslib_1.__metadata("design:paramtypes", [Object])
], ValidationPipe);
