При переносе приложений между версиями API, изменении схем данных, обновлении архитектуры или переходе между фреймворками возникает задача гарантировать корректность входящих данных. Ошибки валидации во время миграции способны привести к:
Библиотека Validator.js позволяет автоматизировать значительную часть подобных проверок и снизить количество ручной логики.
Предположим, старая версия приложения использовала следующий формат пользователя:
{
"name": "Alex",
"mail": "alex@test.com",
"age": "25"
}
Новая версия требует:
{
"fullName": "Alex",
"email": "alex@test.com",
"age": 25
}
Во время миграции необходимо:
Validator.js позволяет централизовать такие проверки.
npm install validator
const validator = require('validator');
import validator from 'validator';
import validator from 'validator';
function validateUser(user) {
return validator.isEmail(user.email);
}
console.log(validateUser({
email: 'admin@test.com'
}));
Результат:
true
Во многих старых базах встречаются пустые значения:
{
name: '',
email: 'test@test.com'
}
Validator.js помогает автоматически отбрасывать подобные записи.
function validateName(name) {
return !validator.isEmpty(name);
}
Во время миграции часто требуется преобразовывать строки в числа.
function validateAge(age) {
return validator.isInt(age.toString(), {
min: 0,
max: 120
});
}
import validator from 'validator';
function migrateUser(oldUser) {
const errors = [];
if (!validator.isEmail(oldUser.mail)) {
errors.push('Некорректный email');
}
if (validator.isEmpty(oldUser.name)) {
errors.push('Пустое имя');
}
if (!validator.isInt(oldUser.age)) {
errors.push('Возраст должен быть числом');
}
if (errors.length > 0) {
return {
success: false,
errors
};
}
return {
success: true,
data: {
fullName: oldUser.name,
email: oldUser.mail,
age: Number(oldUser.age)
}
};
}
const users = [
{
name: 'Alex',
mail: 'alex@test.com',
age: '25'
},
{
name: '',
mail: 'wrong-email',
age: 'abc'
}
];
const migratedUsers = [];
const failedUsers = [];
for (const user of users) {
const result = migrateUser(user);
if (result.success) {
migratedUsers.push(result.data);
} else {
failedUsers.push({
user,
errors: result.errors
});
}
}
for (const item of failedUsers) {
console.log('Ошибка миграции:');
console.log(item.user);
console.log(item.errors);
}
Validator.js содержит функции очистки данных, что особенно важно при автоматической миграции.
const email = validator.normalizeEmail(
'Admin@TEST.COM'
);
console.log(email);
Результат:
admin@test.com
const name = validator.trim(' Alex ');
console.log(name);
Результат:
Alex
Во время миграции старых данных могут встречаться XSS-вставки.
const value = validator.escape(
'<script>alert(1)</script>'
);
console.log(value);
При обновлении API старые клиенты могут отправлять устаревшие форматы данных.
import validator from 'validator';
function validateRequest(req, res, next) {
const email = req.body.email;
if (!validator.isEmail(email)) {
return res.status(400).json({
error: 'Invalid email'
});
}
next();
}
function normalizeUser(user) {
if (user.mail && !user.email) {
user.email = user.mail;
}
return user;
}
function validateNormalizedUser(user) {
return validator.isEmail(user.email);
}
{
login: 'alex',
status: '1'
}
{
username: 'alex',
isActive: true
}
function migrateSchema(data) {
return {
username: data.login,
isActive: data.status === '1'
};
}
function validateSchema(data) {
return validator.isLength(
data.username,
{
min: 3
}
);
}
async function saveUser(user) {
if (!validator.isEmail(user.email)) {
throw new Error('Invalid email');
}
await db.users.insert(user);
}
name,email,age
Alex,alex@test.com,25
Ivan,wrong-email,abc
function validateCsvRow(row) {
return (
validator.isEmail(row.email) &&
validator.isInt(row.age)
);
}
const validRows = rows.filter(validateCsvRow);
async function migrateUsers(users) {
return Promise.all(
users.map(async user => {
const result = migrateUser(user);
if (!result.success) {
return null;
}
return result.data;
})
);
}
const url = 'https://example.com';
validator.isURL(url);
Во время миграции распределённых систем часто требуется проверка идентификаторов.
validator.isUUID(
'550e8400-e29b-41d4-a716-446655440000'
);
validator.isJSON('{"name":"Alex"}');
function migrateDate(date) {
if (!validator.isISO8601(date)) {
return null;
}
return new Date(date);
}
При миграции миллионов записей нежелательно загружать весь массив в память.
import fs from 'fs';
import readline from 'readline';
const rl = readline.createInterface({
input: fs.createReadStream('users.txt')
});
rl.on('line', line => {
if (validator.isJSON(line)) {
const user = JSON.parse(line);
console.log(user);
}
});
const rules = {
email(value) {
return validator.isEmail(value);
},
age(value) {
return validator.isInt(
value.toString(),
{
min: 0,
max: 120
}
);
}
};
function validate(field, value) {
if (!rules[field]) {
return true;
}
return rules[field](value);
}
function buildError(field, message) {
return {
field,
message,
timestamp: Date.now()
};
}
function validateEnv(env) {
return (
validator.isPort(env.PORT) &&
validator.isURL(env.API_URL)
);
}
function compatibilityMiddleware(
req,
res,
next
) {
if (req.body.mail) {
req.body.email = req.body.mail;
}
next();
}
function sanitizeInput(value) {
return validator.escape(
validator.trim(value)
);
}
const schema = {
email: 'email',
age: 'int',
website: 'url'
};
function validateField(type, value) {
switch (type) {
case 'email':
return validator.isEmail(value);
case 'int':
return validator.isInt(value);
case 'url':
return validator.isURL(value);
default:
return false;
}
}
function validateFinalUser(user) {
return (
validator.isEmail(user.email) &&
validator.isLength(
user.fullName,
{
min: 2
}
)
);
}
User.beforeCreate(user => {
if (!validator.isEmail(user.email)) {
throw new Error('Invalid email');
}
});
describe('Migration', () => {
test('should migrate user', () => {
const result = migrateUser({
name: 'Alex',
mail: 'alex@test.com',
age: '25'
});
expect(result.success)
.toBe(true);
});
});
undefinedНекоторые методы Validator.js принимают только строки.
Неправильно:
validator.isEmail(undefined);
Правильно:
validator.isEmail(
String(value || '')
);
Неправильно:
validator.isInt(25);
Правильно:
validator.isInt('25');
Плохо:
users.forEach(user => {
validator.isEmail(user.email);
validator.isEmail(user.email);
});
Хорошо:
users.forEach(user => {
const isValidEmail =
validator.isEmail(user.email);
console.log(isValidEmail);
});
Рекомендуемая структура:
migration/
├── validators/
├── transformers/
├── reports/
├── services/
└── scripts/
import validator from 'validator';
export function validateUser(user) {
return {
email: validator.isEmail(user.email),
age: validator.isInt(
user.age.toString()
)
};
}
export function transformUser(user) {
return {
fullName: user.name,
email: user.mail,
age: Number(user.age)
};
}
function repairEmail(email) {
const normalized =
validator.normalizeEmail(email);
if (!normalized) {
return null;
}
return normalized;
}
Некорректные данные полностью отклоняются.
if (!validator.isEmail(email)) {
throw new Error('Invalid email');
}
Некорректные данные помечаются.
if (!validator.isEmail(email)) {
warnings.push({
field: 'email',
value: email
});
}
function validateCorporateEmail(email) {
return validator.isEmail(email, {
host_whitelist: ['company.com']
});
}
import validator from 'validator';
class MigrationService {
validate(user) {
const errors = [];
if (!validator.isEmail(user.mail)) {
errors.push('Invalid email');
}
if (!validator.isInt(user.age)) {
errors.push('Invalid age');
}
return errors;
}
transform(user) {
return {
fullName: validator.trim(user.name),
email: validator.normalizeEmail(
user.mail
),
age: Number(user.age)
};
}
migrate(user) {
const errors =
this.validate(user);
if (errors.length > 0) {
return {
success: false,
errors
};
}
return {
success: true,
data: this.transform(user)
};
}
}