/* ── Lightweight input validation middleware (no external deps) ─────────── */ /** * validate(schema) — Express middleware factory. * * Schema format: * { body: { field: { type, required, min, max, minLen, maxLen, match, oneOf, custom } }, * params: { ... }, * query: { ... } } * * Supported rule keys: * type — 'string' | 'number' | 'boolean' | 'object' | 'array' * required — true/false (default false) * min / max — for numbers * minLen / maxLen — for strings * match — RegExp * oneOf — array of allowed values * custom — fn(value) => string|null (return error string or null) */ function validate(schema) { return (req, res, next) => { const errors = []; for (const source of ['body', 'params', 'query']) { const rules = schema[source]; if (!rules) continue; const data = req[source] || {}; for (const [field, rule] of Object.entries(rules)) { const val = data[field]; const label = `${source}.${field}`; // Required check if (rule.required && (val === undefined || val === null || val === '')) { errors.push(`${label} обязателен`); continue; } // Skip optional absent fields if (val === undefined || val === null) continue; // Type check if (rule.type) { const t = rule.type; if (t === 'array' && !Array.isArray(val)) { errors.push(`${label} должен быть массивом`); continue; } if (t !== 'array' && typeof val !== t) { errors.push(`${label} должен быть типа ${t}`); continue; } } // Number constraints if (typeof val === 'number') { if (rule.min !== undefined && val < rule.min) errors.push(`${label} минимум ${rule.min}`); if (rule.max !== undefined && val > rule.max) errors.push(`${label} максимум ${rule.max}`); if (rule.integer && !Number.isInteger(val)) errors.push(`${label} должен быть целым числом`); } // String constraints if (typeof val === 'string') { if (rule.minLen !== undefined && val.length < rule.minLen) errors.push(`${label} минимум ${rule.minLen} символов`); if (rule.maxLen !== undefined && val.length > rule.maxLen) errors.push(`${label} максимум ${rule.maxLen} символов`); if (rule.match && !rule.match.test(val)) errors.push(`${label} неверный формат`); } // Enum if (rule.oneOf && !rule.oneOf.includes(val)) { errors.push(`${label} должен быть одним из: ${rule.oneOf.join(', ')}`); } // Custom validator if (rule.custom) { const err = rule.custom(val); if (err) errors.push(`${label}: ${err}`); } } } if (errors.length) { return res.status(400).json({ error: errors.join('; ') }); } next(); }; } module.exports = validate;