Skip to content

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: у Directus meta — только по явному ?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. Конкурентный повтор ключа → 409 idempotency_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» держим для тяжёлых интеграций.

Внутренняя база знаний студии