fix: add resolve endpoint and correct live path

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 <noreply@anthropic.com>
This commit is contained in:
Oleg Proskurin 2025-11-09 23:40:31 +07:00
parent e55c02d158
commit 874cc4fcba
7 changed files with 102 additions and 16 deletions

View File

@ -41,8 +41,8 @@ generationsRouter.post(
referenceImages, referenceImages,
aspectRatio, aspectRatio,
flowId, flowId,
outputAlias, assignAlias,
flowAliases, assignFlowAlias,
autoEnhance, autoEnhance,
meta, meta,
} = req.body; } = req.body;
@ -68,8 +68,8 @@ generationsRouter.post(
referenceImages, referenceImages,
aspectRatio, aspectRatio,
flowId, flowId,
outputAlias, assignAlias,
flowAliases, assignFlowAlias,
autoEnhance, autoEnhance,
meta, meta,
requestId: req.requestId, requestId: req.requestId,

View File

@ -1,6 +1,6 @@
import { Response, Router } from 'express'; import { Response, Router } from 'express';
import type { Router as RouterType } 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 { StorageFactory } from '@/services/StorageFactory';
import { asyncHandler } from '@/middleware/errorHandler'; import { asyncHandler } from '@/middleware/errorHandler';
import { validateApiKey } from '@/middleware/auth/validateApiKey'; import { validateApiKey } from '@/middleware/auth/validateApiKey';
@ -16,11 +16,13 @@ import type {
GetImageResponse, GetImageResponse,
UpdateImageResponse, UpdateImageResponse,
DeleteImageResponse, DeleteImageResponse,
ResolveAliasResponse,
} from '@/types/responses'; } from '@/types/responses';
export const imagesRouter: RouterType = Router(); export const imagesRouter: RouterType = Router();
let imageService: ImageService; let imageService: ImageService;
let aliasService: AliasService;
const getImageService = (): ImageService => { const getImageService = (): ImageService => {
if (!imageService) { if (!imageService) {
@ -29,6 +31,13 @@ const getImageService = (): ImageService => {
return imageService; return imageService;
}; };
const getAliasService = (): AliasService => {
if (!aliasService) {
aliasService = new AliasService();
}
return aliasService;
};
/** /**
* POST /api/v1/images/upload * POST /api/v1/images/upload
* Upload a single image file and create database record * 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<ResolveAliasResponse>) => {
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 /api/v1/images/:id
* Get a single image by ID * Get a single image by ID

View File

@ -36,12 +36,12 @@ const getImageService = (): ImageService => {
}; };
/** /**
* GET /api/v1/live/generate * GET /api/v1/live
* Generate image with prompt caching * Generate image with prompt caching
* Returns image bytes directly with cache headers * Returns image bytes directly with cache headers
*/ */
liveRouter.get( liveRouter.get(
'/generate', '/',
validateApiKey, validateApiKey,
requireProjectKey, requireProjectKey,
rateLimitByApiKey, rateLimitByApiKey,

View File

@ -21,8 +21,8 @@ export interface CreateGenerationParams {
referenceImages?: string[] | undefined; // Aliases to resolve referenceImages?: string[] | undefined; // Aliases to resolve
aspectRatio?: string | undefined; aspectRatio?: string | undefined;
flowId?: string | undefined; flowId?: string | undefined;
outputAlias?: string | undefined; assignAlias?: string | undefined;
flowAliases?: Record<string, string> | undefined; assignFlowAlias?: Record<string, string> | undefined;
autoEnhance?: boolean | undefined; autoEnhance?: boolean | undefined;
enhancedPrompt?: string | undefined; enhancedPrompt?: string | undefined;
meta?: Record<string, unknown> | undefined; meta?: Record<string, unknown> | undefined;
@ -125,12 +125,12 @@ export class GenerationService {
fileSize: 0, // TODO: Get actual file size from storage fileSize: 0, // TODO: Get actual file size from storage
fileHash, fileHash,
source: 'generated', source: 'generated',
alias: params.outputAlias || null, alias: params.assignAlias || null,
meta: params.meta || {}, meta: params.meta || {},
}); });
if (params.flowAliases && params.flowId) { if (params.assignFlowAlias && params.flowId) {
await this.assignFlowAliases(params.flowId, params.flowAliases, imageRecord.id); await this.assignFlowAliases(params.flowId, params.assignFlowAlias, imageRecord.id);
} }
if (params.flowId) { if (params.flowId) {

View File

@ -1,6 +1,6 @@
import { eq, and, isNull, desc, count, sql } from 'drizzle-orm'; import { eq, and, isNull, desc, count, sql } from 'drizzle-orm';
import { db } from '@/db'; import { db } from '@/db';
import { images } from '@banatie/database'; import { images, flows } from '@banatie/database';
import type { Image, NewImage, ImageFilters } from '@/types/models'; import type { Image, NewImage, ImageFilters } from '@/types/models';
import { buildWhereClause, buildEqCondition, withoutDeleted } from '@/utils/helpers'; import { buildWhereClause, buildEqCondition, withoutDeleted } from '@/utils/helpers';
import { ERROR_MESSAGES } from '@/utils/constants'; import { ERROR_MESSAGES } from '@/utils/constants';
@ -18,6 +18,15 @@ export class ImageService {
if (!image) { if (!image) {
throw new Error('Failed to create image record'); 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; return image;
} }

View File

@ -9,8 +9,8 @@ export interface CreateGenerationRequest {
referenceImages?: string[]; // Array of aliases to resolve referenceImages?: string[]; // Array of aliases to resolve
aspectRatio?: string; // e.g., "1:1", "16:9", "3:2", "9:16" aspectRatio?: string; // e.g., "1:1", "16:9", "3:2", "9:16"
flowId?: string; flowId?: string;
outputAlias?: string; // Alias to assign to generated image assignAlias?: string; // Alias to assign to generated image
flowAliases?: Record<string, string>; // Flow-scoped aliases to assign assignFlowAlias?: Record<string, string>; // Flow-scoped aliases to assign
autoEnhance?: boolean; autoEnhance?: boolean;
enhancementOptions?: { enhancementOptions?: {
template?: 'photorealistic' | 'illustration' | 'minimalist' | 'sticker' | 'product' | 'comic' | 'general'; template?: 'photorealistic' | 'illustration' | 'minimalist' | 'sticker' | 'product' | 'comic' | 'general';

View File

@ -89,7 +89,7 @@ export interface AliasResolutionResponse {
alias: string; alias: string;
imageId: string; imageId: string;
scope: AliasScope; scope: AliasScope;
flowId?: string; flowId?: string | undefined;
image: ImageResponse; image: ImageResponse;
} }