Документация Loreset
Loreset — Headless CMS для иерархического и полиморфного игрового контента. На этой странице собрана документация по текущей версии платформы: спецификация языка схем LSL, форматы экспорта (bundle-v1 и versions.json) и описание реализованных подсистем.
Обзор платформы
Платформа построена по принципу API-first: единственный интерфейс — REST API v1 с JWT-аутентификацией. Все ответы возвращаются в едином контракте:
{ "success": true, "data": { … }, "message": null, "errors": null }
Общая схема компонентов:
Frontend SPA ──JWT Bearer──> REST API (v1)
│
┌──────────────┼──────────────────┐
│ │ │
AclManager LslValidator ExportBuilder
│ │ │
└──────> ItemStorage Publisher (S3/local)
│ │
I18nIndexer CDN (S3)
│
i18n_registry
Данные хранятся по принципу Hybrid Storage: метаданные, связи и версии — в реляционных таблицах (SQLite или MySQL), сами объекты с произвольной вложенностью — в JSON-документах. JSON-колонки хранятся как TEXT для переносимости между СУБД.
Ключевые понятия
| Понятие | Описание |
|---|---|
| Проект | Игра. Содержит коллекции, участников, ассеты и настройки CDN. |
| Коллекция | «Справочник» — именованный набор записей, описанный LSL-схемой. Код уникален в рамках проекта. Флаг is_singleton ограничивает коллекцию одной записью (например, глобальные настройки баланса). |
| Схема | Иммутабельная версия LSL-описания коллекции. Публикация новой версии сдвигает HEAD-указатель current_schema_id, старые версии остаются доступными. |
| Запись (item) | Не содержит данных — только статус (draft → published → archived) и HEAD-указатель на актуальную ревизию. |
| Ревизия | Иммутабельный JSON-снапшот данных записи. Каждая правка добавляет ревизию с version + 1. История никогда не переписывается. |
| Публикация | Выгрузка версионированного контент-бандла и индекса versions.json на CDN проекта. |
Реализованные возможности
Текущая версия платформы включает следующие подсистемы:
- Проекты и участники — многопользовательские проекты с ролями (
viewer,translator,content_manager,admin) и индивидуальными переопределениями доступа на уровне коллекций. - Коллекции и схемы LSL — создание коллекций вместе с первой версией схемы, иммутабельное версионирование схем, валидация определения схемы при сохранении.
- Записи и ревизии — CRUD записей с валидацией по текущей схеме, история ревизий, rollback без потери истории, синглтоны, статусы публикации.
- Локализация — переводы внутри JSON-документов (
i18n_string/i18n_text), автоматическая индексация в плоский реестр, точечная правка переводов, CSV-экспорт и импорт для Google Sheets. - Ассеты — загрузка файлов проекта (иконки, звуки) с произвольными метаданными; id ассета используется в полях типа
asset. - Импорт и экспорт — самодостаточный бандл
loreset/bundle-v1; импорт работает как идемпотентный upsert в одной транзакции. - Публикация на CDN — драйверы
s3(любое S3-совместимое хранилище: AWS, MinIO, DO Spaces, Selectel) иlocalдля разработки; монотонные версии, чексуммы, история публикаций. - Инструменты разработчика — OpenAPI-спецификация и коллекция Postman, генерируемые из кода; сидер демо-данных; unit- и functional-тесты.
.po / .xliff, refresh-токены.Loreset Schema Language (LSL)
LSL — язык описания схем коллекций. Он базируется на JSON Schema, но расширен игровыми типами данных и UI-хинтами, по которым админка автоматически генерирует формы редактирования. Схема — обычный JSON-файл, который удобно хранить в Git (Schema-as-Code).
Минимальная схема коллекции:
{
"type": "object",
"properties": {
"title": { "type": "i18n_string", "ui": { "label": "Название" } },
"trigger_event": {
"type": "enum",
"options": ["first_login", "loot_drop", "level_complete"]
},
"is_hidden": { "type": "boolean" }
},
"required": ["title", "trigger_event"]
}
Переиспользуемые блоки выносятся в $defs и подключаются через {"$ref": "#/$defs/Name"}. Каждому полю можно задать объект ui с подписью (label) и компонентом отображения (component) — например, searchable_select, sortable_list, tabs.
Типы данных LSL
| Тип | Значение в данных |
|---|---|
integer, float, boolean | соответствующие скаляры |
string, text, richtext | строка (text — многострочный, richtext — с тегами-ссылками <ref/> на другие записи) |
timestamp | unix time (int) или дата-строка |
duration | число секунд |
asset | id ассета (строка) |
i18n_string, i18n_text | объект {"_i18n": true, "en": "...", "ru": "..."} |
enum | одно из options (скаляры или объекты {value, label}) |
relation | id записи целевой коллекции (target); при value_field: "id" проверяется существование цели |
array | список, каждый элемент валидируется по items |
dictionary | объект «ключ → значение», значения валидируются по схеме values |
union | объект с полем-дискриминатором; вариант выбирается по variants |
object | вложенный объект с properties / required |
relation — связь между коллекциями
"hero": {
"type": "relation",
"target": "heroes",
"value_field": "id",
"display_template": "{{id}}. {{name}} ({{class}})",
"filters": { "is_playable": { "eq": true } },
"ui": { "label": "Герой", "component": "searchable_select" }
}
union — полиморфные структуры
Ключевой для игр тип: поле принимает одну из нескольких структур в зависимости от значения дискриминатора.
// Схема
{
"type": "union",
"discriminator": "reward_type",
"variants": {
"currency": { "$ref": "#/$defs/RewardCurrency" },
"item": { "$ref": "#/$defs/RewardItem" }
}
}
// Валидные данные
{ "reward_type": "currency", "currency": "gold", "amount": 500, "func": "add" }
dictionary — карты «ключ → значение»
"currency_wallet": {
"type": "dictionary",
"keys": { "type": "relation", "target": "currencies" },
"values": { "type": "integer" },
"ui": { "label": "Кошелёк (Валюта → Количество)" }
}
Правила валидации данных
Данные записи проверяются по текущей схеме коллекции перед каждым сохранением; невалидные данные не сохраняются.
- Неизвестные поля запрещены (
Unknown field). - Поля из
requiredобязаны присутствовать и быть не-null. - Служебные поля с префиксом
_(_i18n,_legacy) игнорируются. relationсvalue_field: "id"проверяет существование целевой записи в проекте.- Ошибки возвращаются картой «путь → сообщение»:
{
"success": false,
"message": "Item data does not match the collection schema.",
"errors": {
"$.trigger_event": "Value is not in the list of allowed options.",
"$.rewards[0].amount": "Expected an integer."
}
}
Контент-бандл (loreset/bundle-v1)
Единый самодостаточный JSON для игровых движков: схемы, опубликованные записи и плоский словарь переводов. Используется и для экспорта в клиент, и для переноса контента между средами (Dev → Prod).
{
"format": "loreset/bundle-v1",
"project": "heliostorm",
"version": 7,
"generated_at": "2026-07-08T12:00:00+00:00",
"checksum": "sha256:ab12…",
"schemas": {
"bonuses": { "version": 2, "schema": { … } }
},
"items": {
"bonuses": [
{
"id": "93a01703-…",
"data": {
"title": { "en": "Welcome Pack", "ru": "Приветственный набор" },
"trigger_event": "first_login"
}
}
]
},
"i18n": {
"bonuses.93a01703-….title": { "en": "Welcome Pack", "ru": "Приветственный набор" }
}
}
version— монотонный номер публикации проекта; движок сравнивает его со своей локальной копией.checksum— sha256 от полезной нагрузки (schemas+items+i18n).- Данные «схлопнуты»: служебные поля (
_i18n,_legacyи любые с префиксом_) удалены, переводы остаются объектами{locale: value}. - Включаются только записи со статусом
published.
Экспорт — GET /v1/projects/{id}/export. Импорт — POST /v1/projects/{id}/import с телом бандла: отсутствующие коллекции создаются, изменившиеся схемы публикуются новой версией, записи вставляются или обновляются по id, повторный импорт того же бандла — no-op. Всё выполняется в одной транзакции.
Индекс версий на CDN (versions.json)
При каждой успешной публикации рядом с бандлом bundle-v{N}.json перезаписывается файл versions.json — индекс всех успешных публикаций проекта. Игровой клиент одним запросом статического файла узнаёт актуальную версию контента и URL бандла, не обращаясь к API:
<base_url>/<prefix>/versions.json
// например: https://cdn.example.com/content/versions.json
Формат файла
{
"format": "loreset/versions-v1",
"project": "my-rpg",
"generated_at": 1783508000,
"latest": 7,
"versions": [
{
"version": 7,
"file": "bundle-v7.json",
"url": "https://cdn.example.com/content/bundle-v7.json",
"checksum": "sha256:ab12…",
"items_count": 132,
"published_at": 1783508000
},
{
"version": 6,
"file": "bundle-v6.json",
"url": "https://cdn.example.com/content/bundle-v6.json",
"checksum": "sha256:cd34…",
"items_count": 130,
"published_at": 1783420000
}
]
}
Поля верхнего уровня
| Поле | Тип | Описание |
|---|---|---|
format | string | Маркер формата, всегда loreset/versions-v1 |
project | string | Код проекта |
generated_at | int | Unix-время генерации индекса |
latest | int | Номер последней (актуальной) версии бандла |
versions | array | Список успешных публикаций, новые сверху |
Элемент массива versions
| Поле | Тип | Описание |
|---|---|---|
version | int | Номер версии бандла (монотонно растёт в рамках проекта) |
file | string | Имя файла бандла; публичная ссылка собирается как <base_url>/<prefix>/<file> |
url | string | URL файла, сохранённый на момент публикации |
checksum | string | Контрольная сумма (sha256:…), совпадает с полем checksum внутри бандла |
items_count | int | Количество записей в бандле |
published_at | int | Unix-время публикации |
failed в индекс не попадают. Если выгрузка versions.json не удалась, вся публикация помечается как failed.Рекомендуемый сценарий обновления на клиенте
- Скачать
versions.json. - Сравнить
latestс версией локальной копии бандла. - Если версия новее — скачать файл записи с
version == latestи проверитьchecksum. - Любую сетевую ошибку трактовать мягко: продолжать работу на текущей версии.
Записи и ревизии
Запись хранит только статус и HEAD-указатель; данные лежат в иммутабельных ревизиях. Любое изменение — правка, перевод, rollback, импорт — создаёт новую ревизию и сдвигает HEAD.
# Создание записи (валидируется по текущей схеме)
curl -X POST https://api.loreset.dev/v1/collections/<uuid>/items \
-H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
-d '{
"status": "draft",
"data": {
"title": { "_i18n": true, "en": "Welcome Pack", "ru": "Приветственный набор" },
"trigger_event": "first_login",
"rewards": [
{ "reward_type": "currency", "currency": "gold", "amount": 500, "func": "add" }
]
}
}'
Rollback не удаляет историю: данные старой ревизии копируются в новую, HEAD сдвигается на неё. Было v1…v3 — станет v4 с данными выбранной ревизии:
curl -X POST https://api.loreset.dev/v1/items/<uuid>/rollback \
-H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
-d '{"revision_id": "<uuid-старой-ревизии>"}'
Статусы записи: draft → published → archived (переходы произвольные). В экспортный бандл попадают только published. В коллекции-синглтоне можно создать лишь одну запись.
Локализация (i18n)
Переводы живут внутри данных записи — любое поле типа i18n_string / i18n_text хранится как объект с флагом _i18n:
"title": { "_i18n": true, "en": "Welcome Pack", "ru": "Приветственный набор" }
При каждой новой ревизии индексатор обходит JSON и пересобирает плоский реестр переводов: item_id + field_path + locale → value. Путь записывается в нотации rewards[0].title. Правка перевода — версионируемое изменение: создаётся новая ревизия записи.
CSV-пайплайн для Google Sheets
Экспорт отдаёт CSV с одной строкой на переводимое поле и колонкой на локаль:
collection,item_id,field_path,en,ru
bonuses,93a01703-…,title,Welcome Pack,Приветственный набор
bonuses,93a01703-…,rewards[0].title,Sword,
Файл загружается в Google Sheets, переводчики заполняют пустые ячейки, затем CSV импортируется обратно. Правила импорта:
- строки матчатся по
item_id+field_path; - пустые ячейки пропускаются — существующие переводы не затираются;
- на каждую изменённую запись создаётся одна новая ревизия, сколько бы полей ни поменялось;
- строки без прав доступа или с неизвестными записями попадают в
skipped, импорт не прерывается.
Модель доступа (ACL)
Четыре уровня доступа, каждый включает предыдущие:
| Уровень | Что позволяет |
|---|---|
view | чтение записей, схем, реестра переводов, экспорт |
translate | + правка переводов (точечная и CSV-импорт) |
edit | + создание/правка/удаление записей, rollback, смена статуса, загрузка ассетов |
manage | + структура (коллекции, схемы), участники, ACL, настройки проекта, импорт, публикация |
Источники прав (в порядке приоритета):
- Супер-админ — полный доступ ко всему.
- Роль в проекте — базовый уровень:
viewer→ view,translator→ translate,content_manager→ edit,admin→ manage. - Переопределение на коллекцию — перекрывает роль проекта для конкретного справочника. Значение
noneполностью скрывает коллекцию: она исчезает из списков, любые запросы дают 403.
Пример: у контент-менеджера стоит none на справочник «Квесты» — он редактирует всё, кроме квестов. У зрителя стоит edit на «Бонусы» — он может править только бонусы.
Публикация на CDN
У каждого проекта свои настройки CDN в поле cdn_settings:
{
"driver": "s3",
"bucket": "my-rpg-content",
"region": "eu-central-1",
"endpoint": "https://s3.example.com",
"base_url": "https://cdn.example.com",
"prefix": "content/"
}
Драйвер s3 работает с любым S3-совместимым хранилищем (AWS S3, MinIO, DO Spaces, Selectel и т.п., включая кастомный endpoint и path-style). Драйвер local кладёт файлы в локальное хранилище — удобно для разработки. Секреты маскируются во всех ответах API.
Публикация (уровень manage):
curl -X POST https://api.loreset.dev/v1/projects/1/publish \
-H "Authorization: Bearer <token>"
Что происходит:
- Выделяется следующий монотонный
version. - Собирается бандл
loreset/bundle-v1из опубликованных записей. - Файл
bundle-v{N}.jsonвыгружается на CDN через драйвер проекта. - Рядом перезаписывается индекс
versions.json. - В историю публикаций пишется запись с URL, чексуммой и количеством записей.
История публикаций доступна по GET /v1/projects/{id}/publications — список отсортирован по версии, новые сверху. При сбое выгрузки создаётся запись со статусом failed, и такая публикация не попадает в versions.json.
versions.json и горячее обновление контента с CDN.