# 📖 V2 MIGRATION PLAYBOOK: Руководство по безопасной миграции > **Статус**: ✅ **ПРОВЕРЕНО НА ПРАКТИКЕ** > **Основано на**: Успешной миграции раздела "Услуги" 03.09.2025 > **Применимо к**: Любым разделам SFERA требующим V1→V2 миграции --- ## 🎯 ОБЗОР МЕТОДОЛОГИИ ### ФИЛОСОФИЯ БЕЗОПАСНОЙ МИГРАЦИИ: > **"Измерь дважды, отрежь один раз"** - полный анализ перед любыми изменениями ### КЛЮЧЕВЫЕ ПРИНЦИПЫ: 1. **Изоляция V2 от V1** - никаких пересечений во время разработки 2. **Поэтапность** - маленькие шаги с проверкой после каждого 3. **Rollback готовность** - возможность отката на любом этапе 4. **Сохранение функциональности** - пользователь не должен заметить изменений --- ## 📋 УНИВЕРСАЛЬНЫЙ ЧЕКЛИСТ МИГРАЦИИ ### ПОДГОТОВИТЕЛЬНЫЙ ЭТАП (КРИТИЧЕСКИ ВАЖЕН): ``` □ Проанализировать все компоненты использующие старую систему □ Определить зависимости и связанные системы □ Создать список файлов требующих обновления □ Убедиться в понимании бизнес-логики домена □ Проверить наличие тестов для критических функций □ Создать backup стратегию для критических данных ``` ### ЭТАП СОЗДАНИЯ V2 АРХИТЕКТУРЫ: ``` □ Создать новые Prisma модели с доменной изоляцией □ Реализовать полный набор V2 резолверов (Query + Mutation) □ Создать GraphQL типы и схемы □ Подключить резолверы к основному GraphQL API □ Протестировать V2 API независимо от V1 □ Проверить npm run build на отсутствие ошибок ``` ### ЭТАП МИГРАЦИИ КОМПОНЕНТОВ: ``` □ Обновить импорты на V2 запросы □ Изменить data references в компонентах □ Обновить мутации на V2 версии □ Исправить refetchQueries в формах □ Проверить Apollo Client кэш обновления □ Протестировать каждый обновленный компонент ``` ### ЭТАП ОТКЛЮЧЕНИЯ V1: ``` □ Убедиться что все компоненты используют V2 □ Отключить V1 резолверы в основном файле резолверов □ Удалить неиспользуемые V1 импорты □ Проверить отсутствие ошибок в консоли браузера □ Провести полное функциональное тестирование □ Подтвердить npm run build проходит без ошибок ``` --- ## 🔧 ПОШАГОВЫЙ АЛГОРИТМ МИГРАЦИИ ### ШАГ 1: ГЛУБОКИЙ АНАЛИЗ СУЩЕСТВУЮЩЕЙ СИСТЕМЫ #### 1.1 Найти все компоненты домена: ```bash # Поиск компонентов использующих V1 запросы rg "GET_MY_SERVICES|GET_MY_SUPPLIES|GET_MY_LOGISTICS" --type ts # Поиск direct data access rg "data\?\.myServices|data\?\.mySupplies|data\?\.myLogistics" --type ts # Поиск мутаций V1 rg "CREATE_SERVICE|UPDATE_SERVICE|DELETE_SERVICE" --type ts ``` #### 1.2 Проанализировать структуру данных: ```bash # Найти Prisma модели rg "model.*Service|model.*Supply|model.*Logistics" prisma/schema.prisma # Найти связанные типы rg "Service.*{|Supply.*{|Logistics.*{" --type ts ``` #### 1.3 Понять бизнес-логику: ```typescript // КРИТИЧЕСКИ ВАЖНО: Прочитать все резолверы V1 // Понять какие поля обязательные, какие расчеты происходят // Найти все места где данные трансформируются ``` ### ШАГ 2: СОЗДАНИЕ V2 МОДЕЛЕЙ ДАННЫХ #### 2.1 Шаблон V2 Prisma модели: ```prisma model DomainEntityV2 { id String @id @default(cuid()) organizationId String // ОБЯЗАТЕЛЬНО: доменная изоляция name String description String? price Decimal? @db.Decimal(10, 2) // Для денежных полей isActive Boolean @default(true) // Мягкое удаление createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Связи organization Organization @relation("OrganizationDomainV2", fields: [organizationId], references: [id]) // Индексы производительности @@index([organizationId, isActive]) @@map("domain_entity_v2") } ``` #### 2.2 Ключевые принципы моделирования: - **`organizationId`** - ОБЯЗАТЕЛЬНО для доменной изоляции - **`Decimal`** - для всех денежных полей (точность) - **`isActive`** - вместо физического удаления - **Индексы** - на часто используемые комбинации полей - **`@@map`** - явное имя таблицы с суффиксом `_v2` ### ШАГ 3: РЕАЛИЗАЦИЯ V2 РЕЗОЛВЕРОВ #### 3.1 Структура файла резолвера: ```typescript // domain-entity-v2.ts import { prisma } from '@/lib/prisma' import { Context } from '../context' // ===== QUERIES ===== export const domainEntityQueries = { myDomainEntities: async (_: unknown, __: unknown, context: Context) => { const { user } = context if (!user?.organization?.id) { throw new Error('Организация не найдена') } return await prisma.domainEntityV2.findMany({ where: { organizationId: user.organization.id, // ДОМЕННАЯ ИЗОЛЯЦИЯ isActive: true, }, include: { organization: true, }, orderBy: [ { name: 'asc' }, ], }) }, domainEntitiesByOrganization: async ( _: unknown, { organizationId }: { organizationId: string }, context: Context, ) => { // Публичные данные для других участников return await prisma.domainEntityV2.findMany({ where: { organizationId, isActive: true, }, include: { organization: true, }, }) }, } // ===== MUTATIONS ===== export const domainEntityMutations = { createDomainEntity: async ( _: unknown, { input }: { input: CreateDomainEntityInput }, context: Context, ) => { try { const { user } = context if (!user?.organization?.id) { return { success: false, message: 'Организация не найдена', entity: null, } } // ВАЛИДАЦИЯ if (!input.name?.trim()) { return { success: false, message: 'Название обязательно', entity: null, } } const entity = await prisma.domainEntityV2.create({ data: { ...input, organizationId: user.organization.id, // AUTO-ASSIGN }, include: { organization: true, }, }) return { success: true, message: 'Успешно создано', entity, } } catch (error) { console.error('Error creating domain entity:', error) return { success: false, message: 'Внутренняя ошибка сервера', entity: null, } } }, updateDomainEntity: async ( _: unknown, { input }: { input: UpdateDomainEntityInput }, context: Context, ) => { try { const { user } = context if (!user?.organization?.id) { return { success: false, message: 'Организация не найдена', entity: null } } // ПРОВЕРКА ПРИНАДЛЕЖНОСТИ const existingEntity = await prisma.domainEntityV2.findFirst({ where: { id: input.id, organizationId: user.organization.id, // SECURITY CHECK }, }) if (!existingEntity) { return { success: false, message: 'Объект не найден', entity: null } } const entity = await prisma.domainEntityV2.update({ where: { id: input.id }, data: { ...input, id: undefined, // Убираем id из данных для обновления }, include: { organization: true, }, }) return { success: true, message: 'Успешно обновлено', entity, } } catch (error) { console.error('Error updating domain entity:', error) return { success: false, message: 'Ошибка при обновлении', entity: null, } } }, deleteDomainEntity: async ( _: unknown, { id }: { id: string }, context: Context, ) => { try { const { user } = context if (!user?.organization?.id) { throw new Error('Организация не найдена') } // МЯГКОЕ УДАЛЕНИЕ const result = await prisma.domainEntityV2.updateMany({ where: { id, organizationId: user.organization.id, // SECURITY CHECK }, data: { isActive: false, }, }) return result.count > 0 } catch (error) { console.error('Error deleting domain entity:', error) return false } }, } ``` #### 3.2 Подключение к основным резолверам: ```typescript // В /src/graphql/resolvers/index.ts import { domainEntityQueries, domainEntityMutations } from './domain-entity-v2' const mergedResolvers = mergeResolvers( // ... existing resolvers // V2 DOMAIN ENTITY { Query: domainEntityQueries, Mutation: domainEntityMutations, }, ) ``` ### ШАГ 4: СОЗДАНИЕ GraphQL СХЕМЫ #### 4.1 Добавить в typedefs.ts: ```graphql # V2 DOMAIN ENTITY TYPES type DomainEntityV2 { id: ID! organizationId: String! name: String! description: String price: Float isActive: Boolean! createdAt: DateTime! updatedAt: DateTime! organization: Organization! } type DomainEntityResponse { success: Boolean! message: String! entity: DomainEntityV2 } # V2 DOMAIN ENTITY INPUTS input CreateDomainEntityInput { name: String! description: String price: Float } input UpdateDomainEntityInput { id: ID! name: String description: String price: Float isActive: Boolean } extend type Query { myDomainEntities: [DomainEntityV2!]! domainEntitiesByOrganization(organizationId: ID!): [DomainEntityV2!]! } extend type Mutation { createDomainEntity(input: CreateDomainEntityInput!): DomainEntityResponse! updateDomainEntity(input: UpdateDomainEntityInput!): DomainEntityResponse! deleteDomainEntity(id: ID!): Boolean! } ``` ### ШАГ 5: СОЗДАНИЕ GraphQL ЗАПРОСОВ #### 5.1 Создать queries файл: ```typescript // /src/graphql/queries/domain-entity-v2.ts import { gql } from '@apollo/client' export const GET_MY_DOMAIN_ENTITIES_V2 = gql` query GetMyDomainEntitiesV2 { myDomainEntities { id organizationId name description price isActive createdAt updatedAt organization { id name } } } ` export const GET_DOMAIN_ENTITIES_BY_ORGANIZATION = gql` query GetDomainEntitiesByOrganization($organizationId: ID!) { domainEntitiesByOrganization(organizationId: $organizationId) { id organizationId name description price isActive organization { id name } } } ` export const CREATE_DOMAIN_ENTITY = gql` mutation CreateDomainEntity($input: CreateDomainEntityInput!) { createDomainEntity(input: $input) { success message entity { id name description price organization { id name } } } } ` export const UPDATE_DOMAIN_ENTITY = gql` mutation UpdateDomainEntity($input: UpdateDomainEntityInput!) { updateDomainEntity(input: $input) { success message entity { id name description price isActive } } } ` export const DELETE_DOMAIN_ENTITY = gql` mutation DeleteDomainEntity($id: ID!) { deleteDomainEntity(id: $id) } ` ``` ### ШАГ 6: ТЕСТИРОВАНИЕ V2 API #### 6.1 Проверить через GraphQL Playground: ```graphql # Тест создания mutation { createDomainEntity(input: { name: "Test Entity" description: "Test Description" price: 100.50 }) { success message entity { id name price } } } # Тест получения данных query { myDomainEntities { id name price isActive } } ``` #### 6.2 Проверить сборку: ```bash npm run build ``` ### ШАГ 7: МИГРАЦИЯ КОМПОНЕНТОВ #### 7.1 Алгоритм обновления компонента: ```typescript // БЫЛО (V1): import { GET_MY_OLD_ENTITIES } from '@/graphql/queries' const { data } = useQuery(GET_MY_OLD_ENTITIES) const entities = data?.myOldEntities || [] const [createEntity] = useMutation(CREATE_OLD_ENTITY, { refetchQueries: [{ query: GET_MY_OLD_ENTITIES }] }) // СТАЛО (V2): import { GET_MY_DOMAIN_ENTITIES_V2, CREATE_DOMAIN_ENTITY } from '@/graphql/queries/domain-entity-v2' const { data } = useQuery(GET_MY_DOMAIN_ENTITIES_V2) const entities = data?.myDomainEntities || [] const [createEntity] = useMutation(CREATE_DOMAIN_ENTITY, { refetchQueries: [{ query: GET_MY_DOMAIN_ENTITIES_V2 }], update: (cache, { data }) => { if (data?.createDomainEntity?.success) { // ОБНОВИТЬ APOLLO CACHE const existingData = cache.readQuery({ query: GET_MY_DOMAIN_ENTITIES_V2 }) if (existingData) { cache.writeQuery({ query: GET_MY_DOMAIN_ENTITIES_V2, data: { myDomainEntities: [ ...existingData.myDomainEntities, data.createDomainEntity.entity ] } }) } } } }) ``` #### 7.2 Чеклист обновления компонента: ``` □ Заменить импорты V1→V2 запросов □ Обновить useQuery на V2 версию □ Изменить data references (data?.myOldEntities → data?.myDomainEntities) □ Обновить мутации на V2 версии □ Исправить refetchQueries arrays □ Добавить Apollo cache update логику □ Протестировать все CRUD операции □ Проверить отсутствие console errors ``` ### ШАГ 8: ОТКЛЮЧЕНИЕ V1 СИСТЕМЫ #### 8.1 Проверить что все компоненты мигрированы: ```bash # НЕ ДОЛЖНО БЫТЬ РЕЗУЛЬТАТОВ: rg "GET_MY_OLD_ENTITIES" --type ts rg "data\?\.myOldEntities" --type ts rg "CREATE_OLD_ENTITY" --type ts ``` #### 8.2 Отключить V1 резолверы: ```typescript // В /src/graphql/resolvers/index.ts Query: (() => { const { myOldEntities: _myOldEntities, // ← ОТКЛЮЧИТЬ V1 // ... другие отключаемые ...filteredQuery } = oldResolvers.Query || {} return filteredQuery })(), ``` #### 8.3 Финальная проверка: ```bash npm run build # Должно пройти без ошибок npm run lint # Проверить warnings ``` --- ## 🚨 КРИТИЧЕСКИЕ ПРАВИЛА МИГРАЦИИ ### ❌ НИКОГДА НЕ ДЕЛАТЬ: 1. **Изменять V1 и V2 одновременно** - риск поломки обеих систем 2. **Мигрировать все компоненты разом** - невозможность rollback 3. **Отключать V1 до полной миграции V2** - потеря функциональности 4. **Игнорировать ошибки сборки** - скрытые проблемы в production 5. **Пропускать тестирование** - риск багов в production ### ✅ ВСЕГДА ДЕЛАТЬ: 1. **Создавать V2 полностью независимо от V1** 2. **Тестировать каждый этап отдельно** 3. **Мигрировать компоненты поочередно** 4. **Проверять npm run build после каждого изменения** 5. **Отключать V1 только после полной проверки V2** --- ## 🛡️ СТРАТЕГИЯ ROLLBACK ### БЫСТРЫЙ ОТКАТ КОМПОНЕНТА: ```typescript // В компоненте: вернуть старые импорты - import { GET_MY_DOMAIN_ENTITIES_V2 } from '@/graphql/queries/domain-entity-v2' + import { GET_MY_OLD_ENTITIES } from '@/graphql/queries' - const entities = data?.myDomainEntities || [] + const entities = data?.myOldEntities || [] ``` ### ОТКАТ V1 РЕЗОЛВЕРОВ: ```typescript // В index.ts: убрать из исключений Query: (() => { const { // myOldEntities: _myOldEntities, // ← ЗАКОММЕНТИРОВАТЬ ОТКЛЮЧЕНИЕ ...filteredQuery } = oldResolvers.Query || {} return filteredQuery })(), ``` ### ПОЛНЫЙ ОТКАТ ЧЕРЕЗ GIT: ```bash # Откат конкретных файлов git checkout HEAD~1 -- src/components/domain/entity-component.tsx # Откат всей ветки git reset --hard HEAD~5 # Осторожно! ``` --- ## 📊 МЕТРИКИ УСПЕШНОЙ МИГРАЦИИ ### КОЛИЧЕСТВЕННЫЕ ПОКАЗАТЕЛИ: | Метрика | Цель | Способ измерения | |---------|------|------------------| | **Компоненты мигрированы** | 100% | `rg "V1_QUERIES" --type ts` должен быть пуст | | **Тесты проходят** | 100% | `npm test` без ошибок | | **Сборка успешна** | ✅ | `npm run build` без ошибок | | **ESLint warnings** | ≤ уровня до миграции | `npm run lint` | | **Console errors** | 0 | Браузерная консоль | ### КАЧЕСТВЕННЫЕ ПОКАЗАТЕЛИ: - ✅ Пользователи не заметили изменений в UI - ✅ Все функции работают как раньше - ✅ Производительность не ухудшилась - ✅ V1 система полностью отключена - ✅ V2 система масштабируема для новых функций --- ## 🎯 ШАБЛОН MIGRATION COMMIT ```bash git commit -m "feat: migrate [DOMAIN] from V1 to V2 ✅ COMPLETED: - Created [N] V2 Prisma models with domain isolation - Implemented full CRUD GraphQL V2 resolvers - Migrated [N] components from V1 to V2 queries - Connected V2 mutations to main resolvers - Disabled V1 resolvers: [list] 🏗️ ARCHITECTURE: - Domain isolation by organizationId - Specialized tables replace universal V1 models - Full TypeScript typing and validation - Optimized Apollo Client cache management 🧪 VERIFICATION: - npm run build: ✅ successful - All UI functions: ✅ working - V1 resolvers: ✅ disabled - User experience: ✅ unchanged 🔧 FILES CHANGED: - NEW: /src/graphql/resolvers/[domain]-v2.ts - NEW: /src/graphql/queries/[domain]-v2.ts - UPDATED: [N] component files V1→V2 - UPDATED: /src/graphql/resolvers/index.ts (V1 disabled) - UPDATED: /prisma/schema.prisma (V2 models) 📊 IMPACT: - [N] components fully migrated - [N] V1 resolvers safely disabled - 100% backward compatibility maintained - Ready for production deployment Co-authored-by: [Team members]" ``` --- ## 📚 ПОЛЕЗНЫЕ КОМАНДЫ ### ПОИСК И АНАЛИЗ: ```bash # Найти все использования V1 запросов rg "GET_MY_[A-Z_]+" --type ts | grep -v "_V2" # Найти компоненты с прямым доступом к V1 данным rg "data\?\.my[A-Z]" --type ts # Найти мутации V1 rg "(CREATE|UPDATE|DELETE)_[A-Z_]+" --type ts | grep -v "_V2" # Проверить что V1 отключен rg "myOldEntities:" src/graphql/resolvers/index.ts ``` ### ПРОВЕРКИ КАЧЕСТВА: ```bash # Проверка типов npx tsc --noEmit # Проверка линтера npm run lint # Проверка сборки npm run build # Поиск TODO/FIXME от миграции rg "TODO.*V2|FIXME.*V2" --type ts ``` ### ТЕСТИРОВАНИЕ: ```bash # Unit тесты npm test -- --grep "V2" # E2E тесты конкретного домена npm run e2e:test -- --spec "**/domain-entity-v2.cy.ts" # Проверка покрытия npm run test:coverage ``` --- ## 🎓 ОБУЧАЮЩИЕ МАТЕРИАЛЫ ### ДЛЯ НОВИЧКОВ В V2 МИГРАЦИИ: 1. Прочитать **V2_SERVICES_MIGRATION_REPORT.md** - реальный пример 2. Изучить **V2_ARCHITECTURE_SERVICES.md** - техническая документация 3. Посмотреть код миграции Services как reference implementation ### ДЛЯ ЭКСПЕРТОВ: 1. Адаптировать шаблоны под специфику конкретного домена 2. Расширить метрики успеха под требования проекта 3. Создать автоматизацию для повторяющихся задач миграции ### TROUBLESHOOTING GUIDE: - **"V2 мутации не работают"** → Проверить подключение к index.ts resolvers - **"Данные не отображаются"** → Проверить data references в компонентах - **"Apollo cache не обновляется"** → Добавить update функции в мутации - **"TypeScript ошибки"** → Проверить соответствие types в GraphQL схеме --- ## 🚀 ЗАКЛЮЧЕНИЕ Этот playbook основан на реальном опыте успешной миграции и содержит все критические знания для безопасного перехода любого домена SFERA с V1 на V2. **🎯 КЛЮЧЕВЫЕ TAKEAWAYS:** - **Безопасность превыше скорости** - лучше медленно, но без ошибок - **Тестирование на каждом этапе** - предотвращение проблем в production - **Документация изменений** - возможность rollback и понимания для команды - **Доменная изоляция V2** - фундамент масштабируемости архитектуры **🏆 РЕЗУЛЬТАТ ПРИМЕНЕНИЯ:** Используя этот playbook, любой разработчик SFERA сможет провести V1→V2 миграцию безопасно, эффективно и с полным пониманием процесса. --- _Создано: 03.09.2025_ _Основано на: Реальном опыте миграции раздела "Услуги"_ _Статус: Production Ready Playbook_ _Применимость: Универсальная для всех доменов SFERA_