Автоматизация миграции

При переносе приложений между версиями API, изменении схем данных, обновлении архитектуры или переходе между фреймворками возникает задача гарантировать корректность входящих данных. Ошибки валидации во время миграции способны привести к:

  • повреждению данных;
  • несовместимости форматов;
  • нарушению контрактов API;
  • падению сервисов;
  • потере обратной совместимости.

Библиотека Validator.js позволяет автоматизировать значительную часть подобных проверок и снизить количество ручной логики.


Автоматизация миграции данных

Типовая проблема миграции

Предположим, старая версия приложения использовала следующий формат пользователя:

{
  "name": "Alex",
  "mail": "alex@test.com",
  "age": "25"
}

Новая версия требует:

{
  "fullName": "Alex",
  "email": "alex@test.com",
  "age": 25
}

Во время миграции необходимо:

  • проверить наличие обязательных полей;
  • убедиться в корректности email;
  • преобразовать строки в числа;
  • отфильтровать некорректные записи;
  • зафиксировать ошибки.

Validator.js позволяет централизовать такие проверки.


Установка Validator.js

Установка через npm

npm install validator

Подключение

CommonJS

const validator = require('validator');

ES Modules

import validator from 'validator';

Валидация данных перед миграцией

Проверка email

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 содержит функции очистки данных, что особенно важно при автоматической миграции.


Очистка email

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);

Миграция REST API

Проверка входящих запросов

При обновлении API старые клиенты могут отправлять устаревшие форматы данных.


Пример middleware

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
        }
    );
}

Использование Validator.js с базами данных

Проверка перед записью

async function saveUser(user) {

    if (!validator.isEmail(user.email)) {
        throw new Error('Invalid email');
    }

    await db.users.insert(user);
}

Миграция CSV-файлов

Исходный CSV

name,email,age
Alex,alex@test.com,25
Ivan,wrong-email,abc

Проверка строк CSV

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;
        })
    );
}

Проверка URL во время миграции

Старые ссылки

const url = 'https://example.com';

Валидация

validator.isURL(url);

Проверка UUID

Во время миграции распределённых систем часто требуется проверка идентификаторов.

validator.isUUID(
    '550e8400-e29b-41d4-a716-446655440000'
);

Проверка JSON

Валидация строк JSON

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()
    };
}

Миграция конфигурационных файлов

Проверка ENV-переменных

function validateEnv(env) {

    return (
        validator.isPort(env.PORT) &&
        validator.isURL(env.API_URL)
    );
}

Интеграция с Express

Middleware миграционной совместимости

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
            }
        )
    );
}

Использование Validator.js вместе с ORM

Пример с Sequelize

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/

validators/userValidator.js

import validator from 'validator';

export function validateUser(user) {

    return {
        email: validator.isEmail(user.email),
        age: validator.isInt(
            user.age.toString()
        )
    };
}

transformers/userTransformer.js

export function transformUser(user) {

    return {
        fullName: user.name,
        email: user.mail,
        age: Number(user.age)
    };
}

Автоматическое восстановление данных

Исправление некорректных email

function repairEmail(email) {

    const normalized =
        validator.normalizeEmail(email);

    if (!normalized) {
        return null;
    }

    return normalized;
}

Стратегии миграции

Strict Mode

Некорректные данные полностью отклоняются.

if (!validator.isEmail(email)) {
    throw new Error('Invalid email');
}

Soft Mode

Некорректные данные помечаются.

if (!validator.isEmail(email)) {

    warnings.push({
        field: 'email',
        value: email
    });
}

Проверка доменных ограничений

Разрешённые домены

function validateCorporateEmail(email) {

    return validator.isEmail(email, {
        host_whitelist: ['company.com']
    });
}

Построение полноценного migration-service

Комплексный пример

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)
        };
    }
}