feature/api-development #1

Merged
usulpro merged 47 commits from feature/api-development into main 2025-11-29 23:03:01 +07:00
3 changed files with 43 additions and 16 deletions
Showing only changes of commit 6803a23aa3 - Show all commits

View File

@ -13,7 +13,7 @@ import { validateAndNormalizePagination } from '@/utils/validators';
import { buildPaginatedResponse } from '@/utils/helpers'; import { buildPaginatedResponse } from '@/utils/helpers';
import { toImageResponse } from '@/types/responses'; import { toImageResponse } from '@/types/responses';
import { db } from '@/db'; import { db } from '@/db';
import { flows } from '@banatie/database'; import { flows, type Image } from '@banatie/database';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import type { import type {
UploadImageResponse, 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: * Sets, updates, or removes the project-scoped alias for an image:
* - Alias must start with @ symbol * - Alias must start with @ symbol (when assigning)
* - Must be unique within the project * - Must be unique within the project
* - Replaces existing alias if image already has one * - Replaces existing alias if image already has one
* - Used for alias resolution in generations and CDN access * - 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 * This is a dedicated endpoint introduced in Section 6.1 to separate
* alias assignment from general metadata updates. * 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.params.id_or_alias - Image ID (UUID) or alias (@-prefixed)
* @param {string} [req.query.flowId] - Flow ID for flow-scoped alias resolution * @param {string} [req.query.flowId] - Flow ID for flow-scoped alias resolution
* @param {object} req.body - Request body * @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} 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} 401 - Missing or invalid API key
* @returns {object} 409 - Alias already exists * @returns {object} 409 - Alias already exists
* *
* @throws {Error} IMAGE_NOT_FOUND - Image does not exist * @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 * @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 * PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias
* { * {
* "alias": "@hero-background" * "alias": "@hero-background"
* } * }
* *
* @example Remove alias
* PUT /api/v1/images/550e8400-e29b-41d4-a716-446655440000/alias
* {
* "alias": null
* }
*
* @example Project-scoped alias identifier * @example Project-scoped alias identifier
* PUT /api/v1/images/@old-hero/alias * PUT /api/v1/images/@old-hero/alias
* { * {
@ -762,11 +769,12 @@ imagesRouter.put(
const { flowId } = req.query; const { flowId } = req.query;
const { alias } = req.body; 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({ res.status(400).json({
success: false, success: false,
error: { 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', code: 'VALIDATION_ERROR',
}, },
}); });
@ -815,7 +823,16 @@ imagesRouter.put(
return; 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({ res.json({
success: true, success: true,

View File

@ -89,7 +89,7 @@ export class ImageService {
async update( async update(
id: string, id: string,
updates: { updates: {
alias?: string; alias?: string | null;
focalPoint?: { x: number; y: number }; focalPoint?: { x: number; y: number };
meta?: Record<string, unknown>; meta?: Record<string, unknown>;
} }

View File

@ -139,11 +139,21 @@ export async function uploadFile(
): Promise<any> { ): Promise<any> {
const formData = new FormData(); const formData = new FormData();
// Read file // Read file and detect MIME type from extension
const fs = await import('fs/promises'); const fs = await import('fs/promises');
const path = await import('path');
const fileBuffer = await fs.readFile(filepath); const fileBuffer = await fs.readFile(filepath);
const blob = new Blob([fileBuffer]); const ext = path.extname(filepath).toLowerCase();
formData.append('file', blob, 'test-image.png'); const mimeTypes: Record<string, string> = {
'.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 // Add other fields
for (const [key, value] of Object.entries(fields)) { for (const [key, value] of Object.entries(fields)) {