- добавлены 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
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.
|