138 lines
3.4 KiB
JavaScript
138 lines
3.4 KiB
JavaScript
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,
|
|
};
|