Тема
Directus / Stripe / Shopify — уроки для API
Продолжение разбора Bitrix24 REST: три эталона по своим зонам — Directus (runtime-типы контента и права — наша модель), Stripe (золотой стандарт REST-дизайна и надёжности мутаций), Shopify Admin API (массовые обмены и вебхуки в масштабе). Исследовано по официальной документации и исходникам (Directus склонирован, GitHub issues проверены). Здесь только то, чего не было в нашем плане после Bitrix-разбора.
Что подтвердилось (решения не менять)
- JSONB вместо реальных таблиц (Directus делает
ALTER TABLEв рантайме): их модель платит локами DDL на больших таблицах, иммутабельностью имени/типа поля и знаменателем типов по худшей СУБД. Наш JSONB свободен от DDL-рисков. Подтверждено их же болью. - REST базовый + GraphQL опциональный: GraphQL-first Shopify — ответ на overfetching в масштабе десятков тысяч приложений, не наш случай. Урок: не делать «второй полный API», GraphQL — точечно для сложных выборок.
- Thin-payload вебхуков: Stripe пришёл к тому же в v2 (thin events неверсионированы, клиент делает fetch-by-id); Shopify дал подписчику
include_fields. Наш план верен. - Опциональный
total: у Directusmeta— только по явному?meta=; та же модель. - Версии
/api/vNоставить: date-версии Stripe требуют дорогой инфраструктуры «version gates», сам Stripe уходит от этого в v2.
Новые пробелы
1. Идемпотентность мутаций ⚠️ приоритет №1 (Stripe)
У нас не спроектирована вообще, а без неё ретраи 1С/маркетплейсов создают дубли заказов и лидов. Семантика Stripe целиком:
- Заголовок
Idempotency-Key(UUID) на POST-мутациях; сервер кэширует статус + тело первого ответа (включая ошибки) на 24 часа; повтор — тот же ответ + заголовокIdempotent-Replayed: true. - Тот же ключ + другое тело → fingerprint-сравнение → ошибка
idempotency_error, не тихий replay. Конкурентный повтор ключа → 409idempotency_key_in_use. - Ретрай-политика клиента: 4xx → новый ключ; 5xx/сеть → тот же ключ.
Laravel-пакеты (square1/laravel-idempotency и аналоги) покрывают базу, но без fingerprint и 409-лока — реалистично свой middleware (~1–2 дня). → волна 24.
2. Request-Id в каждом ответе + богатая ошибка (Stripe)
ULID в заголовке каждого ответа и в контексте логов (Log::withContext); в объект ошибки добавить type (класс: invalid_request / api_error / idempotency_error) и doc_url. Почти бесплатно, резко ускоряет саппорт интеграторов. → волна 24.
3. OpenAPI, урезанный под права токена (Directus)
Их /server/specs/oas прогоняет схему через права запрашивающего: OpenAPI = «что видишь ты», а не «что есть вообще». Наш планируемый GET /api/v1/schema должен делать так же — иначе схема раскрывает клиенту чужие модули и поля. → деталь волны 24.
4. Интроспекция прав: GET /permissions/me (Directus)
Эндпоинт эффективных прав (access: none/partial/full per action + поля) — по нему фронт (острова, кабинет) строит формы, не хардкодя роли. → волна 24.
5. Лок мутаций одного объекта (Stripe)
Одновременные мутации одной сущности от 1С и админки → Cache::lock("{model}:{id}"), при таймауте — 429 lock_timeout. → волна 24.
6. События с diff: previous_attributes (Stripe)
В update-событии — снимок изменённых полей «до». Подписчик (CRM/1С) синхронизируется без хранения прошлого состояния. В Laravel — getChanges()/getOriginal() при диспатче. Плюс request.idempotency_key в событии — второй слой анти-эхо к нашему origin. → спека webhooks-out (фаза 2–3), payload событий — волна 24 (аддитивно).
7. Reconciliation как штатная пара вебхукам (Shopify)
Официальная доктрина Shopify: «доставка вебхуков не гарантирована» → периодическая сверка. Нам нужен дешёвый эндпоинт ?updated_since= + cursor на каждом ресурсе — штатный, а не костыль интегратора. → волна 24 (дёшево при cursorPaginate).
8. Bulk-канал для массовых обменов (Shopify)
Их Bulk Operations — готовый дизайн нашего «канала для 1С/маркетплейсов» (фаза 3):
- Экспорт:
POST /bulk/exports {resource, filter}→ 202 + id → воркер пишет JSONL в storage → вебхукbulk.finishedс подписанным URL (TTL 7 дней);partial_data_urlпри сбое; прогрессobject_count; иерархии — плоскими строками с__parentId. - Импорт: staged upload JSONL (строка = набор полей записи) → асинхронная обработка → JSONL-отчёт, где строка N = результат строки N, ошибки построчные.
- Лимит: 1 активная операция на клиента; выполнение вне rate-limit.
→ фаза 3, ТЗ cms/import/cms/export/integrations-bus.
9. Гигиена вебхук-подписок (Shopify + Stripe)
- Автоотключение подписки после N провалов подряд (у Shopify — 8 за 4 часа) + уведомление владельцу; у нас dead-letter есть, «похорон» мёртвых подписчиков нет.
- Ротация signing-секрета с перекрытием: несколько валидных подписей
v1=, старый секрет живёт ≤ 24 ч (Stripe). - Толеранс timestamp 5 минут, timing-safe сравнение HMAC.
- Заголовки доставки:
Webhook-Id(дедупликация) +Event-Id(корреляция ретраев).
→ спека cms/webhooks-out (фаза 2–3).
10. Test/live-режим в самом токене (Stripe)
Префикс токена (cms_live_… / cms_test_…) + флаг livemode в каждом событии: стейджинговая интеграция физически не может мутировать прод. Дешёвая конвенция, закладывается при введении abilities. → волна 21 (вместе со scope-токенами).
11. Перенос схемы типов контента: diff + optimistic hash (Directus)
Их /schema/snapshot → diff → apply: apply принимает hash целевой схемы на момент diff — если прод-схему кто-то поменял между diff и apply, apply отклоняется. У нас типы контента — данные (контент-конвейер), но при переносе dev→prod тот же риск гонки. Добавить hash-проверку в cms:export/cms:import для cms_content_types. → спека data-exchange / волна 22 (перенос сайта).
12. Декларация поля: разделить schema и meta (Directus)
Их триада type / schema (БД-факт) / meta (interface, options, conditions, group, note, translations) — чистое разделение «что хранится» и «как редактировать». Наш Field::make() смешивает; для snapshot-диффов и версионирования схемы разделение выгодно. Плюс conditions[] — декларативное «hidden/required если поле X = Y» данными, а не кодом. → спека field-engine, рефактор без изменения хранимых данных.
13. Один Filter-DSL на четыре роли (Directus)
У Directus язык фильтров един для: запросов API, валидации полей, row-level прав и условий автоматизации. У нас это четыре разных словаря (query-builder, rules, Policy, FilterBus-условия). Полное объединение — дорого; минимум: языку фильтров API научиться _and/_or-группам и кванторам _some/_none для hasMany. → спека api-contract, по мере потребности.
14. Владение кастомными полями: namespace (Shopify)
Metafields имеют владельца: app-owned (интеграция контролирует, мерчанту read-only), merchant-owned, app-data (скрыто из UI). Наш кейс: поля, созданные модулем 1С на типе контента, не должны редактироваться контент-менеджером. Поле owner_module в схеме поля + read-only в Filament для чужих. → спека field-engine, фаза 3.
15. Версионирование: fall forward + health-report (Shopify)
- Запрос устаревшей версии не падает, а обслуживается старейшей поддерживаемой с честным заголовком
X-Api-Versionфактической версии. - Пиннинг версии payload per вебхук-подписка (payload — тоже контракт).
- «API health report»: отчёт по токену «ваши интеграции зовут deprecated, дедлайн» — при квартальных релизах почти обязателен. → фаза 5, вместе с маркетплейсом.
16. Анти-паттерны, подтверждённые issues Directus
Дёшево предотвратить сейчас, дорого чинить потом:
- Ревизии без ретеншна и индексов: 504 на ~1M строк activity (#17894); ретеншн они добавили только в 11.3. У нас
cms.revisions.keepесть — проверить, что индексы и джоба очистки войдут в реализацию, а не остались строкой в спеке. - Права, вычисляемые запросами на каждый request без кэша: до 2 минут на сложных ролях (#25861). Наш RBAC — кэшировать резолв прав (spatie умеет, включить).
- Wildcard-разворот связей без потолка: 45 с на
*с relation (#7783) + дыры в permissions (#3219). Наши includes — только whitelist + лимит глубины (у Stripe expand ≤ 4 уровней).
Статус планирования (2026-07-14)
| Урок | Куда |
|---|---|
| §1 Idempotency-Key, §2 Request-Id + ошибка, §3 схема под права, §4 permissions/me, §5 lock_timeout, §7 reconciliation | Волна 24 (дополнена) |
| §10 test/live-префикс токена | Волна 21 (со scope-токенами) |
| §11 hash-проверка переноса типов | Волна 22 (перенос сайта) |
| §6 previous_attributes, §9 гигиена подписок | ТЗ cms/webhooks-out (фаза 2–3) |
| §8 bulk-канал JSONL | Фаза 3 — cms/import/cms/export/integrations-bus |
| §12–14 движок полей и Filter-DSL | ✅ внесено в спеки field-engine и api-contract (2026-07-14) |
| §15 fall forward, health-report, тарифные лимиты | Фаза 5 |
| §16 анти-паттерны | Чек-лист в гейты соответствующих волн |
Что сознательно не берём
- Date-версии API (Stripe) — инфраструктура version gates дороже пользы;
/vNнаш. - GraphQL-first / отказ от REST (Shopify) — их масштаб, не наш.
- Реальные таблицы + DDL в рантайме (Directus) — JSONB-решение подтверждено.
- Отдельная подсистема webhooks — Directus сам её удалил в v11, влив в автоматизацию; у нас вебхуки — модуль поверх событий, не отдельное ядро. Совпадает.
- EventBridge/Pub-Sub транспорты — до спроса; но идею «доставка в очередь вместо HTTP» держим для тяжёлых интеграций.