
- ✅ Добавлено поле 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>
792 lines
24 KiB
Markdown
792 lines
24 KiB
Markdown
# 📖 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_ |