2026-05-05 09:59:21 +03:00
# Data Model
2026-05-04 22:53:55 +03:00
2026-05-05 09:59:21 +03:00
Текущая PostgreSQL модель описана в `packages/database/src/schema.ts` и миграциях Drizzle в `packages/database/drizzle` .
2026-05-04 22:53:55 +03:00
## allowed_image_hosts
```text
2026-05-05 09:59:21 +03:00
id uuid pk default gen_random_uuid()
hostname text not null unique
enabled boolean not null default true
description text nullable
created_at timestamptz not null default now()
updated_at timestamptz not null default now()
2026-05-04 22:53:55 +03:00
```
Правила normalization:
- lowercase;
- без protocol;
- без path;
- без trailing slash;
- без wildcard на первом этапе;
- source URL должен быть `http` или `https` ;
- запрещены localhost, private IP, loopback, link-local.
## image_assets
```text
2026-05-05 09:59:21 +03:00
id uuid pk default gen_random_uuid()
public_id text not null unique
current_version integer not null default 1
status asset_status not null default active
created_at timestamptz not null default now()
updated_at timestamptz not null default now()
2026-05-04 22:53:55 +03:00
```
2026-05-05 09:59:21 +03:00
`public_id` - стабильный идентификатор в public URL. `current_version` указывает активную версию source image и используется для cache invalidation без purge.
## image_asset_versions
```text
id uuid pk default gen_random_uuid()
asset_id uuid not null references image_assets(id) on delete cascade
version integer not null
source_url text not null
source_host text not null
source_hash text not null
original_s3_key text nullable
width integer nullable
height integer nullable
content_type text nullable
size_bytes bigint nullable
created_at timestamptz not null default now()
```
Каждое изменение source image создаёт новую версию. Старые versioned URLs остаются immutable, новые клиенты получают URL с новым `v{version}` .
2026-05-04 22:53:55 +03:00
## image_variants
```text
2026-05-05 09:59:21 +03:00
id uuid pk default gen_random_uuid()
asset_id uuid not null references image_assets(id) on delete cascade
asset_version_id uuid not null references image_asset_versions(id) on delete cascade
asset_version integer not null
preset text not null
variant_hash text not null unique
requested_format requested_format not null default auto
format variant_format not null
width integer not null
height integer nullable
2026-05-05 13:25:28 +03:00
resize_mode resize_mode not null default fit
2026-05-05 09:59:21 +03:00
quality integer not null
s3_key text not null unique
content_type text nullable
etag text nullable
status variant_status not null default pending
size_bytes bigint nullable
error text nullable
attempt_count integer not null default 0
last_accessed_at timestamptz nullable
created_at timestamptz not null default now()
updated_at timestamptz not null default now()
```
`requested_format` хранит то, что запросил клиент (`auto` , `avif` , `webp` , `jpg` , `png` ). `format` хранит фактический output format после negotiation по `Accept` .
## Enums
```text
asset_status: active | disabled | deleted
requested_format: auto | avif | webp | jpg | png
variant_format: avif | webp | jpg | png
2026-05-05 13:25:28 +03:00
resize_mode: fit | fill
2026-05-05 09:59:21 +03:00
variant_status: pending | processing | ready | failed
2026-05-04 22:53:55 +03:00
```
## Unique constraints
```text
allowed_image_hosts(hostname)
2026-05-05 09:59:21 +03:00
image_assets(public_id)
image_asset_versions(asset_id, version)
2026-05-05 13:25:28 +03:00
image_variants(asset_id, asset_version, preset, width, height, resize_mode, quality, format)
2026-05-05 09:59:21 +03:00
image_variants(s3_key)
image_variants(variant_hash)
```
Индексы:
```text
image_asset_versions(source_hash)
image_variants(status)
2026-05-04 22:53:55 +03:00
```
## S3 layout
```text
2026-05-05 09:59:21 +03:00
originals/{assetId}/v{version}/source
variants/{assetId}/v{version}/{variantHash}.{format}
```
`variantHash` должен включать:
- `assetId` ;
- `assetVersion` ;
- `preset` ;
- normalized width;
2026-05-05 13:25:28 +03:00
- normalized height, где `0` означает auto height;
- normalized resize mode;
2026-05-05 09:59:21 +03:00
- normalized quality;
- фактический output format;
- параметры transform, влияющие на bytes.
Для `f=auto` в public URL в S3 всё равно пишется фактический формат:
```text
variants/asset_123/v4/card_w640_q80_avif.avif
variants/asset_123/v4/card_w640_q80_webp.webp
variants/asset_123/v4/card_w640_q80_jpg.jpg
```
2026-05-05 13:25:28 +03:00
Public URL также versioned. Для fixed preset `w` и `q` можно не передавать, для responsive preset `w` обязателен:
2026-05-05 09:59:21 +03:00
```text
/images/{assetId}/v{version}/{preset}?w={width}& q={quality}& f=auto
2026-05-05 13:25:28 +03:00
/images/{assetId}/v{version}/avatar?f=auto
2026-05-04 22:53:55 +03:00
```
## Presets
2026-05-05 13:25:28 +03:00
Клиент не должен бесконтрольно создавать произвольные трансформации. Сейчас есть статический config в `packages/image-config` .
Режимы:
- `fixed` - preset задаёт один размер, например `avatar` .
- `responsive` - preset задаёт allowlist ширин, например `card` и `hero` .
- `custom` - произвольный single image, только если включён `IMAGE_ALLOW_CUSTOM_TRANSFORMS=true` .
2026-05-04 22:53:55 +03:00
Пример:
```text
avatar:
2026-05-05 13:25:28 +03:00
mode: fixed
width: 256
height: 256
2026-05-04 22:53:55 +03:00
formats: avif, webp, jpg
quality: 80
resize: fill
card:
2026-05-05 13:25:28 +03:00
mode: responsive
2026-05-04 22:53:55 +03:00
widths: 320, 640, 960
formats: avif, webp, jpg
2026-05-05 13:25:28 +03:00
qualities: 75, 80
2026-05-04 22:53:55 +03:00
resize: fit
hero:
2026-05-05 13:25:28 +03:00
mode: responsive
2026-05-04 22:53:55 +03:00
widths: 1280, 1920
formats: avif, webp, jpg
2026-05-05 13:25:28 +03:00
qualities: 75, 80
2026-05-04 22:53:55 +03:00
resize: fit
```