225 lines
6.0 KiB
Markdown
225 lines
6.0 KiB
Markdown
|
|
# Черновик 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.
|