UNPKG

prisma-class-dto-generator

Version:

Generate Prisma DTOs with seamless class-validator and class-transformer integration for TypeScript applications.

280 lines 11.9 kB
import { IsArray, IsDefined, IsOptional } from "class-validator"; import { validationMetadatasToSchemas, JSONSchema } from "class-validator-jsonschema"; import { BadRequestError, createParamDecorator, UseBefore } from "routing-controllers"; import multer from "multer"; import bytes from "bytes"; import { OpenAPI } from "routing-controllers-openapi"; import { toDTO } from "../utils/toDTO.js"; export function parseFileSize(value) { if (typeof value === "number") { return value; } return parseFloat(bytes(value)); } const FILE_FIELDS_METADATA = Symbol("FILE_FIELDS_METADATA"); function storeFileFieldMetadata(target, propertyKey, isArray, options) { const existing = Reflect.getMetadata(FILE_FIELDS_METADATA, target.constructor) || []; existing.push({ propertyKey, isArray, options, }); Reflect.defineMetadata(FILE_FIELDS_METADATA, existing, target.constructor); } function getFileFieldsMetadata(dtoClass) { return Reflect.getMetadata(FILE_FIELDS_METADATA, dtoClass) || []; } /** * @IsFile - a decorator for a single file field (Express.Multer.File). */ export function IsFile(options = {}) { return (target, propertyKey) => { storeFileFieldMetadata(target, propertyKey, false, options); if (!options.isRequired) { IsOptional()(target, propertyKey); } else { IsDefined()(target, propertyKey); } const schema = { type: "string", format: "binary", description: generateFileDescription(options, false), }; if (options.maxSize) { schema['x-maxSize'] = options.maxSize; } if (options.minSize) { schema['x-minSize'] = options.minSize; } if (options.mimeTypes) { schema['x-mimeTypes'] = options.mimeTypes?.map((item) => (item instanceof RegExp ? item.toString() : new RegExp(item).toString())); } if (options.name) { schema['x-fieldName'] = options.name; } JSONSchema(schema)(target, propertyKey); }; } /** * @IsFiles - a decorator for an array of files (Express.Multer.File[]). */ export function IsFiles(options = {}) { return (target, propertyKey) => { storeFileFieldMetadata(target, propertyKey, true, options); if (!options.isRequired) { IsOptional()(target, propertyKey); } else { IsDefined()(target, propertyKey); } IsArray()(target, propertyKey); const schema = { type: "array", description: generateFileDescription(options, true), items: { type: "string", format: "binary", }, }; if (typeof options.maxFiles === 'number') { schema.maxItems = options.maxFiles; } if (typeof options.minFiles === 'number') { schema.minItems = options.minFiles; } if (options.maxSize) { schema['x-maxSize'] = options.maxSize; } if (options.minSize) { schema['x-minSize'] = options.minSize; } if (options.mimeTypes) { schema['x-mimeTypes'] = options.mimeTypes?.map((item) => (item instanceof RegExp ? item.toString() : new RegExp(item).toString())); } if (options.name) { schema['x-fieldName'] = options.name; } JSONSchema(schema)(target, propertyKey); }; } /** * @BodyMultipart - merges req.body and req.files into one object. */ export function BodyMultipart(type) { return createParamDecorator({ required: true, async value(action) { const req = action.request; const bodyData = type ? toDTO(type, req.body || {}) : req.body || {}; const data = Array.isArray(req.files) ? { ...bodyData, files: req.files } : { ...bodyData, ...req.files || {} }; return data; }, }); } function generateFileDescription(options, isArray) { let description = `Upload ${isArray ? "multiple files" : "a file"}`; if (options.name) { description += ` under the key '${options.name}'.`; } if (options.mimeTypes && options.mimeTypes.length > 0) { const allowedTypes = options.mimeTypes.map((regex) => regex.toString()).join(", "); description += ` Allowed MIME types: ${allowedTypes}.`; } if (options.minSize) { description += ` Minimum size: ${options.minSize}.`; } if (options.maxSize) { description += ` Maximum size: ${options.maxSize}.`; } if (options.minFiles) { description += ` Minimum number of files: ${options.minFiles}.`; } if (options.maxFiles) { description += ` Maximum number of files: ${options.maxFiles}.`; } return description; } export function UseMulter(dtoClass) { const uploadEngine = multer({ storage: multer.memoryStorage() }); return function (target, propertyKey, descriptor) { const fileFields = getFileFieldsMetadata(dtoClass); const multerFields = fileFields.map((meta) => { const fieldName = meta.options.name || meta.propertyKey; const maxCount = meta.isArray ? (meta.options.maxFiles ?? 99) : 1; return { name: fieldName, maxCount }; }); UseBefore((req, res, next) => { uploadEngine.fields(multerFields)(req, res, (err) => { if (err) return next(err); if (!req.files) return next(); for (const meta of fileFields) { const fieldName = meta.options.name || meta.propertyKey; const files = req.files[fieldName]; if (!files || files.length === 0) { if (meta.options.isRequired) { return next(new BadRequestError(`No files uploaded for field: ${fieldName}`)); } else { if (meta.isArray) { req.files[fieldName] = []; } else { req.files[fieldName] = undefined; } } continue; } if (meta.isArray) { if (meta.options.minFiles && files.length < meta.options.minFiles) { return next(new BadRequestError(`Too few files uploaded for '${fieldName}'. Minimum number: ${meta.options.minFiles}.`)); } if (meta.options.maxFiles && files.length > meta.options.maxFiles) { return next(new BadRequestError(`Too many files uploaded for '${fieldName}'. Maximum number: ${meta.options.maxFiles}.`)); } } else { if (meta?.options?.isRequired && files.length === 0) { return next(new BadRequestError(`No files uploaded for field: ${fieldName}`)); } else if (files?.length) { req.files[fieldName] = files[0]; } } for (const file of files) { if (meta.options.minSize) { const minSizeBytes = parseFileSize(meta.options.minSize); if (file.size < minSizeBytes) { return next(new BadRequestError(`File ${file.originalname} is too small. Minimum size is ${meta.options.minSize}.`)); } } if (meta.options.maxSize) { const maxSizeBytes = parseFileSize(meta.options.maxSize); if (file.size > maxSizeBytes) { return next(new BadRequestError(`File ${file.originalname} is too large. Maximum size is ${meta.options.maxSize}.`)); } } if (meta.options.mimeTypes && meta.options.mimeTypes.length > 0) { const matched = meta.options.mimeTypes.some((item) => { const regex = item instanceof RegExp ? item : new RegExp(item); // Преобразуем строку в RegExp, если нужно return regex.test(file.mimetype); }); if (!matched) { return next(new BadRequestError(`File ${file.originalname} has invalid type (${file.mimetype}). Allowed: ${meta.options.mimeTypes.map((item) => (item instanceof RegExp ? item.toString() : new RegExp(item).toString())).join(", ")}.`)); } } } } next(); }); })(target, propertyKey, descriptor); return OpenAPI((operation) => { operation.requestBody = operation.requestBody || {}; operation.requestBody.content = operation.requestBody.content || {}; const schemas = validationMetadatasToSchemas({ refPointerPrefix: "#/components/schemas/" }); const dtoSchema = schemas[dtoClass.name]; if (!dtoSchema) { throw new Error(`Schema for ${dtoClass.name} not found. Make sure the class is decorated with class-validator, reflect-metadata, and the schema generation is called appropriately.`); } if (dtoSchema.type !== "object") { dtoSchema.type = "object"; } if (!dtoSchema.properties) { dtoSchema.properties = {}; } for (const meta of fileFields) { const fieldName = meta.options.name || meta.propertyKey; if (meta.isArray) { dtoSchema.properties[fieldName] = { type: "array", description: generateFileDescription(meta.options, true), items: { type: "string", format: "binary", }, }; if (meta.options.minFiles) { dtoSchema.properties[fieldName].minItems = meta.options.minFiles; } if (meta.options.maxFiles) { dtoSchema.properties[fieldName].maxItems = meta.options.maxFiles; } } else { dtoSchema.properties[fieldName] = { type: "string", format: "binary", description: generateFileDescription(meta.options, false), }; } } operation.requestBody.content["multipart/form-data"] = { schema: dtoSchema, }; return operation; })(target, propertyKey, descriptor); }; } export function UseMultipart() { return function (target, propertyKey, descriptor) { const upload = multer(); UseBefore(upload.any())(target, propertyKey, descriptor); OpenAPI({ requestBody: { required: true, content: { "multipart/form-data": { schema: { type: "object", }, }, }, }, })(target, propertyKey, descriptor); }; } //# sourceMappingURL=files.js.map