40227-vm/backend/docs/typescript-esm-migration-plan.md
Dmitri d4a5378adf Refactor: migrate frontend to Vite/React, add product backend modules
Frontend:
- Replace Next.js with Vite + React + TypeScript
- Add new component architecture (app-shell, sidebar, dashboard modules)
- Implement product modules: FRAME, safety protocols, walkthrough checkin,
  campus/staff attendance, personality quiz, sign language, classroom timer
- Add shadcn/ui component library with Tailwind CSS
- Remove legacy generated components, stores, and pages

Backend:
- Add product migrations: frame_entries, user_progress, safety_quiz_results,
  walkthrough_checkins, communication_events, personality_quiz_results,
  campus_attendance_config/summaries, staff_attendance_records, content_catalog
- Add corresponding models, services, and routes
- Implement cookie-based auth with refresh token rotation
- Add content catalog seeder with product content
- Migrate to ESLint flat config
- Switch from yarn to npm

Infrastructure:
- Update .gitignore for new tooling
- Add project documentation (CLAUDE.md, docs/)
- Remove deprecated config files and yarn.lock

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-06-09 15:18:23 +02:00

44 KiB
Raw Blame History

План миграции бэкэнда на TypeScript + ESM

Статус: черновик плана (реализация ещё не начата). Дата: 2026-06-09.

Этот документ описывает полную, поэтапную миграцию backend/ с CommonJS/JavaScript на TypeScript со строгим режимом и нативные ESM-модули (import/export). План составлен так, чтобы приложение оставалось рабочим на каждом шаге и соответствовало правилам CLAUDE.md (без any, без отключения линтера/TS, без приведения типов, импорты через алиас @, документация и тесты на каждый модуль, минимальные изменения, без оверинжиниринга).


0. Принятые решения (2026-06-09)

Зафиксированы по итогам обсуждения и проверочных spike:

  1. Дата/время: momentdayjs (минимальный diff, близкий API).
  2. sequelize-cli в ESM (spike проведён): подтверждено — CLI загружает .cjs-конфиг и .sequelizerc в пакете с "type": "module". Но т.к. выбраны TS-миграции (см. п.4), sequelize-cli из миграционного флоу убирается.
  3. Инструменты: dev — раннер tsx (исполняет .ts+ESM напрямую, это инструмент, не React-разметка), прод — сборка tsc + tsc-alias. Бандлер не вводим.
  4. Миграции — на TypeScript. Используем Umzug 3 + tsx (spike подтвердил выполнение .ts-миграций end-to-end). Хранилище — SequelizeStorage поверх существующей таблицы SequelizeMeta, поэтому история уже применённых миграций сохраняется. Лучшие практики — см. раздел 11.
  5. Версия Node: 24 (Active LTS). Удовлетворяет самым строгим ограничениям зависимостей (vitest, eslint требуют >=24). Node 26 — ещё «Current», не LTS, в прод не берём.
  6. Тесты: минимальный смоук-набор на этапе миграции (+ unit на чистые модули).
  7. OAuth-стратегии passport: обновление — отдельной задачей после миграции; задача внесена в docs/full-integration-refactor-plan.md.

ORM остаётся Sequelize 6.37 (решение зафиксировано отдельно: Prisma — слишком большой рефакторинг, v7 — alpha).


1. Текущее состояние (факты, на которых строится план)

Замеры по backend/src (без node_modules):

  • Всего файлов .js: 206, суммарно ~55 651 строк.
  • Распределение по слоям:
    • db/ — 90 файлов (модели 39, db/api 28, миграции 15, сидеры 5), ~33 400 строк — самый объёмный слой.
    • routes/ — 45 файлов, ~12 700 строк.
    • services/ — 48 файлов, ~7 600 строк.
    • constants/ — 13 файлов, middlewares/ — 3, auth/ — 2, config/ — 1 + config.js, ai/ — 1.
  • Модульная система: 100% CommonJS. require( используется в 173 файлах, module.exports — в 201; ESM-импортов (import …) — 0.
  • Экспорт-паттерны: module.exports = class — 74 файла (services, db/api, middlewares, helpers); module.exports = function(sequelize, DataTypes) — 38 (фабрики моделей Sequelize).
  • ORM: Sequelize 6.37, миграции/сидеры через sequelize-cli 6.6.5 (CommonJS-инструмент, читает .sequelizercsrc/db/db.config.js).
  • Express 5.2, аутентификация на passport (JWT + Google + Microsoft), документация через swagger-jsdoc (парсит ./src/routes/*.js).
  • Dev-запуск: watcher.js (chokidar + nodemon), он же триггерит db:migrate / db:seed при появлении новых файлов.
  • ESLint flat-config с sourceType: 'commonjs' и правилом import-x/no-unresolved.
  • Тестов в бэкэнде сейчас нет (0 файлов *.test.js / *.spec.js); проверка — только node -c (синтаксис) согласно docs/full-integration-refactor-plan.md.
  • Алиас @ в бэкэнде пока не используется (хотя CLAUDE.md его требует); все импорты относительные.
  • Окружения: engines.node >= 18, Dockerfile на node:20.15.1-alpine, локально доступен Node 22.

Важный ориентир: фронтенд уже на TypeScript + ESM + Vite (strict, paths: { "@/*": ["./src/*"] }, vitest, playwright). Бэкэнд логично привести к тем же конвенциям (strict TS, алиас @, vitest).

ESM-блокеры, выявленные в коде

  1. Динамическая загрузка моделей в db/models/index.js: fs.readdirSync(__dirname) + require(path.join(...)) в цикле. В нативном ESM require недоступен; нужен либо явный список import, либо динамический import().
  2. __dirname / __filename в 7 файлах: config/load-env.js, index.js, ai/LocalAIApi.js, db/models/index.js, services/email/list/{passwordReset,addressVerification,invitation}.js. В ESM их нет — замена на fileURLToPath(import.meta.url).
  3. sequelize-cli загружает .sequelizerc, db.config.js, миграции и сидеры как CommonJS через require. При "type": "module" файлы .js станут ESM и CLI сломается.
  4. swagger-jsdoc указывает на ./src/routes/*.js — после сборки путь к исходникам/выходу изменится.
  5. watcher.js + nodemon заточены под запуск .js-файла напрямую.
  6. В ESM (NodeNext) относительные импорты требуют явного расширения (./foo.js), а алиасы tsconfig не резолвятся Node в рантайме без дополнительного инструмента.

2. Целевое состояние

  • Весь рантайм-код бэкэнда — .ts со строгим TypeScript (strict: true, noImplicitAny, strictNullChecks), без any и без приведения типов.
  • Нативный ESM: "type": "module" в package.json, import/export во всём рантайм-коде.
  • Импорты через алиас @ (как на фронтенде), работающие и в dev, и в прод-сборке.
  • Сборка tscdist/, запуск прод из dist/; dev — через tsx watch (TS + ESM + алиасы без отдельной сборки).
  • Миграции и сидеры остаются на sequelize-cli и сохраняют CommonJS (расширение .cjs) — как исторические артефакты, переписывать их на TS нецелесообразно.
  • Введён минимальный каркас типизированных тестов (vitest) и typecheck как гейт качества.
  • Обновлены Dockerfile, скрипты package.json, ESLint и документация.

3. Ключевые проектные решения (с обоснованием)

3.1. Две фазы вместо «большого взрыва»

Задача совмещает две независимые трансформации: смену языка (JS → TS) и смену модульной системы (CJS → ESM). Делать обе сразу на 206 файлах рискованно. Рекомендуется разделить:

  • Фаза A — типизация (остаёмся на CommonJS). TypeScript компилируется в module: "CommonJS". Включаем allowJs: true, чтобы .ts и .js сосуществовали, и мигрируем файлы снизу вверх по дереву зависимостей. Приложение всё это время запускается как раньше. Риск минимальный.
  • Фаза B — перевод на ESM. Когда весь код уже на TS, механически переводим модульную систему: "type": "module", module: "NodeNext", requireimport, module.exportsexport, __dirnameimport.meta.url, переписываем динамический загрузчик моделей, чиним sequelize-cli и swagger-пути.

Такое разделение изолирует «языковые» ошибки от «модульных» и даёт чёткие точки отката.

Альтернатива (не рекомендуется): одновременный переход на tsx + "type": "module" и правка всех импортов сразу. Быстрее по числу шагов, но даёт большой нестабильный diff и трудный откат. Противоречит принципу «минимальные изменения».

3.2. Инструмент сборки и запуска

  • Сборка: tsc (официальный компилятор) в dist/. Просто, предсказуемо, без бандлера — соответствует «без оверинжиниринга».
  • Алиасы @ в выходной сборке: tsc не переписывает @/* в относительные пути. Добавляем tsc-alias как пост-шаг сборки. (Альтернатива — нативные imports в package.json с префиксом #, но CLAUDE.md требует именно @.)
  • Dev-запуск: tsx watch src/index.ts — исполняет TS+ESM напрямую и резолвит paths из tsconfig, заменяя nodemon. Авто-миграции/сидинг из watcher.js выносим в отдельный predev-шаг или оставляем лёгкий watcher только для db/migrations+db/seeders (см. 4.7).

3.3. Миграции — переход на Umzug + TypeScript (решение принято)

sequelize-cli — CommonJS-инструмент и не поддерживает .ts-миграции. Поскольку выбраны TS-миграции (решение 0.4), отказываемся от sequelize-cli в пользу Umzug 3 — это та же библиотека, что лежит под капотом sequelize-cli, поэтому переход совместим по хранилищу истории.

Подход (детали и пример каркаса — в разделе 11):

  • Собственный лёгкий раннер db/migrate.ts на Umzug, запускаемый через tsx.
  • Хранилище истории — SequelizeStorage поверх существующей таблицы SequelizeMeta. Уже применённые миграции остаются записанными, повторно не выполняются.
  • Новые миграции пишутся как .ts (ESM, типизированный QueryInterface).
  • Существующие 15 миграций не переписываем: оставляем как есть в формате .cjs (исторические артефакты), Umzug-glob включает и .cjs, и .ts, tsx грузит оба. Это исключает риск нарушить уже применённую историю БД.
  • Сидеры — вторым экземпляром Umzug (отдельная meta-таблица) либо оставляем минимальный текущий механизм; решается в разделе 11.

Spike подтвердил: Umzug 3.8.3 + tsx выполняют .ts-миграции end-to-end. Отдельно подтверждено, что .cjs-артефакты корректно грузятся в ESM-пакете, что нужно для сосуществования старых .cjs и новых .ts миграций.

Модели переводим на TS/ESM (их импортирует рантайм). Команды миграций моделей не требуют.

3.4. Загрузчик моделей и типобезопасный объект db

Заменяем fs.readdirSync + require на явные import всех 39 моделей в db/models/index.ts и сборку строго типизированного объекта db. Это убирает динамику (несовместимую с ESM-статикой), даёт автодополнение и исключает any. Минус — вербозный index с 39 импортами, но это разовый предсказуемый код. Динамический import() здесь не используем — только статические.

3.4.1. Политика импортов: статика по умолчанию

Решение: избегаем динамических импортов (import()), кроме случаев, где это действительно оправдано. На текущем коде таких случаев нет:

  • Единственный реальный динамический require — загрузчик моделей (db/models/index.js:25) — переводится на статические импорты (3.4).
  • Найденные import("…") — это JSDoc-аннотации типов, а не рантайм; в TS становятся обычными import type.
  • __dirname/__filename в 7 файлах используются для fs-чтения файлов (.env, HTML-шаблоны писем), не для импортов. Замена на path.dirname(fileURLToPath(import.meta.url)) сохраняет их статическими — это не динамический импорт.

Если в будущем потребуется ленивая загрузка (тяжёлый опциональный модуль, разрыв цикла зависимостей), динамический import() допускается точечно и с обоснованием в коде, а не как паттерн. Циклы зависимостей предпочтительно разрывать рефакторингом, а не import().

3.5. Стратегия типизации моделей Sequelize

Модели сейчас — фабрики sequelize.define('name', {...}). Чтобы не переписывать 39 моделей в class-based стиль (большой рискованный diff), рекомендуется остаться на define(), но добавить типобезопасность:

  • На каждую модель — интерфейс атрибутов (XAttributes) и creation-атрибутов (XCreationAttributes).
  • Фабрика возвращает ModelStatic<Model<XAttributes, XCreationAttributes>>.
  • associate типизируется через общий тип реестра Db.

Это самый объёмный по трудозатратам пункт миграции (39 моделей + связи), поэтому он вынесен в отдельную подфазу и может идти параллельно остальному после готовности каркаса.

3.6. Типизация Express и слоёв

  • @types/express, Request/Response/NextFunction, расширение Request полем currentUser через declaration merging (src/types/express.d.ts).
  • helpers.wrapAsync, commonErrorHandler, middlewares — строго типизированные сигнатуры.
  • db/api и services — публичные методы получают типы входных DTO и возвращаемых моделей.

4. Поэтапный план работ

Фаза 0 — Подготовка инструментов (без изменения рантайм-поведения)

  1. Установить dev-зависимости: typescript, tsx, tsc-alias, @types/node, и типы для библиотек без собственных деклараций: @types/express, @types/cors, @types/passport, @types/passport-jwt, @types/jsonwebtoken, @types/nodemailer, @types/multer, @types/swagger-jsdoc, @types/swagger-ui-express, @types/bcrypt, @types/validator. (axios, sequelize, pg, dayjs, @json2csv/plainjs, umzug поставляют типы сами; @types/lodash не нужен — lodash удаляется.) Добавить рантайм-зависимость umzug (для TS-миграций, см. раздел 11).
  2. Создать backend/tsconfig.json (Фаза A — CommonJS):
    • strict: true, noImplicitAny, strictNullChecks, noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch (зеркало фронтенда).
    • target: ES2022, module: CommonJS, moduleResolution: Node, esModuleInterop: true.
    • allowJs: true, checkJs: false — для сосуществования .js и .ts.
    • outDir: dist, rootDir: src, baseUrl: src, paths: { "@/*": ["*"] }.
  3. Обновить ESLint: добавить typescript-eslint, парсер для .ts, сохранить import-x/no-unresolved с TS-резолвером. На Фазе A не ломать существующие .js-правила.
  4. Добавить скрипт typecheck: tsc --noEmit и завести его как обязательный гейт.

Критерий готовности фазы: npm run typecheck проходит на смешанной кодовой базе, приложение запускается как раньше.

Фаза 0.5 — Аудит и обновление зависимостей (совмещается с Фазой A)

Полный разбор — в разделе 10. Кратко по порядку:

  1. Удалить неиспользуемые драйверы и пакеты: mysql2, tedious, sqlite (диалект везде postgres, 0 использований), lodash (один импорт lodash/get, заменяется на optional chaining).
  2. Заменить deprecated json2csv@5.0.7@json2csv/plainjs@7 (26 файлов).
  3. Заменить устаревшую momentdayjs (26 файлов).
  4. Консолидировать загрузку файлов: убрать formidable (1 файл), оставить multer; убрать body-parserexpress.json() (Express 5).
  5. Убрать sequelize-cli (миграции переходят на Umzug + tsx, см. раздел 11). OAuth-стратегии — отдельной задачей (см. full-integration-refactor-plan.md); sequelize-json-schema пересмотреть при типизации routes/openai.ts.
  6. Минорно поднять актуальные пакеты (@google-cloud/storage 7.19→7.21 и т.п.).

Эти изменения лучше делать вместе с типизацией соответствующих модулей, чтобы новые типы сразу ложились на новые API. Каждое — отдельным PR со смоук-проверкой.

Фаза A — Перевод на TypeScript (язык), модульная система = CommonJS

Мигрируем снизу вверх по зависимостям, по одному связному блоку за раз; после каждого блока — typecheck + ручной/смоук-прогон.

  1. constants/ (13 файлов) — листовые, без зависимостей. Перевести в .ts, экспортировать типизированные константы (as const).
  2. config/load-env.ts, config.ts — типизировать чтение env (хелперы requiredEnv/readBooleanEnv/readNumberEnv/readListEnv уже есть, добавить типы и тип Config).
  3. helpers.ts, db/utils.ts — общие утилиты.
  4. db/models/* — типизация моделей (см. 3.5) + типобезопасный db/models/index.ts (см. 3.4). Самый крупный блок; можно дробить по доменам.
  5. db/api/* (28 файлов) — типизировать публичные статические методы, опираясь на типы моделей.
  6. services/* (48 файлов), включая services/notifications/errors/*, services/email/list/*.
  7. auth/* и middlewares/* — типизация passport-стратегий, cookies, проверки прав; declaration merging для Request.currentUser.
  8. ai/LocalAIApi.ts.
  9. routes/* (45 файлов) — типизированные роутеры; swagger-JSDoc-комментарии переносятся как есть (swagger-jsdoc парсит и .ts).
  10. index.ts — точка входа.
  11. Существующие миграции/сидеры/db.config/.sequelizerc — на Фазе A не трогаем (остаются .js, CommonJS-пакет их корректно исполняет). Перевод на Umzug — в Фазе B.

Критерий готовности: 0 .js в рантайм-коде (кроме исторических миграций/сидеров), typecheck зелёный, приложение работает.

Фаза B — Перевод на ESM (модульная система)

  1. Переключить tsconfig на ESM: module: NodeNext, moduleResolution: NodeNext. Отключить allowJs (язык уже весь TS).
  2. Поставить "type": "module" в package.json.
  3. Конвертировать синтаксис во всех .ts: requireimport, module.exportsexport / export default. (Можно ускорить кодмодом cjstoesm — но результат вычитывать вручную; правило: не доверять слепо.)
  4. __dirname/__filenamepath.dirname(fileURLToPath(import.meta.url)) в 7 файлах.
  5. Переписать db/models/index.ts на статические import всех моделей.
  6. Поправить относительные импорты под NodeNext (явные расширения в выходе обеспечивает tsc-alias; алиас @ — предпочтительный путь).
  7. Миграции на Umzug + tsx (раздел 11): убрать sequelize-cli и .sequelizerc; перевести db.config.jsdb/config.ts; добавить раннер db/migrate.ts (Umzug + SequelizeStorage на SequelizeMeta); существующие 15 миграций оставить как .cjs, новые писать на .ts. Проверить migrate up на чистой БД и на БД с уже применённой историей.
  8. swagger-jsdoc: сделать путь apis env-зависимым — dev: ./src/routes/*.ts, прод: ./dist/routes/*.js.
  9. dev-запуск: заменить nodemon/watcher.js на tsx watch src/index.ts (hot-reload + стриминг логов в реальном времени). Авто-применение миграций — predev-шаг (tsx src/db/migrate.ts up); если нужно сохранить применение «на лету» при добавлении файла, оставить лёгкий chokidar-вотчер на db/migrations/db/seeders. Паритет с текущим поведением проверяется в Фазе D, п.4.
  10. db/reset.js (использует execSync('sequelize ...')) — переписать на вызов Umzug-раннера (migrate down/up или sequelize.sync для dev), перевести в .ts/ESM.

Критерий готовности: npm run build (tsc + tsc-alias) собирает dist/, node dist/index.js стартует, миграции/сидеры проходят, все маршруты отвечают.

Фаза C — Скрипты, сборка, контейнеризация

  1. package.json скрипты:
    • dev: tsx watch src/index.ts
    • build: tsc && tsc-alias
    • start: node dist/index.js (прод; миграции — отдельной командой перед стартом)
    • db:migrate: tsx src/db/migrate.ts up, db:rollback: tsx src/db/migrate.ts down, db:seed: tsx src/db/seed.ts (раздел 11)
    • typecheck, lint, test.
    • Указать engines.node: ">=24" (Active LTS, решение 0.5).
  2. Обновить корневой package.json (build:production, start:production) под новый build-флоу бэкэнда.
  3. Копирование ассетов в dist/. tsc собирает только .ts. Не-TS ассеты, которые читаются через fs по пути от import.meta.url, нужно копировать в dist/ отдельным build-шагом: HTML-шаблоны писем src/services/email/htmlTemplates/** (используются в services/email/list/*). Добавить в build (например, cpy/copyfiles/rsync) и проверить, что пути от import.meta.url резолвятся в dist/.
  4. Dockerfile: базовый образ node:24-alpine (текущая Active LTS); добавить шаг npm run build, в прод-образ копировать dist/ (включая скопированные шаблоны) + node_modules (multi-stage build). Заменить yarn install на npm ci (консистентно с корневым build:production).
  5. .dockerignore/.gitignore: добавить dist/.

Фаза D — Тесты и верификация

  1. Добавить vitest (консистентно с фронтендом) и базовый каркас тестов.
  2. Покрыть unit-тестами критичные чистые модули: config (парсинг env), helpers, db/utils, auth-хелперы, проверки прав.
  3. Смоук-проверка рантайма: старт сервера, /api-docs, healthcheck, 12 защищённых маршрута.
  4. Проверка паритета dev-workflow (nodemon/watcher). Убедиться, что после перехода на tsx watch опыт разработки не хуже текущего watcher.js + nodemon:
    • Hot-reload: правка любого .ts в src/** вызывает автоматический перезапуск сервера; в консоли видны сообщения о рестарте (аналог nodemon restarted due changes).
    • Логи в реальном времени: console.*/логи приложения стримятся в stdout сразу, без буферизации; проверить, что вывод не теряется при рестарте (tsx watch не глушит stdout дочернего процесса).
    • Миграции в dev: новые миграции применяются ожидаемым образом. Текущий watcher.js авто-применял новые файлы в db/migrations/db/seeders «на лету» через chokidar; план заменяет это на predev-шаг db:migrate. Подтвердить, что выбранный сценарий (ручной npm run db:migrate при добавлении миграции или сохранённый отдельный chokidar-вотчер) задокументирован и работает. Если авто-применение «на лету» нужно — оставить лёгкий watcher, запускающий tsx src/db/migrate.ts up на событие add.
    • Завершение процесса: Ctrl+C корректно останавливает tsx watch и дочерний сервер (нет «зависших» процессов на порту).
  5. Обновить docs/full-integration-refactor-plan.md (раздел Backend baseline): добавить typecheck, build, тесты.
  6. Финальный гейт: typecheck + lint + vitest + build + ручной прогон миграций на чистой БД + подтверждённый паритет dev-workflow (п.4).

5. Типизация: где основной объём

Слой Файлов Сложность типизации
db/models 39 Высокая — атрибуты, creation-атрибуты, связи
db/api 28 Средняя — DTO входа/выхода, опции транзакций
routes 45 Средняя — Request/Response, currentUser, обёртки
services 48 Средняя — бизнес-DTO, ошибки
constants 13 Низкая
прочее (auth, middlewares, config, ai, helpers) ~10 Низкая–средняя

Основной риск и трудозатраты сосредоточены в db/ (модели + api). Рекомендуется выделить типизацию моделей в отдельный поток работ и при необходимости распараллелить по доменам (люди/учёба/посещаемость/финансы/контент).


6. Риски и меры

  • Переход миграций на Umzug — главный риск. Меры: использовать SequelizeStorage с той же таблицей SequelizeMeta, чтобы история уже применённых миграций сохранилась; обязательно прогнать раннер и на чистой БД, и на БД с существующей историей; новые миграции — .ts, старые — оставить .cjs без переписывания. (Базовый механизм подтверждён spike: Umzug 3.8.3 + tsx выполняют .ts-миграции.)
  • Разрастание any под давлением сроков — запрещено CLAUDE.md. Мера: typecheck как блокирующий гейт; временно сложные места изолировать узкими типами, а не any.
  • Расхождение dev (tsx) и прод (dist) — алиасы/расширения резолвятся по-разному. Мера: всегда проверять и npm run dev, и node dist/index.js в CI.
  • Связи Sequelize теряют типы при define()-подходе. Мера: общий тип реестра Db + строго типизированный associate.
  • swagger пути ломаются после сборки. Мера: env-зависимый apis-glob, проверка /api-docs в смоуке.
  • Большой diff усложняет ревью. Мера: PR на каждый связный блок (по разделам Фазы A), а не одним коммитом.

7. Стратегия отката

  • Фаза A полностью обратима: TS компилируется в тот же CommonJS; при проблеме можно остановиться на любом блоке — смешанная кодовая база рабочая.
  • Фаза B — отдельная ветка/серия PR; точка возврата — последний зелёный коммит Фазы A.
  • Содержимое уже применённых миграций не меняется; Umzug читает ту же SequelizeMeta, поэтому история БД не затрагивается. Откат миграционного слоя = вернуть прежний db/reset.js/CLI-вызовы.

8. Порядок исполнения (сводка)

Фаза 0 (инструменты) → Фаза A (TS, снизу вверх: constants → config → helpers/utils → models → db/api → services → auth/middlewares → ai → routes → index) → Фаза B (ESM: tsconfig NodeNext, type:module, синтаксис import/export, import.meta.url, загрузчик моделей, Umzug-миграции, swagger, tsx) → Фаза C (скрипты/Docker/Node 24) → Фаза D (тесты/верификация/доки).

Оценка объёма намеренно не дана в часах: основной труд — типизация db/ (~61 файл) и routes (45 файлов). После готовности каркаса (Фаза 0 + загрузчик моделей) остальное — предсказуемая механическая работа поблочно.


9. Открытые вопросы

Все вопросы по состоянию на 2026-06-09 закрыты — см. раздел 0 «Принятые решения». Остаётся один пункт к проверке в коде по ходу работ: подтвердить замену sequelize-json-schema при типизации routes/openai.ts (раздел 10.3).


10. Аудит зависимостей: обновление и замена

Версии проверены по npm-реестру на 2026-06-09 (не из памяти модели). Источник истины — реестр; несколько пакетов в package.json (например, lodash, nodemailer, cors, eslint) уже соответствуют последним стабильным релизам в текущем реестре, менять их не нужно.

10.1. Удалить (не используются в коде)

Пакет Версия Причина
mysql2 3.22.5 Диалект БД везде postgres, 0 использований
tedious 19.2.1 MSSQL-драйвер, 0 использований
sqlite 5.1.1 0 использований
lodash 4.18.1 Единственный импорт lodash/get в services/notifications/helpers.js — заменяется на optional chaining (?.)

Эффект: меньше поверхности атаки, легче и быстрее установка/сборка. Оставляем только драйвер pg + pg-hstore.

10.2. Заменить — deprecated / устаревшие

Текущий Статус Рекомендуемая замена Масштаб
json2csv 5.0.7 deprecated («Package no longer supported») @json2csv/plainjs 7.0.6 (официальный преемник, scoped-пакеты) 26 файлов
moment 2.30.1 Maintenance mode, проект сам рекомендует альтернативы dayjs 1.11.21 (решено: минимальный diff, близкий API) 26 файлов
body-parserindex.js) Избыточен в Express 5 Встроенный express.json() 1 файл
formidable 3.5.4 Дублирует multer Консолидировать на multer 2.1.1 (популярнее, активнее), переписать services/file.js 1 файл
sequelize-cli 6.6.5 (devDep) Не поддерживает .ts-миграции Umzug 3.8.3 + tsx (раздел 11) весь миграционный флоу

Выбор dayjs зафиксирован (решение 0.1).

10.3. Заменить — опционально (низкая поддержка)

Текущий Последняя публикация Вариант Комментарий
passport-google-oauth2 0.2.0 2022 passport-google-oauth20 2.0.0 или openid-client 6.8.4 Низкая активность; passport-google-oauth20 — почти drop-in, openid-client — стратегически современнее, но больше работы
passport-microsoft 2.1.0 2024 оставить либо openid-client 6.8.4 Поддержка приемлема; единый openid-client под оба провайдера — отдельная задача
sequelize-json-schema 2.1.1 2022 zod-to-json-schema 3.25.2 или ручная схема Используется в 1 файле (routes/openai.js); пересмотреть при типизации этого роута

Решено (0.7): OAuth-замена выносится в отдельную задачу после основной миграции (внесена в docs/full-integration-refactor-plan.md), чтобы не смешивать риски (изменение потока аутентификации ≠ смена языка/модулей).

10.4. Обновить версии (минор/патч, без замены)

@google-cloud/storage 7.19 → 7.21. Остальные ключевые рантайм-пакеты уже на актуальных стабильных: express 5.2.1, helmet 8.2.0, axios 1.17.0, bcrypt 6.0.0, jsonwebtoken 9.0.3, pg 8.21.0, pg-hstore 2.3.4, cors 2.8.6, passport 0.7.0, passport-jwt 4.0.1, csv-parser 3.2.1, swagger-jsdoc 6.3.0, swagger-ui-express 5.0.1, chokidar 5.0.0, nodemailer 8.0.10, multer 2.1.1.

Перед общим обновлением выполнить npm outdated и npm audit, фиксировать точные версии в lock-файле.

10.5. Sequelize — остаёмся на 6 (важно)

sequelize стабильный — 6.37.8; версия 7 существует только как alpha (7.0.0-alpha.9). Пользователь просил стабильные версии, поэтому переход на Sequelize 7 (TS-native) сейчас не делаем. ORM как таковой не меняем: переписывание моделей под другой ORM (Drizzle/Prisma) противоречит принципу «минимальные изменения» и выходит за рамки миграции на TS/ESM. Это возможная отдельная инициатива в будущем, но не часть данного плана.

10.6. Новые зависимости для TS/ESM (актуальные версии)

  • Рантайм: umzug 3.8.3 (миграции, раздел 11).
  • Dev: typescript 6.0.3, tsx 4.22.4, tsc-alias 1.8.17, vitest 4.1.8, typescript-eslint 8.61.0, @types/node 25.9.2, @types/express 5.0.6, @types/multer 2.1.0, и типы для остальных библиотек без собственных деклараций (см. Фаза 0, п. 1).
  • Не добавлять: @types/lodash (lodash удаляется); @types/passport-google-oauth20 (OAuth-замена отложена).

Целевая среда исполнения — Node 24 (Active LTS): удовлетворяет самым строгим engines.node зависимостей (vitest и eslint требуют >=24).

10.7. Порядок применения

  1. Сначала удаления (10.1) — самый безопасный шаг, не меняет поведение.
  2. Затем замены (10.2) — модуль за модулем, вместе с типизацией соответствующих файлов в Фазе A.
  3. Опциональные замены (10.3) — после стабилизации основной миграции, отдельными задачами.
  4. Минорные апдейты (10.4) — единым PR с npm audit перед финальной верификацией (Фаза D).

11. Лучшие практики: TS-миграции на Umzug

Umzug — библиотека-движок, на которой построен и сам sequelize-cli, поэтому переход совместим по хранилищу истории. Подход (подтверждён spike: Umzug 3.8.3 + tsx исполняют .ts-миграции):

  1. Единое хранилище истории. Использовать SequelizeStorage с той же таблицей SequelizeMeta, что вёл sequelize-cli. Уже применённые миграции остаются зарегистрированными и повторно не запускаются — история БД не теряется.

  2. Сосуществование старого и нового. Glob включает и .cjs (15 существующих миграций — не переписываем), и .ts (все новые). tsx грузит оба формата.

  3. Типизированные миграции. Каждая новая миграция — .ts с типизированным контекстом QueryInterface:

    // src/db/migrations/2026XXXX-create-foo.ts
    import type { QueryInterface } from 'sequelize';
    import { DataTypes } from 'sequelize';
    
    export async function up({ context: qi }: { context: QueryInterface }) {
      await qi.createTable('foo', {
        id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
      });
    }
    export async function down({ context: qi }: { context: QueryInterface }) {
      await qi.dropTable('foo');
    }
    
  4. Раннер с CLI. Один файл src/db/migrate.ts создаёт Umzug и пробрасывает CLI через umzug.runAsCLI() (даёт команды up/down/pending/executed):

    // src/db/migrate.ts
    import { Umzug, SequelizeStorage } from 'umzug';
    import { sequelize } from '@/db/models'; // существующий инстанс Sequelize
    
    export const migrator = new Umzug({
      migrations: { glob: 'src/db/migrations/*.{cjs,ts}' },
      context: sequelize.getQueryInterface(),
      storage: new SequelizeStorage({ sequelize }), // таблица SequelizeMeta
      logger: console,
    });
    
    if (import.meta.url === `file://${process.argv[1]}`) {
      migrator.runAsCLI();
    }
    
  5. Скрипты: db:migrate = tsx src/db/migrate.ts up, db:rollback = ... down, db:status = ... pending. В Dockerfile/прод миграции запускать перед стартом (tsx ставить в рантайм-зависимости либо запускать из собранного dist).

  6. Сидеры. Второй экземпляр Umzug со своей meta-таблицей (SequelizeMeta_seeders) и аналогичным раннером src/db/seed.ts; существующие 5 сидеров перенести как .cjs (не переписывать) или по необходимости — на .ts.

  7. Замена watcher.js. Авто-миграции в dev — через predev-шаг (tsx src/db/migrate.ts up), а не chokidar-вотчер; перезапуск сервера — tsx watch.

  8. Проверка перед мерджем. Прогнать раннер дважды: на чистой БД (все миграции применяются по порядку) и на БД с уже заполненной SequelizeMeta (применяются только новые).