Files
image-platform/docs/backend-contract-draft.md
S.Gromov bcadb85a83 feat: добавить базовые сервисы image-platform
- добавлены backend, admin, gateway и worker skeleton
- добавлены Drizzle schema, database package и initial migration
- добавлены shared packages для RabbitMQ topology и S3 helpers
- обновлены dev-инфраструктура, env example, scripts и dependencies
- обновлена документация под versioned image URLs и read-through flow
2026-05-05 09:59:21 +03:00

225 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Черновик Backend Контракта
Это черновик будущего бизнес-контракта для NestJS backend. Сейчас реализованы system endpoints, Swagger и placeholder для internal image ensure.
## System
```text
GET /api/health
GET /docs
GET /docs-json
```
NestJS backend отдаёт JSON, metadata, statuses и URLs. Public image origin находится в Fastify Gateway. Backend владеет PostgreSQL, S3 orchestration и RabbitMQ jobs.
## Internal Image Ensure
Этот internal endpoint вызывается Gateway на L1 miss. Gateway не ходит в DB/S3 напрямую.
```text
POST /api/internal/images/ensure
```
Текущий статус реализации - endpoint зарезервирован и возвращает `501 Not Implemented`. Gateway `/images/*` пока тоже возвращает placeholder `501 Not Implemented`.
Request body:
```json
{
"assetId": "asset_123",
"version": 4,
"preset": "card",
"width": 640,
"quality": 80,
"format": "webp"
}
```
Query params:
| Param | Описание |
|---|---|
| `w` | целевая ширина, должна быть разрешена preset или округлена до ближайшей разрешённой |
| `q` | качество, должно быть из allowlist качества |
| `f` | `auto`, `avif`, `webp`, `jpg`; для Next/image по умолчанию `auto` |
Responsibilities:
- проверить `assetId` и `preset`;
- вычислить deterministic `variantHash`;
- проверить PostgreSQL и S3;
- если variant готов в S3, вернуть bytes или stream metadata для Gateway;
- если variant отсутствует, создать/переиспользовать variant row;
- поставить `generate-variant` job в RabbitMQ;
- дождаться `ready` до timeout, чтобы первый `next/image` request мог получить картинку;
- вернуть image response или metadata для Gateway.
Response headers:
```http
Content-Type: image/avif | image/webp | image/jpeg
Cache-Control: public, max-age=31536000, immutable
Vary: Accept
ETag: "..."
```
Ошибки:
| Status | Когда |
|---|---|
| `400` | некорректные query params |
| `404` | asset или preset не найден |
| `409` | variant уже генерируется и sync ожидание отключено |
| `422` | source image нельзя обработать |
| `502` | external imgproxy недоступен |
Public endpoint реализуется в Fastify Gateway, internal ensure endpoint - в Backend:
```text
client -> CDN -> Gateway /images/* -> Backend ensure -> RabbitMQ -> Worker -> imgproxy -> S3
```
Важно: `/images/*` не должен жить в NestJS `/api`. Gateway остаётся public image origin, Backend остаётся управлением процессов.
Public image URL versioned: `/images/{assetId}/v{version}/{preset}?w={width}&q={quality}&f=auto`.
## Allowed Hosts
```text
GET /allowed-hosts
POST /allowed-hosts
PATCH /allowed-hosts/:id
DELETE /allowed-hosts/:id
```
## Assets
```text
GET /assets
POST /assets
GET /assets/:id
DELETE /assets/:id
```
`POST /assets` request:
```json
{
"sourceUrl": "https://example.com/photo.jpg"
}
```
Responsibilities:
- validate source URL;
- check `allowed_image_hosts`;
- create or reuse `image_assets` row;
- optionally save original to S3 later.
## Variants
```text
GET /assets/:id/variants
POST /assets/:id/variants
POST /variants/:id/regenerate
DELETE /variants/:id
```
`POST /assets/:id/variants` request:
```json
{
"preset": "card",
"format": "webp",
"width": 640
}
```
Response if ready:
```json
{
"id": "variant_123",
"status": "ready",
"url": "http://localhost:8888/images/asset_123/v4/card?w=640&q=80&f=webp"
}
```
Response if generation is async:
```json
{
"id": "variant_123",
"status": "pending",
"url": null
}
```
## Image URLs For UI
Для ручного UI можно добавить endpoint, который возвращает готовый набор URLs для `<picture>`/`srcset`. Для `next/image` основным контрактом остаётся custom loader из `docs/next-image-provider.md`.
```text
GET /assets/:id/picture?preset=card
```
Example response:
```json
{
"assetId": "asset_123",
"preset": "card",
"sources": [
{
"type": "image/avif",
"srcset": "http://localhost:8888/images/asset_123/v4/card?w=320&q=80&f=avif 320w, http://localhost:8888/images/asset_123/v4/card?w=640&q=80&f=avif 640w"
},
{
"type": "image/webp",
"srcset": "http://localhost:8888/images/asset_123/v4/card?w=320&q=80&f=webp 320w, http://localhost:8888/images/asset_123/v4/card?w=640&q=80&f=webp 640w"
}
],
"fallback": {
"src": "http://localhost:8888/images/asset_123/v4/card?w=640&q=80&f=jpg",
"width": 640,
"height": null
}
}
```
## Worker Lifecycle
Worker выполняет задачи из RabbitMQ. Задачи создаёт Backend.
RabbitMQ topology:
```text
exchange: image-platform.jobs
queue: image.generate-variant
dead-letter exchange: image-platform.jobs.dlx
dead-letter queue: image.generate-variant.dlq
```
Job payload должен быть минимальным:
```json
{
"jobId": "job_123",
"variantId": "variant_123"
}
```
Worker читает детали variant из PostgreSQL, вызывает imgproxy, пишет результат в S3 и обновляет status в PostgreSQL.
Если генерация тяжёлая или не успела завершиться до timeout, Backend может вернуть Gateway `504`, а job продолжит выполняться/retry по очереди.
PostgreSQL может выступить первой очередью:
```text
SELECT * FROM image_variants
WHERE status = 'pending'
FOR UPDATE SKIP LOCKED
LIMIT 1
```
PostgreSQL-backed очередь не используется как основной механизм: для jobs выбран RabbitMQ.