
- ✅ Добавлено поле 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>
24 KiB
24 KiB
📖 V2 MIGRATION PLAYBOOK: Руководство по безопасной миграции
Статус: ✅ ПРОВЕРЕНО НА ПРАКТИКЕ
Основано на: Успешной миграции раздела "Услуги" 03.09.2025
Применимо к: Любым разделам SFERA требующим V1→V2 миграции
🎯 ОБЗОР МЕТОДОЛОГИИ
ФИЛОСОФИЯ БЕЗОПАСНОЙ МИГРАЦИИ:
"Измерь дважды, отрежь один раз" - полный анализ перед любыми изменениями
КЛЮЧЕВЫЕ ПРИНЦИПЫ:
- Изоляция V2 от V1 - никаких пересечений во время разработки
- Поэтапность - маленькие шаги с проверкой после каждого
- Rollback готовность - возможность отката на любом этапе
- Сохранение функциональности - пользователь не должен заметить изменений
📋 УНИВЕРСАЛЬНЫЙ ЧЕКЛИСТ МИГРАЦИИ
ПОДГОТОВИТЕЛЬНЫЙ ЭТАП (КРИТИЧЕСКИ ВАЖЕН):
□ Проанализировать все компоненты использующие старую систему
□ Определить зависимости и связанные системы
□ Создать список файлов требующих обновления
□ Убедиться в понимании бизнес-логики домена
□ Проверить наличие тестов для критических функций
□ Создать 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 Найти все компоненты домена:
# Поиск компонентов использующих 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 Проанализировать структуру данных:
# Найти Prisma модели
rg "model.*Service|model.*Supply|model.*Logistics" prisma/schema.prisma
# Найти связанные типы
rg "Service.*{|Supply.*{|Logistics.*{" --type ts
1.3 Понять бизнес-логику:
// КРИТИЧЕСКИ ВАЖНО: Прочитать все резолверы V1
// Понять какие поля обязательные, какие расчеты происходят
// Найти все места где данные трансформируются
ШАГ 2: СОЗДАНИЕ V2 МОДЕЛЕЙ ДАННЫХ
2.1 Шаблон V2 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 Структура файла резолвера:
// 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 Подключение к основным резолверам:
// В /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:
# 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 файл:
// /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:
# Тест создания
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 Проверить сборку:
npm run build
ШАГ 7: МИГРАЦИЯ КОМПОНЕНТОВ
7.1 Алгоритм обновления компонента:
// БЫЛО (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 Проверить что все компоненты мигрированы:
# НЕ ДОЛЖНО БЫТЬ РЕЗУЛЬТАТОВ:
rg "GET_MY_OLD_ENTITIES" --type ts
rg "data\?\.myOldEntities" --type ts
rg "CREATE_OLD_ENTITY" --type ts
8.2 Отключить V1 резолверы:
// В /src/graphql/resolvers/index.ts
Query: (() => {
const {
myOldEntities: _myOldEntities, // ← ОТКЛЮЧИТЬ V1
// ... другие отключаемые
...filteredQuery
} = oldResolvers.Query || {}
return filteredQuery
})(),
8.3 Финальная проверка:
npm run build # Должно пройти без ошибок
npm run lint # Проверить warnings
🚨 КРИТИЧЕСКИЕ ПРАВИЛА МИГРАЦИИ
❌ НИКОГДА НЕ ДЕЛАТЬ:
- Изменять V1 и V2 одновременно - риск поломки обеих систем
- Мигрировать все компоненты разом - невозможность rollback
- Отключать V1 до полной миграции V2 - потеря функциональности
- Игнорировать ошибки сборки - скрытые проблемы в production
- Пропускать тестирование - риск багов в production
✅ ВСЕГДА ДЕЛАТЬ:
- Создавать V2 полностью независимо от V1
- Тестировать каждый этап отдельно
- Мигрировать компоненты поочередно
- Проверять npm run build после каждого изменения
- Отключать V1 только после полной проверки V2
🛡️ СТРАТЕГИЯ ROLLBACK
БЫСТРЫЙ ОТКАТ КОМПОНЕНТА:
// В компоненте: вернуть старые импорты
- 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 РЕЗОЛВЕРОВ:
// В index.ts: убрать из исключений
Query: (() => {
const {
// myOldEntities: _myOldEntities, // ← ЗАКОММЕНТИРОВАТЬ ОТКЛЮЧЕНИЕ
...filteredQuery
} = oldResolvers.Query || {}
return filteredQuery
})(),
ПОЛНЫЙ ОТКАТ ЧЕРЕЗ GIT:
# Откат конкретных файлов
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
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]"
📚 ПОЛЕЗНЫЕ КОМАНДЫ
ПОИСК И АНАЛИЗ:
# Найти все использования 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
ПРОВЕРКИ КАЧЕСТВА:
# Проверка типов
npx tsc --noEmit
# Проверка линтера
npm run lint
# Проверка сборки
npm run build
# Поиск TODO/FIXME от миграции
rg "TODO.*V2|FIXME.*V2" --type ts
ТЕСТИРОВАНИЕ:
# Unit тесты
npm test -- --grep "V2"
# E2E тесты конкретного домена
npm run e2e:test -- --spec "**/domain-entity-v2.cy.ts"
# Проверка покрытия
npm run test:coverage
🎓 ОБУЧАЮЩИЕ МАТЕРИАЛЫ
ДЛЯ НОВИЧКОВ В V2 МИГРАЦИИ:
- Прочитать V2_SERVICES_MIGRATION_REPORT.md - реальный пример
- Изучить V2_ARCHITECTURE_SERVICES.md - техническая документация
- Посмотреть код миграции Services как reference implementation
ДЛЯ ЭКСПЕРТОВ:
- Адаптировать шаблоны под специфику конкретного домена
- Расширить метрики успеха под требования проекта
- Создать автоматизацию для повторяющихся задач миграции
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