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
JavaScript
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;
;