Files
image-platform/apps/backend/src/assets/assets.controller.ts
S.Gromov 56d551b43b feat: добавить endpoint picture/srcset
- добавлен contract DTO для picture sources и fallback image
- реализована выдача versioned Gateway URLs по static presets
- обновлена документация business API и dev smoke flow
2026-05-05 13:35:25 +03:00

138 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<AssetsListResponseDto> {
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<CreateAssetResponseDto> {
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<AssetResponseDto> {
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<CreateAssetVersionResponseDto> {
return this.assets.createAssetVersion(publicId, request)
}
@Get(":publicId/picture")
@ApiOperation({
summary: "получить picture/srcset URLs",
description:
"Возвращает готовый контракт для `<picture>` и `<img>` по 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<AssetPictureResponseDto> {
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<AssetVariantsResponseDto> {
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<CreateAssetVariantsResponseDto> {
return this.assets.createAssetVariants(publicId, request)
}
}