config_refactoring

Рефакторинг конфига

Переход от монолитной таблицы config_versions к атомарным таблицам настроек.

Проблема

Текущая архитектура — одна таблица со всеми настройками:

config_versions (
    id, created_at, created_by,
    show_draft_versions,  -- bool
    default_layout,       -- string
    timezone,             -- string
    robots_txt            -- string
)

Недостатки:

  • Изменил одно поле → записалась копия всех полей
  • История смешана — непонятно что именно менялось
  • Добавление нового поля = ALTER TABLE
  • Фронт отправляет все поля даже если изменилось одно

Решение

Каждая настройка — отдельная таблица с историей:

config_site_title_templates (id, created_at, created_by, value text)
config_timezones           (id, created_at, created_by, value text)
config_default_layouts     (id, created_at, created_by, value text)
config_robots_txts         (id, created_at, created_by, value text)
config_show_draft_versions (id, created_at, created_by, value bool)

Преимущества:

  • Чистая история каждой настройки
  • Добавление настройки = CREATE TABLE (не ALTER)
  • Фронт меняет только то что изменилось
  • Легко откатить конкретную настройку

GraphQL API

Универсальные типы

# Для string настроек
type ConfigStringEntry {
  id: ID!
  value: String!
  createdAt: DateTime!
  createdBy: AdminUser!
}

type ConfigString {
  current: String!
  history: [ConfigStringEntry!]!
}

# Для bool настроек
type ConfigBoolEntry {
  id: ID!
  value: Boolean!
  createdAt: DateTime!
  createdBy: AdminUser!
}

type ConfigBool {
  current: Boolean!
  history: [ConfigBoolEntry!]!
}

# Payload для мутаций
union SetConfigStringPayload = SetConfigStringSuccess | ErrorPayload
type SetConfigStringSuccess { entry: ConfigStringEntry! }

union SetConfigBoolPayload = SetConfigBoolSuccess | ErrorPayload
type SetConfigBoolSuccess { entry: ConfigBoolEntry! }

Query

extend type AdminQuery {
  # Новые атомарные настройки
  configSiteTitleTemplate: ConfigString!
  configTimezone: ConfigString!
  configDefaultLayout: ConfigString!
  configRobotsTxt: ConfigString!
  configShowDraftVersions: ConfigBool!

  # Legacy (deprecated, удалить в Фазе 6)
  latestConfig: ConfigVersion! @deprecated(reason: "Use atomic config fields")
}

Mutations

extend type AdminMutation {
  # Новые атомарные мутации
  setConfigSiteTitleTemplate(value: String!): SetConfigStringPayload!
  setConfigTimezone(value: String!): SetConfigStringPayload!
  setConfigDefaultLayout(value: String!): SetConfigStringPayload!
  setConfigRobotsTxt(value: String!): SetConfigStringPayload!
  setConfigShowDraftVersions(value: Boolean!): SetConfigBoolPayload!

  # Legacy (deprecated, удалить в Фазе 6)
  createConfigVersion(input: CreateConfigVersionInput!): ... @deprecated
}

Frontend

Новая страница /admin/config:

┌─────────────────────────────────────────────────┐
│  Настройки сайта                                │
├─────────────────────────────────────────────────┤
│                                                 │
│  Site Title Template                            │
│  ┌─────────────────────────────────┐            │
│  │ %s | Мой сайт                   │  [История] │
│  └─────────────────────────────────┘            │
│  Формат заголовка страницы. %s = название.      │
│                                                 │
│  ─────────────────────────────────────────────  │
│                                                 │
│  Timezone                                       │
│  ┌─────────────────────────────────┐            │
│  │ Europe/Moscow              ▼    │  [История] │
│  └─────────────────────────────────┘            │
│                                                 │
│  ─────────────────────────────────────────────  │
│                                                 │
│  Default Layout                                 │
│  ┌─────────────────────────────────┐            │
│  │ default                         │  [История] │
│  └─────────────────────────────────┘            │
│                                                 │
│  ...                                            │
│                                                 │
└─────────────────────────────────────────────────┘

При клике на [История] — модалка со списком изменений.

План миграции

Фаза 1: site_title_template (новая настройка)

Добавляем новую настройку без миграции данных.

Шаг Описание
1.1 Миграция: create table config_site_title_templates
1.2 sqlc: queries для read/write
1.3 GraphQL: типы ConfigString, ConfigStringEntry
1.4 GraphQL: query configSiteTitleTemplate
1.5 GraphQL: mutation setConfigSiteTitleTemplate
1.6 Resolver: internal/case/admin/setconfigsitetitletemplate/
1.7 Env method: SiteTitleTemplate() string
1.8 rendernotepage: formatTitle()
1.9 Frontend: новая страница /admin/config
1.10 Тесты

Фаза 2: timezone

Миграция существующей настройки.

Шаг Описание
2.1 Миграция: create table config_timezones
2.2 Миграция данных: insert ... select from config_versions
2.3 sqlc queries
2.4 GraphQL: query + mutation
2.5 Resolver
2.6 Заменить LatestConfig().TimezoneConfigTimezone()
2.7 Frontend: добавить на страницу
2.8 Тесты

Фаза 3: default_layout

Аналогично Фазе 2.

Фаза 4: robots_txt

Аналогично Фазе 2.

Фаза 5: show_draft_versions

Аналогично Фазе 2, но с типом bool.

Фаза 6: Удаление legacy

Шаг Описание
6.1 Удалить createConfigVersion mutation
6.2 Удалить latestConfig query
6.3 Удалить LatestConfig() из Env
6.4 Удалить старую страницу настроек
6.5 Миграция: drop table config_versions

Точки изменений (Фаза 1)

Файл Изменение
db/migrations/XXX_config_site_title_templates.sql Новая таблица
db/queries/read.sql GetConfigSiteTitleTemplateHistory, GetLatestConfigSiteTitleTemplate
db/queries/write.sql InsertConfigSiteTitleTemplate
internal/graph/admin.graphqls Типы + query + mutation
internal/graph/admin.resolvers.go Резолверы для query
internal/case/admin/setconfigsitetitletemplate/resolve.go Бизнес-логика мутации
cmd/server/main.go SiteTitleTemplate() string
internal/case/rendernotepage/endpoint.go formatTitle()
assets/ui/admin/config/ Новая страница

Env методы

// Текущие (legacy) — останутся до Фазы 6
func (a *app) LatestConfig() db.ConfigVersion

// Новые атомарные
func (a *app) SiteTitleTemplate() string      // Фаза 1
func (a *app) ConfigTimezone() string         // Фаза 2
func (a *app) ConfigDefaultLayout() string    // Фаза 3
func (a *app) ConfigRobotsTxt() string        // Фаза 4
func (a *app) ConfigShowDraftVersions() bool  // Фаза 5

Дефолты

Настройка Дефолт Описание
site_title_template %s Только название страницы
timezone UTC Часовой пояс
default_layout "" Без кастомного layout
robots_txt open Разрешить индексацию
show_draft_versions true Показывать черновики админам

Валидация

Настройка Правила
site_title_template Должен содержать %s
timezone Валидный timezone (time.LoadLocation)
default_layout Существующий layout или пустая строка
robots_txt open, closed, или произвольный текст
show_draft_versions bool, без валидации