Initial commit - Event Planner application

This commit is contained in:
mberlin
2026-03-18 14:55:56 -03:00
commit 86d779eb4d
7548 changed files with 1006324 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
import { FileValidatorContext } from './file-validator-context.interface';
import { FileValidator } from './file-validator.interface';
import { IFile } from './interfaces';
type FileTypeValidatorContext = FileValidatorContext<Omit<FileTypeValidatorOptions, 'errorMessage'>>;
export type FileTypeValidatorOptions = {
/**
* Expected file type(s) for validation. Can be a string (MIME type)
* or a regular expression to match multiple types.
*
* @example
* // Match a single MIME type
* fileType: 'image/png'
*
* @example
* // Match multiple types using RegExp
* fileType: /^image\/(png|jpeg)$/
*/
fileType: string | RegExp;
/**
* Custom error message displayed when file type validation fails
* Can be provided as a static string, or as a factory function
* that receives the validation context (file and validator configuration)
* and returns a dynamic error message.
*
* @example
* // Static message
* new FileTypeValidator({ fileType: 'image/png', errorMessage: 'Only PNG allowed' })
*
* @example
* // Dynamic message based on file object and validator configuration
* new FileTypeValidator({
* fileType: 'image/png',
* errorMessage: ctx => `Received file type '${ctx.file?.mimetype}', but expected '${ctx.config.fileType}'`
* })
*/
errorMessage?: string | ((ctx: FileTypeValidatorContext) => string);
/**
* If `true`, the validator will skip the magic numbers validation.
* This can be useful when you can't identify some files as there are no common magic numbers available for some file types.
* @default false
*/
skipMagicNumbersValidation?: boolean;
/**
* If `true`, and magic number check fails, fallback to mimetype comparison.
* @default false
*/
fallbackToMimetype?: boolean;
};
/**
* Defines the built-in FileTypeValidator. It validates incoming files by examining
* their magic numbers using the file-type package, providing more reliable file type validation
* than just checking the mimetype string.
*
* @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators)
*
* @publicApi
*/
export declare class FileTypeValidator extends FileValidator<FileTypeValidatorOptions, IFile> {
buildErrorMessage(file?: IFile): string;
isValid(file?: IFile): Promise<boolean>;
}
export {};

View File

@@ -0,0 +1,102 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileTypeValidator = void 0;
const url_1 = require("url");
const logger_service_1 = require("../../services/logger.service");
const file_validator_interface_1 = require("./file-validator.interface");
const load_esm_1 = require("load-esm");
const logger = new logger_service_1.Logger('FileTypeValidator');
/**
* Defines the built-in FileTypeValidator. It validates incoming files by examining
* their magic numbers using the file-type package, providing more reliable file type validation
* than just checking the mimetype string.
*
* @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators)
*
* @publicApi
*/
class FileTypeValidator extends file_validator_interface_1.FileValidator {
buildErrorMessage(file) {
const { errorMessage, ...config } = this.validationOptions;
if (errorMessage) {
return typeof errorMessage === 'function'
? errorMessage({ file, config })
: errorMessage;
}
if (file?.mimetype) {
const baseMessage = `Validation failed (current file type is ${file.mimetype}, expected type is ${this.validationOptions.fileType})`;
/**
* If fallbackToMimetype is enabled, this means the validator failed to detect the file type
* via magic number inspection (e.g. due to an unknown or too short buffer),
* and instead used the mimetype string provided by the client as a fallback.
*
* This message clarifies that fallback logic was used, in case users rely on file signatures.
*/
if (this.validationOptions.fallbackToMimetype) {
return `${baseMessage} - magic number detection failed, used mimetype fallback`;
}
return baseMessage;
}
return `Validation failed (expected type is ${this.validationOptions.fileType})`;
}
async isValid(file) {
if (!this.validationOptions) {
return true;
}
const isFileValid = !!file && 'mimetype' in file;
// Skip magic number validation if set
if (this.validationOptions.skipMagicNumbersValidation) {
return (isFileValid && !!file.mimetype.match(this.validationOptions.fileType));
}
if (!isFileValid)
return false;
if (!file.buffer) {
if (this.validationOptions.fallbackToMimetype) {
return !!file.mimetype.match(this.validationOptions.fileType);
}
return false;
}
try {
let fileTypeModule;
try {
const resolvedPath = require.resolve('file-type');
fileTypeModule = (0, url_1.pathToFileURL)(resolvedPath).href;
}
catch {
fileTypeModule = 'file-type';
}
const { fileTypeFromBuffer } = await (0, load_esm_1.loadEsm)(fileTypeModule);
const fileType = await fileTypeFromBuffer(file.buffer);
if (fileType) {
// Match detected mime type against allowed type
return !!fileType.mime.match(this.validationOptions.fileType);
}
/**
* Fallback logic: If file-type cannot detect magic number (e.g. file too small),
* Optionally fall back to mimetype string for compatibility.
* This is useful for plain text, CSVs, or files without recognizable signatures.
*/
if (this.validationOptions.fallbackToMimetype) {
return !!file.mimetype.match(this.validationOptions.fileType);
}
return false;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Check for common ESM loading issues
if (errorMessage.includes('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING') ||
errorMessage.includes('Cannot find module') ||
errorMessage.includes('ERR_MODULE_NOT_FOUND')) {
logger.warn(`Failed to load the "file-type" package for magic number validation. ` +
`If you are using Jest, run it with NODE_OPTIONS="--experimental-vm-modules". ` +
`Error: ${errorMessage}`);
}
// Fallback to mimetype if enabled
if (this.validationOptions.fallbackToMimetype) {
return !!file.mimetype.match(this.validationOptions.fileType);
}
return false;
}
}
}
exports.FileTypeValidator = FileTypeValidator;

View File

@@ -0,0 +1,5 @@
import { IFile } from './interfaces';
export type FileValidatorContext<TConfig> = {
file?: IFile;
config: TConfig;
};

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,21 @@
import { IFile } from './interfaces';
/**
* Interface describing FileValidators, which can be added to a ParseFilePipe
*
* @see {ParseFilePipe}
* @publicApi
*/
export declare abstract class FileValidator<TValidationOptions = Record<string, any>, TFile extends IFile = IFile> {
protected readonly validationOptions: TValidationOptions;
constructor(validationOptions: TValidationOptions);
/**
* Indicates if this file should be considered valid, according to the options passed in the constructor.
* @param file the file from the request object
*/
abstract isValid(file?: TFile | TFile[] | Record<string, TFile[]>): boolean | Promise<boolean>;
/**
* Builds an error message in case the validation fails.
* @param file the file from the request object
*/
abstract buildErrorMessage(file: any): string;
}

View File

@@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileValidator = void 0;
/**
* Interface describing FileValidators, which can be added to a ParseFilePipe
*
* @see {ParseFilePipe}
* @publicApi
*/
class FileValidator {
constructor(validationOptions) {
this.validationOptions = validationOptions;
}
}
exports.FileValidator = FileValidator;

6
node_modules/@nestjs/common/pipes/file/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,6 @@
export * from './file-type.validator';
export * from './file-validator.interface';
export * from './max-file-size.validator';
export * from './parse-file-options.interface';
export * from './parse-file.pipe';
export * from './parse-file-pipe.builder';

9
node_modules/@nestjs/common/pipes/file/index.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
tslib_1.__exportStar(require("./file-type.validator"), exports);
tslib_1.__exportStar(require("./file-validator.interface"), exports);
tslib_1.__exportStar(require("./max-file-size.validator"), exports);
tslib_1.__exportStar(require("./parse-file-options.interface"), exports);
tslib_1.__exportStar(require("./parse-file.pipe"), exports);
tslib_1.__exportStar(require("./parse-file-pipe.builder"), exports);

View File

@@ -0,0 +1,5 @@
export interface IFile {
mimetype: string;
size: number;
buffer?: Buffer;
}

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1 @@
export * from './file.interface';

View File

@@ -0,0 +1,4 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
tslib_1.__exportStar(require("./file.interface"), exports);

View File

@@ -0,0 +1,44 @@
import { FileValidatorContext } from './file-validator-context.interface';
import { FileValidator } from './file-validator.interface';
import { IFile } from './interfaces';
type MaxFileSizeValidatorContext = FileValidatorContext<Omit<MaxFileSizeValidatorOptions, 'errorMessage' | 'message'>>;
export type MaxFileSizeValidatorOptions = {
/**
* Maximum allowed file size in bytes.
*/
maxSize: number;
/**
* @deprecated Use `errorMessage` instead.
*/
message?: string | ((maxSize: number) => string);
/**
* Custom error message returned when file size validation fails.
* Can be provided as a static string, or as a factory function
* that receives the validation context (file and validator configuration)
* and returns a dynamic error message.
*
* @example
* // Static message
* new MaxFileSizeValidator({ maxSize: 1000, errorMessage: 'File size exceeds the limit' })
*
* @example
* // Dynamic message based on file object and validator configuration
* new MaxFileSizeValidator({
* maxSize: 1000,
* errorMessage: ctx => `Received file size is ${ctx.file?.size}, but it must be smaller than ${ctx.config.maxSize}.`
* })
*/
errorMessage?: string | ((ctx: MaxFileSizeValidatorContext) => string);
};
/**
* Defines the built-in MaxSize File Validator
*
* @see [File Validators](https://docs.nestjs.com/techniques/file-upload#file-validation)
*
* @publicApi
*/
export declare class MaxFileSizeValidator extends FileValidator<MaxFileSizeValidatorOptions, IFile> {
buildErrorMessage(file?: IFile): string;
isValid(file?: IFile): boolean;
}
export {};

View File

@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MaxFileSizeValidator = void 0;
const file_validator_interface_1 = require("./file-validator.interface");
/**
* Defines the built-in MaxSize File Validator
*
* @see [File Validators](https://docs.nestjs.com/techniques/file-upload#file-validation)
*
* @publicApi
*/
class MaxFileSizeValidator extends file_validator_interface_1.FileValidator {
buildErrorMessage(file) {
const { errorMessage, message, ...config } = this.validationOptions;
if (errorMessage) {
return typeof errorMessage === 'function'
? errorMessage({ file, config })
: errorMessage;
}
if (message) {
return typeof message === 'function'
? message(this.validationOptions.maxSize)
: message;
}
if (file?.size) {
return `Validation failed (current file size is ${file.size}, expected size is less than ${this.validationOptions.maxSize})`;
}
return `Validation failed (expected size is less than ${this.validationOptions.maxSize})`;
}
isValid(file) {
if (!this.validationOptions || !file) {
return true;
}
return 'size' in file && file.size < this.validationOptions.maxSize;
}
}
exports.MaxFileSizeValidator = MaxFileSizeValidator;

View File

@@ -0,0 +1,15 @@
import { ErrorHttpStatusCode } from '../../utils/http-error-by-code.util';
import { FileValidator } from './file-validator.interface';
/**
* @publicApi
*/
export interface ParseFileOptions {
validators?: FileValidator[];
errorHttpStatusCode?: ErrorHttpStatusCode;
exceptionFactory?: (error: string) => any;
/**
* Defines if file parameter is required.
* @default true
*/
fileIsRequired?: boolean;
}

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,15 @@
import { FileTypeValidatorOptions } from './file-type.validator';
import { FileValidator } from './file-validator.interface';
import { MaxFileSizeValidatorOptions } from './max-file-size.validator';
import { ParseFileOptions } from './parse-file-options.interface';
import { ParseFilePipe } from './parse-file.pipe';
/**
* @publicApi
*/
export declare class ParseFilePipeBuilder {
private validators;
addMaxSizeValidator(options: MaxFileSizeValidatorOptions): this;
addFileTypeValidator(options: FileTypeValidatorOptions): this;
addValidator(validator: FileValidator): this;
build(additionalOptions?: Omit<ParseFileOptions, 'validators'>): ParseFilePipe;
}

View File

@@ -0,0 +1,33 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParseFilePipeBuilder = void 0;
const file_type_validator_1 = require("./file-type.validator");
const max_file_size_validator_1 = require("./max-file-size.validator");
const parse_file_pipe_1 = require("./parse-file.pipe");
/**
* @publicApi
*/
class ParseFilePipeBuilder {
constructor() {
this.validators = [];
}
addMaxSizeValidator(options) {
return this.addValidator(new max_file_size_validator_1.MaxFileSizeValidator(options));
}
addFileTypeValidator(options) {
return this.addValidator(new file_type_validator_1.FileTypeValidator(options));
}
addValidator(validator) {
this.validators.push(validator);
return this;
}
build(additionalOptions) {
const parseFilePipe = new parse_file_pipe_1.ParseFilePipe({
...additionalOptions,
validators: this.validators,
});
this.validators = [];
return parseFilePipe;
}
}
exports.ParseFilePipeBuilder = ParseFilePipeBuilder;

View File

@@ -0,0 +1,28 @@
import { PipeTransform } from '../../interfaces/features/pipe-transform.interface';
import { FileValidator } from './file-validator.interface';
import { ParseFileOptions } from './parse-file-options.interface';
/**
* Defines the built-in ParseFile Pipe. This pipe can be used to validate incoming files
* with `@UploadedFile()` decorator. You can use either other specific built-in validators
* or provide one of your own, simply implementing it through FileValidator interface
* and adding it to ParseFilePipe's constructor.
*
* @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes)
*
* @publicApi
*/
export declare class ParseFilePipe implements PipeTransform<any> {
protected exceptionFactory: (error: string) => any;
private readonly validators;
private readonly fileIsRequired;
constructor(options?: ParseFileOptions);
transform(value: any): Promise<any>;
private validateFilesOrFile;
private thereAreNoFilesIn;
protected validate(file: any): Promise<any>;
private validateOrThrow;
/**
* @returns list of validators used in this pipe.
*/
getValidators(): FileValidator<Record<string, any>, import("./interfaces").IFile>[];
}

View File

@@ -0,0 +1,76 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParseFilePipe = void 0;
const tslib_1 = require("tslib");
const core_1 = require("../../decorators/core");
const enums_1 = require("../../enums");
const http_error_by_code_util_1 = require("../../utils/http-error-by-code.util");
const shared_utils_1 = require("../../utils/shared.utils");
/**
* Defines the built-in ParseFile Pipe. This pipe can be used to validate incoming files
* with `@UploadedFile()` decorator. You can use either other specific built-in validators
* or provide one of your own, simply implementing it through FileValidator interface
* and adding it to ParseFilePipe's constructor.
*
* @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes)
*
* @publicApi
*/
let ParseFilePipe = class ParseFilePipe {
constructor(options = {}) {
const { exceptionFactory, errorHttpStatusCode = enums_1.HttpStatus.BAD_REQUEST, validators = [], fileIsRequired, } = options;
this.exceptionFactory =
exceptionFactory ||
(error => new http_error_by_code_util_1.HttpErrorByCode[errorHttpStatusCode](error));
this.validators = validators;
this.fileIsRequired = fileIsRequired ?? true;
}
async transform(value) {
const areThereAnyFilesIn = this.thereAreNoFilesIn(value);
if (areThereAnyFilesIn && this.fileIsRequired) {
throw this.exceptionFactory('File is required');
}
if (!areThereAnyFilesIn && this.validators.length) {
await this.validateFilesOrFile(value);
}
return value;
}
async validateFilesOrFile(value) {
if (Array.isArray(value)) {
await Promise.all(value.map(f => this.validate(f)));
}
else {
await this.validate(value);
}
}
thereAreNoFilesIn(value) {
const isEmptyArray = Array.isArray(value) && (0, shared_utils_1.isEmpty)(value);
const isEmptyObject = (0, shared_utils_1.isObject)(value) && (0, shared_utils_1.isEmpty)(Object.keys(value));
return (0, shared_utils_1.isUndefined)(value) || isEmptyArray || isEmptyObject;
}
async validate(file) {
for (const validator of this.validators) {
await this.validateOrThrow(file, validator);
}
return file;
}
async validateOrThrow(file, validator) {
const isValid = await validator.isValid(file);
if (!isValid) {
const errorMessage = validator.buildErrorMessage(file);
throw this.exceptionFactory(errorMessage);
}
}
/**
* @returns list of validators used in this pipe.
*/
getValidators() {
return this.validators;
}
};
exports.ParseFilePipe = ParseFilePipe;
exports.ParseFilePipe = ParseFilePipe = tslib_1.__decorate([
(0, core_1.Injectable)(),
tslib_1.__param(0, (0, core_1.Optional)()),
tslib_1.__metadata("design:paramtypes", [Object])
], ParseFilePipe);