# 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 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 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, 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 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: ```text /images/{assetId}/v{version}/{preset}?w={width}&q={quality}&f=auto ``` ## Presets Клиент не должен передавать произвольные трансформации. Сначала нужны ограниченные presets. Пример: ```text avatar: widths: 128, 256, 512 formats: avif, webp, jpg quality: 80 resize: fill card: widths: 320, 640, 960 formats: avif, webp, jpg quality: 80 resize: fit hero: widths: 1280, 1920 formats: avif, webp, jpg quality: 80 resize: fit ```