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:
parent
e55c02d158
commit
874cc4fcba
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue