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

400 lines (399 loc) 14.3 kB
import { getLogger } from "../helper/logger"; const logger = getLogger(); const validationLogger = logger.child({ module: "validationHelper" }); export const OBJECT_ID_REGEX = /^[0-9a-fA-F]{24}$/; export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; export const URL_REGEX = /^https?:\/\/.+/; export const validateObjectId = (id, fieldName = "ID") => { if (!id || typeof id !== "string" || !id.trim()) { return { isValid: false, error: `${fieldName} is required` }; } if (!OBJECT_ID_REGEX.test(id)) { return { isValid: false, error: `Invalid ${fieldName} format` }; } return { isValid: true }; }; export 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 }; }; export 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 }; }; export const validatePositiveNumber = (value, fieldName) => { return validateNumber(value, fieldName, 0); }; export 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 }; }; export 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 }; }; export const validateEmail = (value, fieldName) => { if (value === undefined || value === null) { return { isValid: true }; } if (typeof value !== "string" || !EMAIL_REGEX.test(value)) { return { isValid: false, error: `${fieldName} must be a valid email address` }; } return { isValid: true }; }; export const validateUrl = (value, fieldName) => { if (value === undefined || value === null) { return { isValid: true }; } if (typeof value !== "string" || !URL_REGEX.test(value)) { return { isValid: false, error: `${fieldName} must be a valid URL` }; } return { isValid: true }; }; export 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 }; }; export 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 }; }; export const validateObjectIdArray = (value, fieldName) => { const arrayValidation = validateArray(value, fieldName); if (!arrayValidation.isValid) { return arrayValidation; } if (value && Array.isArray(value)) { for (const id of value) { const idValidation = validateObjectId(id, `${fieldName} item`); if (!idValidation.isValid) { return { isValid: false, error: `All ${fieldName} must be valid MongoDB ObjectIds`, }; } } } return { isValid: true }; }; export const validateUrlArray = (value, fieldName) => { const arrayValidation = validateArray(value, fieldName); if (!arrayValidation.isValid) { return arrayValidation; } if (value && Array.isArray(value)) { for (const url of value) { const urlValidation = validateUrl(url, `${fieldName} item`); if (!urlValidation.isValid) { return { isValid: false, error: `All ${fieldName} must be valid URLs` }; } } } return { isValid: true }; }; export 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 }; }; export 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 }; }; export const validatePricing = (pricing) => { const arrayValidation = validateArray(pricing, "pricing"); if (!arrayValidation.isValid) { return arrayValidation; } if (pricing && Array.isArray(pricing)) { for (const price of pricing) { const objectValidation = validateObject(price, "pricing item"); if (!objectValidation.isValid) { return objectValidation; } const priceValidation = validatePositiveNumber(price.price, "price"); if (!priceValidation.isValid) { return priceValidation; } const currencyValidation = validateRequiredString(price.currency, "currency"); if (!currencyValidation.isValid) { return currencyValidation; } } } return { isValid: true }; }; export const combineValidations = (...validations) => { for (const validation of validations) { if (!validation.isValid) { return validation; } } return { isValid: true }; }; export 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 }; }; export 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, }; }); }; export const createValidationErrorResponse = (message, errors) => { return { status: "error", message, errors, code: 400, timestamp: new Date().toISOString(), }; }; export const createSuccessResponse = (message, data) => { return { status: "success", message, ...(data && { data }), timestamp: new Date().toISOString(), }; }; export const createErrorResponse = (message, code = "ERROR", errors) => { return { status: "error", message, ...(errors && { errors }), code, timestamp: new Date().toISOString(), }; }; export const validateWithZod = (schema, data) => { const validation = schema.safeParse(data); if (validation.success) { return { success: true, data: validation.data }; } else { return { success: false, error: createValidationErrorResponse("Validation failed", formatZodErrors(validation.error)), }; } }; export const sendValidationError = (res, message, errors) => { return res.status(400).json(createValidationErrorResponse(message, errors)); }; export const sendSuccessResponse = (res, message, data, statusCode = 200) => { return res.status(statusCode).json(createSuccessResponse(message, data)); }; export const sendErrorResponse = (res, message, code = "ERROR", errors, statusCode = 500) => { return res.status(statusCode).json(createErrorResponse(message, code, errors)); }; export const sendNotFoundResponse = (res, resource, field = "id") => { return res .status(404) .json(createErrorResponse(`${resource} not found`, "NOT_FOUND", [ { field, message: `${resource} does not exist` }, ])); }; export const sendConflictResponse = (res, field, message) => { return res .status(409) .json(createErrorResponse("Resource conflict", "CONFLICT", [{ field, message }])); }; export const sendUnauthorizedResponse = (res, message = "Unauthorized") => { return res.status(401).json(createErrorResponse(message, "UNAUTHORIZED")); }; export const sendForbiddenResponse = (res, message = "Forbidden") => { return res.status(403).json(createErrorResponse(message, "FORBIDDEN")); }; export const sendQueryValidationError = (res, message, field = "query") => { return res.status(400).json({ status: "error", message, code: "VALIDATION_ERROR", errors: [{ field, message }], timestamp: new Date().toISOString(), }); }; export 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, }; }; export const sendPrismaErrorResponse = (res, error, logger) => { const { message, code, statusCode } = 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(), }); };