UNPKG

go-meow

Version:

A modular microservice template built with TypeScript, Express, and Prisma (MongoDB). It includes service scaffolding tools, consistent query utilities with data grouping, Zod validation, structured logging, comprehensive seeding system, and Swagger/OpenA

435 lines (434 loc) 17 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.sendPrismaErrorResponse = exports.handlePrismaError = exports.sendQueryValidationError = exports.sendForbiddenResponse = exports.sendUnauthorizedResponse = exports.sendConflictResponse = exports.sendNotFoundResponse = exports.sendErrorResponse = exports.sendSuccessResponse = exports.sendValidationError = exports.validateWithZod = exports.createErrorResponse = exports.createSuccessResponse = exports.createValidationErrorResponse = exports.formatZodErrors = exports.validateRequiredFields = exports.combineValidations = exports.validatePricing = exports.validateStringLength = exports.validateObject = exports.validateUrlArray = exports.validateObjectIdArray = exports.validateArray = exports.validateEnum = exports.validateUrl = exports.validateEmail = exports.validateBoolean = exports.validateInteger = exports.validatePositiveNumber = exports.validateNumber = exports.validateRequiredString = exports.validateObjectId = exports.URL_REGEX = exports.EMAIL_REGEX = exports.OBJECT_ID_REGEX = void 0; const logger_1 = require("../helper/logger"); const logger = (0, logger_1.getLogger)(); const validationLogger = logger.child({ module: "validationHelper" }); exports.OBJECT_ID_REGEX = /^[0-9a-fA-F]{24}$/; exports.EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; exports.URL_REGEX = /^https?:\/\/.+/; const validateObjectId = (id, fieldName = "ID") => { if (!id || typeof id !== "string" || !id.trim()) { return { isValid: false, error: `${fieldName} is required` }; } if (!exports.OBJECT_ID_REGEX.test(id)) { return { isValid: false, error: `Invalid ${fieldName} format` }; } return { isValid: true }; }; exports.validateObjectId = validateObjectId; const validateRequiredString = (value, fieldName) => { if (!value || typeof value !== "string" || !value.trim()) { return { isValid: false, error: `${fieldName} is required and must be a non-empty string` }; } return { isValid: true }; }; exports.validateRequiredString = validateRequiredString; const validateNumber = (value, fieldName, min, max) => { if (value === undefined || value === null) { return { isValid: true }; } if (typeof value !== "number" || isNaN(value)) { return { isValid: false, error: `${fieldName} must be a valid number` }; } if (min !== undefined && value < min) { return { isValid: false, error: `${fieldName} must be at least ${min}` }; } if (max !== undefined && value > max) { return { isValid: false, error: `${fieldName} must be at most ${max}` }; } return { isValid: true }; }; exports.validateNumber = validateNumber; const validatePositiveNumber = (value, fieldName) => { return (0, exports.validateNumber)(value, fieldName, 0); }; exports.validatePositiveNumber = validatePositiveNumber; const validateInteger = (value, fieldName, min, max) => { if (value === undefined || value === null) { return { isValid: true }; } if (typeof value !== "number" || !Number.isInteger(value)) { return { isValid: false, error: `${fieldName} must be a valid integer` }; } if (min !== undefined && value < min) { return { isValid: false, error: `${fieldName} must be at least ${min}` }; } if (max !== undefined && value > max) { return { isValid: false, error: `${fieldName} must be at most ${max}` }; } return { isValid: true }; }; exports.validateInteger = validateInteger; const validateBoolean = (value, fieldName) => { if (value === undefined || value === null) { return { isValid: true }; } if (typeof value !== "boolean") { return { isValid: false, error: `${fieldName} must be a boolean` }; } return { isValid: true }; }; exports.validateBoolean = validateBoolean; const validateEmail = (value, fieldName) => { if (value === undefined || value === null) { return { isValid: true }; } if (typeof value !== "string" || !exports.EMAIL_REGEX.test(value)) { return { isValid: false, error: `${fieldName} must be a valid email address` }; } return { isValid: true }; }; exports.validateEmail = validateEmail; const validateUrl = (value, fieldName) => { if (value === undefined || value === null) { return { isValid: true }; } if (typeof value !== "string" || !exports.URL_REGEX.test(value)) { return { isValid: false, error: `${fieldName} must be a valid URL` }; } return { isValid: true }; }; exports.validateUrl = validateUrl; const validateEnum = (value, allowedValues, fieldName) => { if (value === undefined || value === null) { return { isValid: true }; } if (!allowedValues.includes(value)) { return { isValid: false, error: `${fieldName} must be one of: ${allowedValues.join(", ")}`, }; } return { isValid: true }; }; exports.validateEnum = validateEnum; const validateArray = (value, fieldName, minLength, maxLength) => { if (value === undefined || value === null) { return { isValid: true }; } if (!Array.isArray(value)) { return { isValid: false, error: `${fieldName} must be an array` }; } if (minLength !== undefined && value.length < minLength) { return { isValid: false, error: `${fieldName} must have at least ${minLength} items` }; } if (maxLength !== undefined && value.length > maxLength) { return { isValid: false, error: `${fieldName} must have at most ${maxLength} items` }; } return { isValid: true }; }; exports.validateArray = validateArray; const validateObjectIdArray = (value, fieldName) => { const arrayValidation = (0, exports.validateArray)(value, fieldName); if (!arrayValidation.isValid) { return arrayValidation; } if (value && Array.isArray(value)) { for (const id of value) { const idValidation = (0, exports.validateObjectId)(id, `${fieldName} item`); if (!idValidation.isValid) { return { isValid: false, error: `All ${fieldName} must be valid MongoDB ObjectIds`, }; } } } return { isValid: true }; }; exports.validateObjectIdArray = validateObjectIdArray; const validateUrlArray = (value, fieldName) => { const arrayValidation = (0, exports.validateArray)(value, fieldName); if (!arrayValidation.isValid) { return arrayValidation; } if (value && Array.isArray(value)) { for (const url of value) { const urlValidation = (0, exports.validateUrl)(url, `${fieldName} item`); if (!urlValidation.isValid) { return { isValid: false, error: `All ${fieldName} must be valid URLs` }; } } } return { isValid: true }; }; exports.validateUrlArray = validateUrlArray; const validateObject = (value, fieldName) => { if (value === undefined || value === null) { return { isValid: true }; } if (typeof value !== "object" || Array.isArray(value)) { return { isValid: false, error: `${fieldName} must be an object` }; } return { isValid: true }; }; exports.validateObject = validateObject; const validateStringLength = (value, fieldName, minLength, maxLength) => { if (value === undefined || value === null) { return { isValid: true }; } if (typeof value !== "string") { return { isValid: false, error: `${fieldName} must be a string` }; } if (minLength !== undefined && value.length < minLength) { return { isValid: false, error: `${fieldName} must be at least ${minLength} characters long`, }; } if (maxLength !== undefined && value.length > maxLength) { return { isValid: false, error: `${fieldName} must be at most ${maxLength} characters long`, }; } return { isValid: true }; }; exports.validateStringLength = validateStringLength; const validatePricing = (pricing) => { const arrayValidation = (0, exports.validateArray)(pricing, "pricing"); if (!arrayValidation.isValid) { return arrayValidation; } if (pricing && Array.isArray(pricing)) { for (const price of pricing) { const objectValidation = (0, exports.validateObject)(price, "pricing item"); if (!objectValidation.isValid) { return objectValidation; } const priceValidation = (0, exports.validatePositiveNumber)(price.price, "price"); if (!priceValidation.isValid) { return priceValidation; } const currencyValidation = (0, exports.validateRequiredString)(price.currency, "currency"); if (!currencyValidation.isValid) { return currencyValidation; } } } return { isValid: true }; }; exports.validatePricing = validatePricing; const combineValidations = (...validations) => { for (const validation of validations) { if (!validation.isValid) { return validation; } } return { isValid: true }; }; exports.combineValidations = combineValidations; const validateRequiredFields = (data, requiredFields) => { for (const field of requiredFields) { if (!data[field] || (typeof data[field] === "string" && !data[field].trim())) { return { isValid: false, error: `${field} is required` }; } } return { isValid: true }; }; exports.validateRequiredFields = validateRequiredFields; const formatZodErrors = (error) => { return error.errors.map((err) => { const field = err.path.join("."); let message = err.message; if (err.code === "invalid_type") { if (err.received === "undefined") { message = `${field.charAt(0).toUpperCase() + field.slice(1)} is required`; } else { message = `${field} must be a ${err.expected}`; } } else if (err.code === "invalid_string") { if (err.validation === "email") { message = `${field.charAt(0).toUpperCase() + field.slice(1)} must be a valid email address`; } else if (err.validation === "regex") { message = `${field.charAt(0).toUpperCase() + field.slice(1)} format is invalid`; } } else if (err.code === "too_small") { message = `${field.charAt(0).toUpperCase() + field.slice(1)} must be at least ${err.minimum} characters`; } else if (err.code === "too_big") { message = `${field.charAt(0).toUpperCase() + field.slice(1)} must be at most ${err.maximum} characters`; } return { field, message, }; }); }; exports.formatZodErrors = formatZodErrors; const createValidationErrorResponse = (message, errors) => { return { status: "error", message, errors, code: 400, timestamp: new Date().toISOString(), }; }; exports.createValidationErrorResponse = createValidationErrorResponse; const createSuccessResponse = (message, data) => { return { status: "success", message, ...(data && { data }), timestamp: new Date().toISOString(), }; }; exports.createSuccessResponse = createSuccessResponse; const createErrorResponse = (message, code = "ERROR", errors) => { return { status: "error", message, ...(errors && { errors }), code, timestamp: new Date().toISOString(), }; }; exports.createErrorResponse = createErrorResponse; const validateWithZod = (schema, data) => { const validation = schema.safeParse(data); if (validation.success) { return { success: true, data: validation.data }; } else { return { success: false, error: (0, exports.createValidationErrorResponse)("Validation failed", (0, exports.formatZodErrors)(validation.error)), }; } }; exports.validateWithZod = validateWithZod; const sendValidationError = (res, message, errors) => { return res.status(400).json((0, exports.createValidationErrorResponse)(message, errors)); }; exports.sendValidationError = sendValidationError; const sendSuccessResponse = (res, message, data, statusCode = 200) => { return res.status(statusCode).json((0, exports.createSuccessResponse)(message, data)); }; exports.sendSuccessResponse = sendSuccessResponse; const sendErrorResponse = (res, message, code = "ERROR", errors, statusCode = 500) => { return res.status(statusCode).json((0, exports.createErrorResponse)(message, code, errors)); }; exports.sendErrorResponse = sendErrorResponse; const sendNotFoundResponse = (res, resource, field = "id") => { return res .status(404) .json((0, exports.createErrorResponse)(`${resource} not found`, "NOT_FOUND", [ { field, message: `${resource} does not exist` }, ])); }; exports.sendNotFoundResponse = sendNotFoundResponse; const sendConflictResponse = (res, field, message) => { return res .status(409) .json((0, exports.createErrorResponse)("Resource conflict", "CONFLICT", [{ field, message }])); }; exports.sendConflictResponse = sendConflictResponse; const sendUnauthorizedResponse = (res, message = "Unauthorized") => { return res.status(401).json((0, exports.createErrorResponse)(message, "UNAUTHORIZED")); }; exports.sendUnauthorizedResponse = sendUnauthorizedResponse; const sendForbiddenResponse = (res, message = "Forbidden") => { return res.status(403).json((0, exports.createErrorResponse)(message, "FORBIDDEN")); }; exports.sendForbiddenResponse = sendForbiddenResponse; const sendQueryValidationError = (res, message, field = "query") => { return res.status(400).json({ status: "error", message, code: "VALIDATION_ERROR", errors: [{ field, message }], timestamp: new Date().toISOString(), }); }; exports.sendQueryValidationError = sendQueryValidationError; const handlePrismaError = (error) => { if (error.code === "P2025") { return { message: "Record not found", code: "NOT_FOUND", statusCode: 404, }; } if (error.code === "P2002") { return { message: "A record with this data already exists", code: "DUPLICATE_RECORD", statusCode: 409, }; } if (error.code === "P2003") { return { message: "Related record constraint failed", code: "CONSTRAINT_ERROR", statusCode: 400, }; } if (error.message?.includes("Invalid value for argument")) { const match = error.message.match(/Invalid value for argument `(\w+)`\. Expected (\w+)\./); const field = match?.[1] || "unknown"; const expected = match?.[2] || "valid value"; return { message: `Invalid value for ${field}. Expected ${expected}.`, code: "INVALID_ARGUMENT", statusCode: 400, }; } if (error.message?.includes("Unknown field")) { const match = error.message.match(/Unknown field `(\w+)` for (\w+) statement on model `(\w+)`/); const field = match?.[1] || "unknown"; const statement = match?.[2] || "query"; return { message: `Invalid field '${field}' in ${statement} statement.`, code: "UNKNOWN_FIELD", statusCode: 400, }; } if (error.name === "PrismaClientValidationError") { return { message: "Invalid query parameters", code: "VALIDATION_ERROR", statusCode: 400, }; } if (error.name === "PrismaClientKnownRequestError") { return { message: "Request processing error", code: "REQUEST_ERROR", statusCode: 400, }; } return { message: "Internal server error", code: "INTERNAL_ERROR", statusCode: 500, }; }; exports.handlePrismaError = handlePrismaError; const sendPrismaErrorResponse = (res, error, logger) => { const { message, code, statusCode } = (0, exports.handlePrismaError)(error); logger?.error(`Prisma error: ${error.message}`, error); let fieldName = "database"; if (error.message?.includes("Unknown field") || code === "UNKNOWN_FIELD") { fieldName = "fields"; } else { const modelMatch = error.message?.match(/on model `(\w+)`/); if (modelMatch) { fieldName = modelMatch[1].toLowerCase(); } } return res.status(statusCode).json({ status: "error", message, code, errors: [{ field: fieldName, message }], timestamp: new Date().toISOString(), }); }; exports.sendPrismaErrorResponse = sendPrismaErrorResponse;