Типы контента и поля

На странице Первый тип контента — базовый тур: как создать тип, какие бывают поля, как устроены ссылки и локализация. Здесь — справочник: наследование, интерфейсы, полиморфные блоки, разрешения и то, что под капотом схемы.

Наследование (Extends)

Тип может наследовать поля другого типа. В редакторе типа выберите Родительский тип контента (раздел Наследование) — наследник получит все поля родителя и добавит свои. Пример: blog-post наследует page — появляются title, slug, status, seo, blocks, а к ним добавляются excerpt, published_at, author, tags.

Правила наследования:

  • Одинарное и цепочечное — тип наследует одного родителя, но родитель сам может наследовать: landing-page → page → …. Глубина не ограничена.
  • Потомок перекрывает родителя — если в потомке объявлено поле с тем же именем, оно заменяет родительское на том же месте (порядок сохраняется).
  • Порядок полей стабилен — сначала поля родителя (в их порядке), затем поля потомка.
  • Циклы запрещеныA extends B extends A отклоняется с ошибкой CIRCULAR_INHERITANCE.
  • Родитель должен существовать — указать несуществующий slug нельзя, сервер вернёт PARENT_NOT_FOUND.
  • Extends нельзя менять после создания типа — родитель фиксируется один раз.
  • Наследник может не иметь своих полейlanding-page наследует page и не добавляет ни одного своего поля: вся страница собирается из блоков.

Наследованные поля попадают не только в GraphQL-тип, но и в фильтры и сортировку. blogPosts(where: { status: { eq: "published" } }) работает именно потому, что status унаследован от page.

Интерфейсы (Node, Timestamped)

Каждый тип автоматически реализует два GraphQL-интерфейса:

Интерфейс Поля
Node id: ID!
Timestamped created_at: DateTime, updated_at: DateTime

Эти поля добавляются сервером — их не нужно объявлять в схеме типа. id генерируется автоматически при создании записи, created_at и updated_at проставляются при записи.

Существует третий интерфейс — Entity (id, slug, name), но он не добавляется типам автоматически и нужен для внутренней типизации.

Полиморфные блоки

Поле типа «Тип контента» + Сделать массивом без указания целевого типа — это полиморфный список блоков. Так устроено поле blocks у типа page: в одну страницу можно сложить блоки любых типов (Hero, Text, Gallery, CTA и т. д.), и порядок элементов сохраняется.

В GraphQL такое поле получает тип [Block], где Block — это union всех типов в схеме. Запрос:

query {
  pages {
    title
    blocks {
      ... on HeroSection { title subtitle }
      ... on TextBlock { body }
      ... on Gallery { images { url } }
    }
  }
}

Ограничения полиморфных блоков:

  • Нельзя фильтровать и сортировать — поле blocks не появляется в <Type>Where и <Type>OrderByField.
  • Нельзя локализовать — сервер отклонит (LOCALIZED_POLYMORPHIC_BLOCKS_UNSUPPORTED). Список блоков один на все языки.
  • Имена полей должны быть совместимы — все типы в схеме входят в union Block, поэтому одноимённые поля в разных типах должны совпадать по GraphQL-типу. Иначе — BLOCK_UNION_FIELD_TYPE_CONFLICT.

Union живой: он содержит все типы схемы, поэтому созданный позже тип блока сразу доступен через blocks (дайте асинхронной пересборке схемы секунду-другую). Перечислять фрагменты руками тоже не обязательно: голая выборка blocks { } разворачивается сервером в выборки скалярных полей каждого типа с __typename. А если два УЖЕ СУЩЕСТВУЮЩИХ типа объявили одноимённое поле с разными скалярными типами (например, Int и Float), запросы не ломаются — сервер проставит алиас конфликтующему полю для каждого члена union. Проверка при создании выше страхует только НОВЫЕ определения; старые коллизии терпятся на этапе запросов, а не ломают сайт.

Если нужен список ссылок на один конкретный тип (например, теги), укажите целевой тип — тогда поле становится обычным массивом ссылок, который можно фильтровать и локализовать.

Поле типа Список (select)

Список (select) — это enum-поле: значение выбирается из предопределённого списка вариантов. В GraphQL оно представлено как String, но сервер принуждает значение к списку разрешённых — сохранить произвольную строку нельзя.

Варианты настраиваются прямо в редакторе поля. Когда выбран тип Список, появляется редактор Вариантов: каждая строка содержит:

  • Значение (сохраняется) — что записывается в БД (draft, published).
  • Подпись (отображается) — что видит редактор в админке (Черновик, Опубликовано).

Также выбирается Значение по умолчанию — оно подставляется в новую запись.

Пример: поле status у page хранит draft или published. Попробовать записать archive — сервер вернёт SELECT_VALUE_NOT_ALLOWED.

Если у Списка не задано ни одного варианта, проверка отключается (поле ведёт себя как свободный текст). Это нужно для обратной совместимости со старыми полями, созданными до этого механизма.

Зарезервированные имена

GraphQL-имя типа — это PascalCase от slug (blog-postBlogPost). Имена, которые заняты самой схемой, нельзя использовать:

Node, Entity, Timestamped, EntityDefinition, PropertyDefinition, Condition, ValidationRule, APIKey, APIKeyMutation, Query, Mutation, CreateEntityInput, UpdateEntityInput, PropertyInput, ConditionInput, ValidationRuleInput, AuthPayload.

Если PascalCase от slug совпадёт с одним из них, сервер отклонит создание типа.

Разрешения на тип

У каждого типа есть матрица разрешений — кто из ролей может читать, создавать, обновлять и удалять записи. По умолчанию:

Роль Чтение Создание Изменение Удаление
viewer да
editor да да да
admin / owner да да да да

Разрешения хранятся в поле permissions типа и редактируются на странице «Разрешения» в админке. Подробнее — на странице Роли и доступ.

Что под капотом

Записи хранятся как JSONB в таблице entity_data. Схема типа (определение полей) живёт отдельно, в entity_definitions. Когда вы меняете схему:

  1. Сервер валидирует тип (зарезервированные имена, циклы наследования, конфликты Block-union).
  2. Сохраняет новую версию в entity_definitions и фиксирует версию схемы.
  3. SchemaManager пересобирает GraphQL-схему из актуальных определений.
  4. Новая схема вступает в силу атомарно — идущие запросы дорабатывают на старой, новые идут на новой.

Добавить поле — и оно в API. Удалить — и его нет. Мигрировать данные не нужно: JSON хранит форму, а определение описывает её.

Что дальше


---