import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common" import { ApiBadRequestResponse, ApiConflictResponse, ApiCreatedResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags, } from "@nestjs/swagger" import { AssetsService } from "./assets.service" import { AssetPictureResponseDto } from "./asset-picture-response.dto" import { AssetResponseDto, AssetVariantsResponseDto, AssetsListResponseDto } from "./asset-response.dto" import { CreateAssetVersionRequestDto, CreateAssetVersionResponseDto } from "./create-asset-version.dto" import { CreateAssetVariantsRequestDto, CreateAssetVariantsResponseDto } from "./create-asset-variants.dto" import { CreateAssetRequestDto, CreateAssetResponseDto } from "./create-asset.dto" @ApiTags("assets") @Controller("assets") export class AssetsController { constructor(private readonly assets: AssetsService) {} @Get() @ApiOperation({ summary: "получить список assets", description: "Возвращает последние зарегистрированные assets вместе с source URL текущей версии.", }) @ApiQuery({ description: "Максимальное количество assets в ответе.", example: 50, name: "limit", required: false }) @ApiQuery({ description: "Смещение для простого paging.", example: 0, name: "offset", required: false }) @ApiOkResponse({ description: "Список assets возвращён.", type: AssetsListResponseDto }) listAssets(@Query("limit") limit?: string, @Query("offset") offset?: string): Promise { return this.assets.listAssets({ limit, offset }) } @Post() @ApiOperation({ summary: "зарегистрировать исходное изображение", description: "Создаёт asset и первую версию source image. Source URL сохраняется в PostgreSQL, а публичный image URL строится через Gateway без раскрытия исходной ссылки клиенту.", }) @ApiCreatedResponse({ description: "Asset создан, версия source image зарегистрирована.", type: CreateAssetResponseDto }) @ApiBadRequestResponse({ description: "Некорректный sourceUrl, publicId или source host запрещён настройками." }) @ApiConflictResponse({ description: "Asset с таким publicId уже существует." }) createAsset(@Body() request: CreateAssetRequestDto): Promise { return this.assets.createAsset(request) } @Get(":publicId") @ApiOperation({ summary: "получить asset по publicId", description: "Возвращает metadata asset и source URL текущей версии.", }) @ApiParam({ description: "Публичный идентификатор asset.", example: "asset_demo", name: "publicId" }) @ApiOkResponse({ description: "Asset найден.", type: AssetResponseDto }) @ApiNotFoundResponse({ description: "Asset не найден." }) getAsset(@Param("publicId") publicId: string): Promise { return this.assets.getAsset(publicId) } @Post(":publicId/versions") @ApiOperation({ summary: "создать новую версию source image", description: "Регистрирует новый source URL для существующего asset, увеличивает currentVersion и тем самым создаёт новый immutable Gateway URL `/v{version}` без purge старых URLs.", }) @ApiParam({ description: "Публичный идентификатор asset.", example: "asset_demo", name: "publicId" }) @ApiCreatedResponse({ description: "Новая версия source image создана и стала текущей.", type: CreateAssetVersionResponseDto }) @ApiBadRequestResponse({ description: "Некорректный sourceUrl или source host запрещён настройками." }) @ApiConflictResponse({ description: "Версия asset изменилась конкурентно." }) @ApiNotFoundResponse({ description: "Asset не найден." }) createAssetVersion( @Param("publicId") publicId: string, @Body() request: CreateAssetVersionRequestDto, ): Promise { return this.assets.createAssetVersion(publicId, request) } @Get(":publicId/picture") @ApiOperation({ summary: "получить picture/srcset URLs", description: "Возвращает готовый контракт для `` и `` по static preset: sources, srcset, fallback src, sizes и versioned Gateway URLs. Endpoint не ставит generation jobs: Gateway сгенерирует bytes lazy или отдаст cache.", }) @ApiParam({ description: "Публичный идентификатор asset.", example: "asset_demo", name: "publicId" }) @ApiQuery({ description: "Static preset для picture contract.", example: "card", name: "preset", required: true }) @ApiQuery({ description: "Версия source image. Если не передана, используется currentVersion asset.", example: 1, name: "version", required: false }) @ApiQuery({ description: "Quality. Если не передано, берётся default quality preset.", example: 80, name: "quality", required: false }) @ApiQuery({ description: "Значение для HTML `sizes`.", example: "(min-width: 768px) 50vw, 100vw", name: "sizes", required: false }) @ApiOkResponse({ description: "Picture/srcset contract возвращён.", type: AssetPictureResponseDto }) @ApiBadRequestResponse({ description: "Некорректный preset, version, quality или sizes." }) @ApiNotFoundResponse({ description: "Asset или version не найдены." }) getAssetPicture( @Param("publicId") publicId: string, @Query("preset") preset?: string, @Query("version") version?: string, @Query("quality") quality?: string, @Query("sizes") sizes?: string, ): Promise { return this.assets.getAssetPicture(publicId, { preset, quality, sizes, version }) } @Get(":publicId/variants") @ApiOperation({ summary: "получить variants asset", description: "Возвращает variants asset: preset/custom параметры, status, S3 key, public URL и ошибку генерации, если она была.", }) @ApiParam({ description: "Публичный идентификатор asset.", example: "asset_demo", name: "publicId" }) @ApiQuery({ description: "Версия source image. Если не передана, возвращаются variants всех версий.", example: 1, name: "version", required: false }) @ApiOkResponse({ description: "Variants возвращены.", type: AssetVariantsResponseDto }) @ApiNotFoundResponse({ description: "Asset не найден." }) listAssetVariants( @Param("publicId") publicId: string, @Query("version") version?: string, ): Promise { return this.assets.listAssetVariants(publicId, version) } @Post(":publicId/variants") @ApiOperation({ summary: "поставить generation jobs для variants", description: "Business endpoint для явной подготовки variants. В режиме `single` создаёт один variant, в режиме `family` создаёт набор variants preset по всем разрешённым widths/formats. Endpoint не ждёт bytes, а возвращает созданные/переиспользованные rows и public URLs.", }) @ApiParam({ description: "Публичный идентификатор asset.", example: "asset_demo", name: "publicId" }) @ApiCreatedResponse({ description: "Variants созданы или переиспользованы, jobs поставлены при необходимости.", type: CreateAssetVariantsResponseDto }) @ApiBadRequestResponse({ description: "Некорректный preset/custom transform config." }) @ApiNotFoundResponse({ description: "Asset или version не найдены." }) createAssetVariants( @Param("publicId") publicId: string, @Body() request: CreateAssetVariantsRequestDto, ): Promise { return this.assets.createAssetVariants(publicId, request) } }