# Data Model Текущая PostgreSQL модель описана в `packages/database/src/schema.ts` и миграциях Drizzle в `packages/database/drizzle`. ## allowed_image_hosts ```text 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() ``` Правила normalization: - lowercase; - без protocol; - без path; - без trailing slash; - без wildcard на первом этапе; - source URL должен быть `http` или `https`; - запрещены localhost, private IP, loopback, link-local. ## image_assets ```text 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() ``` `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}`. ## image_variants ```text 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 resize_mode resize_mode not null default fit 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 resize_mode: fit | fill variant_status: pending | processing | ready | failed ``` ## Unique constraints ```text allowed_image_hosts(hostname) image_assets(public_id) image_asset_versions(asset_id, version) image_variants(asset_id, asset_version, preset, width, height, resize_mode, quality, format) image_variants(s3_key) image_variants(variant_hash) ``` Индексы: ```text image_asset_versions(source_hash) image_variants(status) ``` ## S3 layout ```text originals/{assetId}/v{version}/source variants/{assetId}/v{version}/{variantHash}.{format} ``` `variantHash` должен включать: - `assetId`; - `assetVersion`; - `preset`; - normalized width; - normalized height, где `0` означает auto height; - normalized resize mode; - 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 ``` Public URL также versioned. Для fixed preset `w` и `q` можно не передавать, для responsive preset `w` обязателен: ```text /images/{assetId}/v{version}/{preset}?w={width}&q={quality}&f=auto /images/{assetId}/v{version}/avatar?f=auto ``` ## Presets Клиент не должен бесконтрольно создавать произвольные трансформации. Сейчас есть статический config в `packages/image-config`. Режимы: - `fixed` - preset задаёт один размер, например `avatar`. - `responsive` - preset задаёт allowlist ширин, например `card` и `hero`. - `custom` - произвольный single image, только если включён `IMAGE_ALLOW_CUSTOM_TRANSFORMS=true`. Пример: ```text avatar: mode: fixed width: 256 height: 256 formats: avif, webp, jpg quality: 80 resize: fill card: mode: responsive widths: 320, 640, 960 formats: avif, webp, jpg qualities: 75, 80 resize: fit hero: mode: responsive widths: 1280, 1920 formats: avif, webp, jpg qualities: 75, 80 resize: fit ```