This commit is contained in:
starified
2026-04-21 22:03:19 -04:00
parent 36e2d11f2e
commit 08bf320b57
4681 changed files with 566542 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
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,
};