class ValidationError extends Error { constructor(message, details = []) { super(message); this.name = "ValidationError"; this.details = details; } } function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function normalizeString(input, rule = {}) { let value = String(input); if (rule.trim !== false) { value = value.trim(); } if (rule.toLowerCase) { value = value.toLowerCase(); } if (rule.toUpperCase) { value = value.toUpperCase(); } return value; } function validateField(value, key, rule, errors) { if (value == null) { if (rule.required) { errors.push({ field: key, issue: "required" }); } return undefined; } if (rule.type === "string") { const normalized = normalizeString(value, rule); if (rule.minLength != null && normalized.length < rule.minLength) { errors.push({ field: key, issue: "minLength", expected: rule.minLength }); } if (rule.maxLength != null && normalized.length > rule.maxLength) { errors.push({ field: key, issue: "maxLength", expected: rule.maxLength }); } if (rule.pattern && !rule.pattern.test(normalized)) { errors.push({ field: key, issue: "pattern" }); } if (rule.enum && !rule.enum.includes(normalized)) { errors.push({ field: key, issue: "enum", expected: rule.enum }); } return normalized; } if (rule.type === "int") { const asString = String(value).trim(); if (!/^-?\d+$/.test(asString)) { errors.push({ field: key, issue: "type", expected: "int" }); return undefined; } const parsed = Number.parseInt(asString, 10); if (!Number.isSafeInteger(parsed)) { errors.push({ field: key, issue: "type", expected: "safe-int" }); return undefined; } if (rule.min != null && parsed < rule.min) { errors.push({ field: key, issue: "min", expected: rule.min }); } if (rule.max != null && parsed > rule.max) { errors.push({ field: key, issue: "max", expected: rule.max }); } return parsed; } errors.push({ field: key, issue: "unsupported-rule" }); return undefined; } function validateObject(payload, schema, locationName) { const target = payload == null ? {} : payload; const errors = []; if (!isObject(target)) { throw new ValidationError(`Invalid ${locationName}. Expected an object.`, [ { field: locationName, issue: "type", expected: "object" }, ]); } const output = {}; const fields = schema.fields || {}; const allowUnknown = schema.allowUnknown === true; if (!allowUnknown) { for (const key of Object.keys(target)) { if (!Object.prototype.hasOwnProperty.call(fields, key)) { errors.push({ field: key, issue: "unknown" }); } } } for (const [key, rule] of Object.entries(fields)) { const validated = validateField(target[key], key, rule, errors); if (validated !== undefined) { output[key] = validated; } } if (errors.length > 0) { throw new ValidationError(`Invalid ${locationName}.`, errors); } return output; } function validateRequest(req, spec = {}) { return { params: spec.params ? validateObject(req.params, spec.params, "path parameters") : {}, query: spec.query ? validateObject(req.query, spec.query, "query parameters") : {}, body: spec.body ? validateObject(req.body, spec.body, "request body") : {}, }; } module.exports = { ValidationError, validateRequest, };