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
JavaScript
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(),
});
};