diff --git a/apps/api-service/src/routes/v1/images.ts b/apps/api-service/src/routes/v1/images.ts index f107789..9660e90 100644 --- a/apps/api-service/src/routes/v1/images.ts +++ b/apps/api-service/src/routes/v1/images.ts @@ -13,7 +13,7 @@ import { validateAndNormalizePagination } from '@/utils/validators'; import { buildPaginatedResponse } from '@/utils/helpers'; import { toImageResponse } from '@/types/responses'; import { db } from '@/db'; -import { flows } from '@banatie/database'; +import { flows, type Image } from '@banatie/database'; import { eq } from 'drizzle-orm'; import type { UploadImageResponse, @@ -704,13 +704,14 @@ imagesRouter.put( ); /** - * Assign a project-scoped alias to an image + * Assign or remove a project-scoped alias from an image * - * Sets or updates the project-scoped alias for an image: - * - Alias must start with @ symbol + * Sets, updates, or removes the project-scoped alias for an image: + * - Alias must start with @ symbol (when assigning) * - Must be unique within the project * - Replaces existing alias if image already has one * - Used for alias resolution in generations and CDN access + * - Set alias to null to remove existing alias * * This is a dedicated endpoint introduced in Section 6.1 to separate * alias assignment from general metadata updates. @@ -722,24 +723,30 @@ imagesRouter.put( * @param {string} req.params.id_or_alias - Image ID (UUID) or alias (@-prefixed) * @param {string} [req.query.flowId] - Flow ID for flow-scoped alias resolution * @param {object} req.body - Request body - * @param {string} req.body.alias - Project-scoped alias (e.g., "@hero-bg") + * @param {string|null} req.body.alias - Project-scoped alias (e.g., "@hero-bg") or null to remove * - * @returns {UpdateImageResponse} 200 - Updated image with new alias + * @returns {UpdateImageResponse} 200 - Updated image with new/removed alias * @returns {object} 404 - Image not found or access denied - * @returns {object} 400 - Missing or invalid alias + * @returns {object} 400 - Invalid alias format * @returns {object} 401 - Missing or invalid API key * @returns {object} 409 - Alias already exists * * @throws {Error} IMAGE_NOT_FOUND - Image does not exist - * @throws {Error} VALIDATION_ERROR - Alias is required + * @throws {Error} VALIDATION_ERROR - Invalid alias format * @throws {Error} ALIAS_CONFLICT - Alias already assigned to another image * - * @example UUID identifier + * @example Assign alias * PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias * { * "alias": "@hero-background" * } * + * @example Remove alias + * PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias + * { + * "alias": null + * } + * * @example Project-scoped alias identifier * PUT /api/v1/images/@old-hero/alias * { @@ -762,11 +769,12 @@ imagesRouter.put( const { flowId } = req.query; const { alias } = req.body; - if (!alias || typeof alias !== 'string') { + // Validate: alias must be null (to remove) or a non-empty string + if (alias !== null && (typeof alias !== 'string' || alias.trim() === '')) { res.status(400).json({ success: false, error: { - message: 'Alias is required and must be a string', + message: 'Alias must be null (to remove) or a non-empty string', code: 'VALIDATION_ERROR', }, }); @@ -815,7 +823,16 @@ imagesRouter.put( return; } - const updated = await service.assignProjectAlias(imageId, alias); + // Either remove alias (null) or assign new one (override behavior per Section 5.2) + let updated: Image; + if (alias === null) { + // Remove alias + updated = await service.update(imageId, { alias: null }); + } else { + // Reassign alias (clears from any existing image, then assigns to this one) + await service.reassignProjectAlias(alias, imageId, image.projectId); + updated = (await service.getById(imageId))!; + } res.json({ success: true, diff --git a/apps/api-service/src/services/core/ImageService.ts b/apps/api-service/src/services/core/ImageService.ts index f46ee08..76e6542 100644 --- a/apps/api-service/src/services/core/ImageService.ts +++ b/apps/api-service/src/services/core/ImageService.ts @@ -89,7 +89,7 @@ export class ImageService { async update( id: string, updates: { - alias?: string; + alias?: string | null; focalPoint?: { x: number; y: number }; meta?: Record; } diff --git a/tests/api/utils.ts b/tests/api/utils.ts index 4375da7..d604ea2 100644 --- a/tests/api/utils.ts +++ b/tests/api/utils.ts @@ -139,11 +139,21 @@ export async function uploadFile( ): Promise { const formData = new FormData(); - // Read file + // Read file and detect MIME type from extension const fs = await import('fs/promises'); + const path = await import('path'); const fileBuffer = await fs.readFile(filepath); - const blob = new Blob([fileBuffer]); - formData.append('file', blob, 'test-image.png'); + const ext = path.extname(filepath).toLowerCase(); + const mimeTypes: Record = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + }; + const mimeType = mimeTypes[ext] || 'application/octet-stream'; + const filename = path.basename(filepath); + const blob = new Blob([fileBuffer], { type: mimeType }); + formData.append('file', blob, filename); // Add other fields for (const [key, value] of Object.entries(fields)) {