feat: реализовать полную автосинхронизацию V2 системы расходников с nameForSeller и анализ миграции
- ✅ Добавлено поле nameForSeller в FulfillmentConsumable для кастомизации названий - ✅ Добавлено поле inventoryId для связи между каталогом и складом - ✅ Реализована автосинхронизация FulfillmentConsumableInventory → FulfillmentConsumable - ✅ Обновлен UI с колонкой "Название для селлера" в /fulfillment/services/consumables - ✅ Исправлены GraphQL запросы (удалено поле description, добавлены новые поля) - ✅ Создан скрипт sync-inventory-to-catalog.ts для миграции существующих данных - ✅ Добавлена техническая документация архитектуры системы инвентаря - ✅ Создан отчет о статусе миграции V1→V2 с детальным планом 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
792
docs/development/V2_MIGRATION_PLAYBOOK.md
Normal file
792
docs/development/V2_MIGRATION_PLAYBOOK.md
Normal file
@ -0,0 +1,792 @@
|
||||
# 📖 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_
|
Reference in New Issue
Block a user