import { Body, Controller, Get, Headers, Inject, Param, Post, Put, Req, Res } from '@nestjs/common'; import { z } from 'zod'; import { saveProductEditorDraftRequestSchema, type Workspace } from '@atlas/contracts'; import { ApiError } from '../common/api-error.js'; import type { AuthenticatedRequest } from '../auth/auth.guard.js'; import { CurrentWorkspace } from '../workspace/current-workspace.decorator.js'; import { ProductEditorService } from './product-editor.service.js'; type Response = { status: (value: number) => Response }; @Controller() export class ProductEditorController { public constructor( @Inject(ProductEditorService) private readonly editor: ProductEditorService, ) {} @Post('collection-items/:collectionItemId/editor-contexts') async enter( @Param('collectionItemId') collectionItemId: string, @Headers('x-atlas-platform-id') platformId: string | undefined, @Headers('idempotency-key') key: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, @Res({ passthrough: true }) response: Response, ) { const result = await this.editor.enter( workspace.id, request.auth.userId, requiredUuid('collection_item_id', collectionItemId), requiredUuid('X-Atlas-Platform-Id', platformId), requiredUuid('Idempotency-Key', key), ); response.status(result.status); return result.body; } @Get('products/:productId/editor') async read( @Param('productId') productId: string, @Headers('x-atlas-platform-id') platformId: string | undefined, @CurrentWorkspace() workspace: Workspace, ) { return { data: await this.editor.read( workspace.id, requiredUuid('product_id', productId), requiredUuid('X-Atlas-Platform-Id', platformId), ), }; } @Put('products/:productId/editor-draft') async save( @Param('productId') productId: string, @Body() body: unknown, @Headers('x-atlas-platform-id') platformId: string | undefined, @Headers('idempotency-key') key: string | undefined, @Headers('if-match') ifMatch: string | undefined, @Req() request: AuthenticatedRequest, @CurrentWorkspace() workspace: Workspace, ) { const parsed = saveProductEditorDraftRequestSchema.safeParse(body); if (!parsed.success) { throw new ApiError(422, 'VALIDATION_FAILED', '请求参数不符合要求', parsed.error.issues.map((issue) => ({ field: issue.path.join('.'), code: 'INVALID_FIELD', message: issue.message, }))); } return (await this.editor.save( workspace.id, request.auth.userId, requiredUuid('product_id', productId), requiredUuid('X-Atlas-Platform-Id', platformId), requiredUuid('Idempotency-Key', key), parseEditorRevision(ifMatch), parsed.data, )).body; } } function requiredUuid(field: string, value: string | undefined) { if (value && z.uuid().safeParse(value).success) return value; throw new ApiError(400, 'VALIDATION_FAILED', '请求参数不符合要求', [ { field, code: 'INVALID_FIELD', message: `${field} 必须是 UUID` }, ]); } function parseEditorRevision(value: string | undefined) { const match = value?.match(/^"([1-9]\d*):([1-9]\d*)"$/); if (!match) throw new ApiError(400, 'IF_MATCH_REQUIRED', '请求缺少或格式错误的 If-Match'); return { product: Number(match[1]), profile: Number(match[2]) }; }