sync
This commit is contained in:
@@ -87,6 +87,52 @@ export class AssetVariantsResponseDto {
|
||||
variants!: AssetVariantResponseDto[]
|
||||
}
|
||||
|
||||
export class AssetVersionResponseDto {
|
||||
@ApiProperty({ description: "Внутренний UUID версии source image.", example: "3b5da974-bb7f-4d73-b172-d6ad9c244528" })
|
||||
id!: string
|
||||
|
||||
@ApiProperty({ description: "Номер версии source image.", example: 2 })
|
||||
version!: number
|
||||
|
||||
@ApiProperty({ description: "Является ли версия текущей для asset.", example: true })
|
||||
isCurrent!: boolean
|
||||
|
||||
@ApiProperty({ description: "Source URL версии.", example: "https://storage.yandexcloud.net/shared1318/img/1.jpg" })
|
||||
sourceUrl!: string
|
||||
|
||||
@ApiProperty({ description: "Hostname source URL версии.", example: "storage.yandexcloud.net" })
|
||||
sourceHost!: string
|
||||
|
||||
@ApiProperty({ description: "Базовый Gateway path для версии.", example: "/images/asset_demo/v2/card" })
|
||||
imageBasePath!: string
|
||||
|
||||
@ApiPropertyOptional({ description: "Ширина оригинального изображения, если уже определена Worker.", example: 1200, nullable: true, type: Number })
|
||||
width!: number | null
|
||||
|
||||
@ApiPropertyOptional({ description: "Высота оригинального изображения, если уже определена Worker.", example: 800, nullable: true, type: Number })
|
||||
height!: number | null
|
||||
|
||||
@ApiPropertyOptional({ description: "Content-Type оригинального изображения, если уже определён Worker.", example: "image/jpeg", nullable: true, type: String })
|
||||
contentType!: string | null
|
||||
|
||||
@ApiPropertyOptional({ description: "Размер оригинального изображения в bytes, если уже определён Worker.", example: 245760, nullable: true, type: Number })
|
||||
sizeBytes!: number | null
|
||||
|
||||
@ApiProperty({ description: "Дата создания версии.", example: "2026-05-05T12:00:00.000Z" })
|
||||
createdAt!: string
|
||||
}
|
||||
|
||||
export class AssetVersionsResponseDto {
|
||||
@ApiProperty({ description: "Публичный идентификатор asset.", example: "asset_demo" })
|
||||
publicId!: string
|
||||
|
||||
@ApiProperty({ description: "Текущая версия source image.", example: 2 })
|
||||
currentVersion!: number
|
||||
|
||||
@ApiProperty({ description: "История версий source image.", type: [AssetVersionResponseDto] })
|
||||
versions!: AssetVersionResponseDto[]
|
||||
}
|
||||
|
||||
export class AssetsListResponseDto {
|
||||
@ApiProperty({ description: "Список assets.", type: [AssetResponseDto] })
|
||||
assets!: AssetResponseDto[]
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
import { AssetsService } from "./assets.service"
|
||||
import { AssetPictureResponseDto } from "./asset-picture-response.dto"
|
||||
import { AssetResponseDto, AssetVariantsResponseDto, AssetsListResponseDto } from "./asset-response.dto"
|
||||
import { AssetResponseDto, AssetVariantsResponseDto, AssetVersionsResponseDto, 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"
|
||||
@@ -60,6 +60,19 @@ export class AssetsController {
|
||||
return this.assets.getAsset(publicId)
|
||||
}
|
||||
|
||||
@Get(":publicId/versions")
|
||||
@ApiOperation({
|
||||
summary: "получить историю версий source image",
|
||||
description:
|
||||
"Возвращает все зарегистрированные source versions asset с current marker и versioned Gateway base path для каждой версии.",
|
||||
})
|
||||
@ApiParam({ description: "Публичный идентификатор asset.", example: "asset_demo", name: "publicId" })
|
||||
@ApiOkResponse({ description: "История версий source image возвращена.", type: AssetVersionsResponseDto })
|
||||
@ApiNotFoundResponse({ description: "Asset не найден." })
|
||||
listAssetVersions(@Param("publicId") publicId: string): Promise<AssetVersionsResponseDto> {
|
||||
return this.assets.listAssetVersions(publicId)
|
||||
}
|
||||
|
||||
@Post(":publicId/versions")
|
||||
@ApiOperation({
|
||||
summary: "создать новую версию source image",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common"
|
||||
import { imageAssets, imageAssetVersions, imageVariants } from "@image-platform/database"
|
||||
import { imageAssets, imageAssetVersions, imageProjects, imageVariants } from "@image-platform/database"
|
||||
import {
|
||||
CUSTOM_PRESET_NAME,
|
||||
ImageTransformConfigError,
|
||||
@@ -17,8 +17,15 @@ import { createHash, randomUUID } from "node:crypto"
|
||||
|
||||
import { DatabaseService } from "../infra/database.service"
|
||||
import { QueueService } from "../infra/queue.service"
|
||||
import { normalizeProjectSlug } from "../projects/project-slug"
|
||||
import type { AssetPictureResponseDto } from "./asset-picture-response.dto"
|
||||
import type { AssetResponseDto, AssetVariantResponseDto, AssetVariantsResponseDto, AssetsListResponseDto } from "./asset-response.dto"
|
||||
import type {
|
||||
AssetResponseDto,
|
||||
AssetVariantResponseDto,
|
||||
AssetVariantsResponseDto,
|
||||
AssetVersionsResponseDto,
|
||||
AssetsListResponseDto,
|
||||
} from "./asset-response.dto"
|
||||
import type { CreateAssetVersionRequestDto, CreateAssetVersionResponseDto } from "./create-asset-version.dto"
|
||||
import type { CreateAssetVariantsRequestDto, CreateAssetVariantsResponseDto } from "./create-asset-variants.dto"
|
||||
import type { CreateAssetRequestDto, CreateAssetResponseDto } from "./create-asset.dto"
|
||||
@@ -61,10 +68,39 @@ export class AssetsService {
|
||||
private readonly queue: QueueService,
|
||||
) {}
|
||||
|
||||
async listAssets(input: { limit?: string; offset?: string }): Promise<AssetsListResponseDto> {
|
||||
async listAssets(input: { limit?: string; offset?: string; projectSlug?: string }): Promise<AssetsListResponseDto> {
|
||||
const limit = parsePaginationInteger(input.limit, 50, 1, 100)
|
||||
const offset = parsePaginationInteger(input.offset, 0, 0, 10_000)
|
||||
|
||||
if (input.projectSlug) {
|
||||
const project = await this.loadProject(input.projectSlug)
|
||||
|
||||
const rows = await this.database.db
|
||||
.select({
|
||||
createdAt: imageAssets.createdAt,
|
||||
currentVersion: imageAssets.currentVersion,
|
||||
id: imageAssets.id,
|
||||
publicId: imageAssets.publicId,
|
||||
sourceHost: imageAssetVersions.sourceHost,
|
||||
sourceUrl: imageAssetVersions.sourceUrl,
|
||||
status: imageAssets.status,
|
||||
updatedAt: imageAssets.updatedAt,
|
||||
})
|
||||
.from(imageAssets)
|
||||
.innerJoin(
|
||||
imageAssetVersions,
|
||||
and(eq(imageAssetVersions.assetId, imageAssets.id), eq(imageAssetVersions.version, imageAssets.currentVersion)),
|
||||
)
|
||||
.where(eq(imageAssets.projectId, project.id))
|
||||
.orderBy(desc(imageAssets.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
return {
|
||||
assets: rows.map(mapAssetResponse),
|
||||
}
|
||||
}
|
||||
|
||||
const rows = await this.database.db
|
||||
.select({
|
||||
createdAt: imageAssets.createdAt,
|
||||
@@ -90,18 +126,19 @@ export class AssetsService {
|
||||
}
|
||||
}
|
||||
|
||||
async createAsset(request: CreateAssetRequestDto): Promise<CreateAssetResponseDto> {
|
||||
async createAsset(request: CreateAssetRequestDto, projectSlug?: string): Promise<CreateAssetResponseDto> {
|
||||
const source = normalizeSourceUrl(request.sourceUrl)
|
||||
await this.assertAllowedHost(source.hostname)
|
||||
|
||||
const publicId = request.publicId ? normalizePublicId(request.publicId) : generatePublicId()
|
||||
const sourceHash = createSourceHash(source.sourceUrl)
|
||||
const project = projectSlug ? await this.loadProject(projectSlug) : null
|
||||
|
||||
try {
|
||||
const result = await this.database.db.transaction(async (tx) => {
|
||||
const [asset] = await tx
|
||||
.insert(imageAssets)
|
||||
.values({ publicId })
|
||||
.values(project ? { projectId: project.id, publicId } : { publicId })
|
||||
.returning({ id: imageAssets.id, publicId: imageAssets.publicId })
|
||||
|
||||
if (!asset) {
|
||||
@@ -148,6 +185,58 @@ export class AssetsService {
|
||||
return mapAssetResponse(asset)
|
||||
}
|
||||
|
||||
async listAssetVersions(publicId: string): Promise<AssetVersionsResponseDto> {
|
||||
const normalizedPublicId = normalizePublicId(publicId)
|
||||
const [asset] = await this.database.db
|
||||
.select({
|
||||
currentVersion: imageAssets.currentVersion,
|
||||
id: imageAssets.id,
|
||||
publicId: imageAssets.publicId,
|
||||
status: imageAssets.status,
|
||||
})
|
||||
.from(imageAssets)
|
||||
.where(eq(imageAssets.publicId, normalizedPublicId))
|
||||
.limit(1)
|
||||
|
||||
if (!asset || asset.status !== "active") {
|
||||
throw new NotFoundException("asset not found")
|
||||
}
|
||||
|
||||
const versions = await this.database.db
|
||||
.select({
|
||||
contentType: imageAssetVersions.contentType,
|
||||
createdAt: imageAssetVersions.createdAt,
|
||||
height: imageAssetVersions.height,
|
||||
id: imageAssetVersions.id,
|
||||
sizeBytes: imageAssetVersions.sizeBytes,
|
||||
sourceHost: imageAssetVersions.sourceHost,
|
||||
sourceUrl: imageAssetVersions.sourceUrl,
|
||||
version: imageAssetVersions.version,
|
||||
width: imageAssetVersions.width,
|
||||
})
|
||||
.from(imageAssetVersions)
|
||||
.where(eq(imageAssetVersions.assetId, asset.id))
|
||||
.orderBy(desc(imageAssetVersions.version))
|
||||
|
||||
return {
|
||||
currentVersion: asset.currentVersion,
|
||||
publicId: asset.publicId,
|
||||
versions: versions.map((version) => ({
|
||||
contentType: version.contentType,
|
||||
createdAt: version.createdAt.toISOString(),
|
||||
height: version.height,
|
||||
id: version.id,
|
||||
imageBasePath: `/images/${asset.publicId}/v${version.version}/card`,
|
||||
isCurrent: version.version === asset.currentVersion,
|
||||
sizeBytes: version.sizeBytes,
|
||||
sourceHost: version.sourceHost,
|
||||
sourceUrl: version.sourceUrl,
|
||||
version: version.version,
|
||||
width: version.width,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
async createAssetVersion(
|
||||
publicId: string,
|
||||
request: CreateAssetVersionRequestDto,
|
||||
@@ -381,6 +470,21 @@ export class AssetsService {
|
||||
return asset
|
||||
}
|
||||
|
||||
private async loadProject(projectSlug: string) {
|
||||
const normalizedSlug = normalizeProjectSlug(projectSlug)
|
||||
const [project] = await this.database.db
|
||||
.select({ id: imageProjects.id, status: imageProjects.status })
|
||||
.from(imageProjects)
|
||||
.where(eq(imageProjects.slug, normalizedSlug))
|
||||
.limit(1)
|
||||
|
||||
if (!project || project.status !== "active") {
|
||||
throw new NotFoundException("project not found")
|
||||
}
|
||||
|
||||
return project
|
||||
}
|
||||
|
||||
private async loadAssetVersion(publicId: string, versionInput: number | null | undefined): Promise<AssetVersionRow> {
|
||||
const normalizedPublicId = normalizePublicId(publicId)
|
||||
const [asset] = await this.database.db
|
||||
|
||||
Reference in New Issue
Block a user