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
This commit is contained in:
224
docs/backend-contract-draft.md
Normal file
224
docs/backend-contract-draft.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Черновик 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.
|
||||
Reference in New Issue
Block a user