From 874cc4fcba337c97efb5ff6933f858f6d229bb85 Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Sun, 9 Nov 2025 23:40:31 +0700 Subject: [PATCH] fix: add resolve endpoint and correct live path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL FIXES: - Add GET /api/v1/images/resolve/:alias endpoint with 3-tier alias resolution - Supports optional flowId query parameter - Returns image, scope (technical/flow/project), and flowId - Placed before GET /:id to avoid route conflict - Change live endpoint from /api/v1/live/generate to /api/v1/live - Corrects path to match specification PARAMETER NAMING: - Rename outputAlias to assignAlias in requests and service - Rename flowAliases to assignFlowAlias in requests and service - Update generations route to use new parameter names FLOW TIMESTAMP UPDATES: - Add flow.updatedAt trigger in ImageService.create() - Updates flow timestamp when image is uploaded to a flow - Flow.updatedAt already updated in GenerationService.create() AUDIT TRAIL VERIFICATION: - Confirmed apiKeyId is properly saved in generations table - Confirmed apiKeyId is properly saved in images table (both upload and generation) TYPE FIXES: - Add explicit | undefined to AliasResolutionResponse.flowId 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/api-service/src/routes/v1/generations.ts | 8 +- apps/api-service/src/routes/v1/images.ts | 79 ++++++++++++++++++- apps/api-service/src/routes/v1/live.ts | 4 +- .../src/services/core/GenerationService.ts | 10 +-- .../src/services/core/ImageService.ts | 11 ++- apps/api-service/src/types/requests.ts | 4 +- apps/api-service/src/types/responses.ts | 2 +- 7 files changed, 102 insertions(+), 16 deletions(-) diff --git a/apps/api-service/src/routes/v1/generations.ts b/apps/api-service/src/routes/v1/generations.ts index 7574856..9a862bd 100644 --- a/apps/api-service/src/routes/v1/generations.ts +++ b/apps/api-service/src/routes/v1/generations.ts @@ -41,8 +41,8 @@ generationsRouter.post( referenceImages, aspectRatio, flowId, - outputAlias, - flowAliases, + assignAlias, + assignFlowAlias, autoEnhance, meta, } = req.body; @@ -68,8 +68,8 @@ generationsRouter.post( referenceImages, aspectRatio, flowId, - outputAlias, - flowAliases, + assignAlias, + assignFlowAlias, autoEnhance, meta, requestId: req.requestId, diff --git a/apps/api-service/src/routes/v1/images.ts b/apps/api-service/src/routes/v1/images.ts index 1a962ea..cf31e1e 100644 --- a/apps/api-service/src/routes/v1/images.ts +++ b/apps/api-service/src/routes/v1/images.ts @@ -1,6 +1,6 @@ import { Response, Router } from 'express'; import type { Router as RouterType } from 'express'; -import { ImageService } from '@/services/core'; +import { ImageService, AliasService } from '@/services/core'; import { StorageFactory } from '@/services/StorageFactory'; import { asyncHandler } from '@/middleware/errorHandler'; import { validateApiKey } from '@/middleware/auth/validateApiKey'; @@ -16,11 +16,13 @@ import type { GetImageResponse, UpdateImageResponse, DeleteImageResponse, + ResolveAliasResponse, } from '@/types/responses'; export const imagesRouter: RouterType = Router(); let imageService: ImageService; +let aliasService: AliasService; const getImageService = (): ImageService => { if (!imageService) { @@ -29,6 +31,13 @@ const getImageService = (): ImageService => { return imageService; }; +const getAliasService = (): AliasService => { + if (!aliasService) { + aliasService = new AliasService(); + } + return aliasService; +}; + /** * POST /api/v1/images/upload * Upload a single image file and create database record @@ -162,6 +171,74 @@ imagesRouter.get( }) ); +/** + * GET /api/v1/images/resolve/:alias + * Resolve an alias to an image using 3-tier precedence (technical -> flow -> project) + */ +imagesRouter.get( + '/resolve/:alias', + validateApiKey, + requireProjectKey, + asyncHandler(async (req: any, res: Response) => { + const aliasServiceInstance = getAliasService(); + const { alias } = req.params; + const { flowId } = req.query; + + const projectId = req.apiKey.projectId; + + try { + const resolution = await aliasServiceInstance.resolve( + alias, + projectId, + flowId as string | undefined + ); + + if (!resolution) { + res.status(404).json({ + success: false, + error: { + message: `Alias '${alias}' not found`, + code: 'ALIAS_NOT_FOUND', + }, + }); + return; + } + + // Verify project ownership + if (resolution.image && resolution.image.projectId !== projectId) { + res.status(404).json({ + success: false, + error: { + message: 'Alias not found', + code: 'ALIAS_NOT_FOUND', + }, + }); + return; + } + + res.json({ + success: true, + data: { + alias, + imageId: resolution.imageId, + scope: resolution.scope, + flowId: resolution.flowId, + image: resolution.image ? toImageResponse(resolution.image) : ({} as any), + }, + }); + } catch (error) { + res.status(500).json({ + success: false, + error: { + message: error instanceof Error ? error.message : 'Failed to resolve alias', + code: 'RESOLUTION_ERROR', + }, + }); + return; + } + }) +); + /** * GET /api/v1/images/:id * Get a single image by ID diff --git a/apps/api-service/src/routes/v1/live.ts b/apps/api-service/src/routes/v1/live.ts index 342f503..eea1faf 100644 --- a/apps/api-service/src/routes/v1/live.ts +++ b/apps/api-service/src/routes/v1/live.ts @@ -36,12 +36,12 @@ const getImageService = (): ImageService => { }; /** - * GET /api/v1/live/generate + * GET /api/v1/live * Generate image with prompt caching * Returns image bytes directly with cache headers */ liveRouter.get( - '/generate', + '/', validateApiKey, requireProjectKey, rateLimitByApiKey, diff --git a/apps/api-service/src/services/core/GenerationService.ts b/apps/api-service/src/services/core/GenerationService.ts index 8c674d4..0ef30ff 100644 --- a/apps/api-service/src/services/core/GenerationService.ts +++ b/apps/api-service/src/services/core/GenerationService.ts @@ -21,8 +21,8 @@ export interface CreateGenerationParams { referenceImages?: string[] | undefined; // Aliases to resolve aspectRatio?: string | undefined; flowId?: string | undefined; - outputAlias?: string | undefined; - flowAliases?: Record | undefined; + assignAlias?: string | undefined; + assignFlowAlias?: Record | undefined; autoEnhance?: boolean | undefined; enhancedPrompt?: string | undefined; meta?: Record | undefined; @@ -125,12 +125,12 @@ export class GenerationService { fileSize: 0, // TODO: Get actual file size from storage fileHash, source: 'generated', - alias: params.outputAlias || null, + alias: params.assignAlias || null, meta: params.meta || {}, }); - if (params.flowAliases && params.flowId) { - await this.assignFlowAliases(params.flowId, params.flowAliases, imageRecord.id); + if (params.assignFlowAlias && params.flowId) { + await this.assignFlowAliases(params.flowId, params.assignFlowAlias, imageRecord.id); } if (params.flowId) { diff --git a/apps/api-service/src/services/core/ImageService.ts b/apps/api-service/src/services/core/ImageService.ts index 563f020..d6e7ece 100644 --- a/apps/api-service/src/services/core/ImageService.ts +++ b/apps/api-service/src/services/core/ImageService.ts @@ -1,6 +1,6 @@ import { eq, and, isNull, desc, count, sql } from 'drizzle-orm'; import { db } from '@/db'; -import { images } from '@banatie/database'; +import { images, flows } from '@banatie/database'; import type { Image, NewImage, ImageFilters } from '@/types/models'; import { buildWhereClause, buildEqCondition, withoutDeleted } from '@/utils/helpers'; import { ERROR_MESSAGES } from '@/utils/constants'; @@ -18,6 +18,15 @@ export class ImageService { if (!image) { throw new Error('Failed to create image record'); } + + // Update flow timestamp if image is part of a flow + if (image.flowId) { + await db + .update(flows) + .set({ updatedAt: new Date() }) + .where(eq(flows.id, image.flowId)); + } + return image; } diff --git a/apps/api-service/src/types/requests.ts b/apps/api-service/src/types/requests.ts index 1b757fd..f397874 100644 --- a/apps/api-service/src/types/requests.ts +++ b/apps/api-service/src/types/requests.ts @@ -9,8 +9,8 @@ export interface CreateGenerationRequest { referenceImages?: string[]; // Array of aliases to resolve aspectRatio?: string; // e.g., "1:1", "16:9", "3:2", "9:16" flowId?: string; - outputAlias?: string; // Alias to assign to generated image - flowAliases?: Record; // Flow-scoped aliases to assign + assignAlias?: string; // Alias to assign to generated image + assignFlowAlias?: Record; // Flow-scoped aliases to assign autoEnhance?: boolean; enhancementOptions?: { template?: 'photorealistic' | 'illustration' | 'minimalist' | 'sticker' | 'product' | 'comic' | 'general'; diff --git a/apps/api-service/src/types/responses.ts b/apps/api-service/src/types/responses.ts index f230681..dd14bf1 100644 --- a/apps/api-service/src/types/responses.ts +++ b/apps/api-service/src/types/responses.ts @@ -89,7 +89,7 @@ export interface AliasResolutionResponse { alias: string; imageId: string; scope: AliasScope; - flowId?: string; + flowId?: string | undefined; image: ImageResponse; }