feat: phase 2 part 2 - upload enhancements and deletion strategy overhaul
Implement comprehensive deletion cascade logic, upload enhancements, and alias
management updates for Phase 2 Part 2 of the API refactoring.
**Upload Enhancements (Section 5):**
- POST /api/v1/images/upload now supports flowAlias parameter
- Eager flow creation: creates flow record immediately when flowAlias provided
- FlowId logic: undefined → generate UUID, null → keep null, UUID → use provided
- Automatically assigns flowAlias in flow.aliases JSONB upon upload
**Alias Management (Section 6):**
- Removed alias from PUT /api/v1/images/:id request body
- Only focalPoint and meta can be updated via PUT endpoint
- Use dedicated PUT /api/v1/images/:id/alias endpoint for alias assignment
**Deletion Strategy Overhaul (Section 7):**
- **ImageService.hardDelete()** with MinIO cleanup and cascades:
- Deletes physical file from MinIO storage
- Cascades: sets outputImageId=NULL in related generations
- Cascades: removes alias entries from flow.aliases
- Cascades: removes imageId from generation.referencedImages arrays
- MVP approach: proceeds with DB cleanup even if MinIO fails
- **GenerationService.delete()** with conditional logic:
- If output image WITHOUT alias → hard delete both image and generation
- If output image WITH alias → keep image, delete generation only, set generationId=NULL
- **FlowService.delete()** with cascade and alias protection:
- Deletes all generations (uses conditional delete logic)
- Deletes all images WITHOUT alias
- Keeps images WITH alias (sets flowId=NULL)
- Deletes flow record from database
**Type Updates:**
- UploadImageRequest: Added flowAlias parameter (string)
- UpdateImageRequest: Removed alias field (Section 6.1)
- GenerationResponse: Updated prompt fields to match reversed semantics
- prompt: string (what was actually used for generation)
- originalPrompt: string | null (user's original, only if enhanced)
- DeleteImageResponse: Changed to { id: string } (hard delete, no deletedAt)
**Error Constants (Section 11):**
- Removed: GENERATION_ALREADY_SUCCEEDED, MAX_RETRY_COUNT_EXCEEDED
- Added: SCOPE_INVALID_FORMAT, SCOPE_CREATION_DISABLED,
SCOPE_GENERATION_LIMIT_EXCEEDED, STORAGE_DELETE_FAILED
**Technical Notes:**
- Hard delete replaces soft delete throughout the system
- Cascade operations maintain referential integrity
- Alias protection ensures valuable images are preserved
- All Phase 2 Part 2 code compiles with zero new TypeScript errors
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9b9c47e2bf
commit
7d87202934
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
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, AliasService } from '@/services/core';
|
import { ImageService, AliasService } from '@/services/core';
|
||||||
|
|
@ -10,6 +11,9 @@ import { uploadSingleImage, handleUploadErrors } from '@/middleware/upload';
|
||||||
import { validateAndNormalizePagination } from '@/utils/validators';
|
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 { flows } from '@banatie/database';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
import type {
|
import type {
|
||||||
UploadImageResponse,
|
UploadImageResponse,
|
||||||
ListImagesResponse,
|
ListImagesResponse,
|
||||||
|
|
@ -51,7 +55,7 @@ imagesRouter.post(
|
||||||
handleUploadErrors,
|
handleUploadErrors,
|
||||||
asyncHandler(async (req: any, res: Response<UploadImageResponse>) => {
|
asyncHandler(async (req: any, res: Response<UploadImageResponse>) => {
|
||||||
const service = getImageService();
|
const service = getImageService();
|
||||||
const { alias, flowId, meta } = req.body;
|
const { alias, flowId, flowAlias, meta } = req.body;
|
||||||
|
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
|
|
@ -70,6 +74,19 @@ imagesRouter.post(
|
||||||
const projectSlug = req.apiKey.projectSlug;
|
const projectSlug = req.apiKey.projectSlug;
|
||||||
const file = req.file;
|
const file = req.file;
|
||||||
|
|
||||||
|
// FlowId logic (Section 10.1 & 5.1):
|
||||||
|
// - If undefined (not provided) → generate new UUID
|
||||||
|
// - If null (explicitly null) → keep null
|
||||||
|
// - If string (specific value) → use that value
|
||||||
|
let finalFlowId: string | null;
|
||||||
|
if (flowId === undefined) {
|
||||||
|
finalFlowId = randomUUID();
|
||||||
|
} else if (flowId === null) {
|
||||||
|
finalFlowId = null;
|
||||||
|
} else {
|
||||||
|
finalFlowId = flowId;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const storageService = await StorageFactory.getInstance();
|
const storageService = await StorageFactory.getInstance();
|
||||||
|
|
||||||
|
|
@ -96,7 +113,7 @@ imagesRouter.post(
|
||||||
|
|
||||||
const imageRecord = await service.create({
|
const imageRecord = await service.create({
|
||||||
projectId,
|
projectId,
|
||||||
flowId: flowId || null,
|
flowId: finalFlowId,
|
||||||
generationId: null,
|
generationId: null,
|
||||||
apiKeyId,
|
apiKeyId,
|
||||||
storageKey: uploadResult.path!,
|
storageKey: uploadResult.path!,
|
||||||
|
|
@ -109,6 +126,39 @@ imagesRouter.post(
|
||||||
meta: meta ? JSON.parse(meta) : {},
|
meta: meta ? JSON.parse(meta) : {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Eager flow creation if flowAlias is provided (Section 5.1)
|
||||||
|
if (flowAlias && finalFlowId) {
|
||||||
|
// Check if flow exists, create if not
|
||||||
|
const existingFlow = await db.query.flows.findFirst({
|
||||||
|
where: eq(flows.id, finalFlowId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingFlow) {
|
||||||
|
await db.insert(flows).values({
|
||||||
|
id: finalFlowId,
|
||||||
|
projectId,
|
||||||
|
aliases: {},
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign flow alias to uploaded image
|
||||||
|
const flow = await db.query.flows.findFirst({
|
||||||
|
where: eq(flows.id, finalFlowId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (flow) {
|
||||||
|
const currentAliases = (flow.aliases as Record<string, string>) || {};
|
||||||
|
const updatedAliases = { ...currentAliases };
|
||||||
|
updatedAliases[flowAlias] = imageRecord.id;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(flows)
|
||||||
|
.set({ aliases: updatedAliases, updatedAt: new Date() })
|
||||||
|
.where(eq(flows.id, finalFlowId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: toImageResponse(imageRecord),
|
data: toImageResponse(imageRecord),
|
||||||
|
|
@ -292,7 +342,7 @@ imagesRouter.put(
|
||||||
asyncHandler(async (req: any, res: Response<UpdateImageResponse>) => {
|
asyncHandler(async (req: any, res: Response<UpdateImageResponse>) => {
|
||||||
const service = getImageService();
|
const service = getImageService();
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { alias, focalPoint, meta } = req.body;
|
const { focalPoint, meta } = req.body; // Removed alias (Section 6.1)
|
||||||
|
|
||||||
const image = await service.getById(id);
|
const image = await service.getById(id);
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
|
@ -318,12 +368,10 @@ imagesRouter.put(
|
||||||
}
|
}
|
||||||
|
|
||||||
const updates: {
|
const updates: {
|
||||||
alias?: string;
|
|
||||||
focalPoint?: { x: number; y: number };
|
focalPoint?: { x: number; y: number };
|
||||||
meta?: Record<string, unknown>;
|
meta?: Record<string, unknown>;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (alias !== undefined) updates.alias = alias;
|
|
||||||
if (focalPoint !== undefined) updates.focalPoint = focalPoint;
|
if (focalPoint !== undefined) updates.focalPoint = focalPoint;
|
||||||
if (meta !== undefined) updates.meta = meta;
|
if (meta !== undefined) updates.meta = meta;
|
||||||
|
|
||||||
|
|
@ -394,7 +442,7 @@ imagesRouter.put(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/v1/images/:id
|
* DELETE /api/v1/images/:id
|
||||||
* Soft delete an image
|
* Hard delete an image with MinIO cleanup and cascades (Section 7.1)
|
||||||
*/
|
*/
|
||||||
imagesRouter.delete(
|
imagesRouter.delete(
|
||||||
'/:id',
|
'/:id',
|
||||||
|
|
@ -427,14 +475,11 @@ imagesRouter.delete(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleted = await service.softDelete(id);
|
await service.hardDelete(id);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: { id },
|
||||||
id: deleted.id,
|
|
||||||
deletedAt: deleted.deletedAt?.toISOString() || null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { flows, generations, images } from '@banatie/database';
|
||||||
import type { Flow, NewFlow, FlowFilters, FlowWithCounts } from '@/types/models';
|
import type { Flow, NewFlow, FlowFilters, FlowWithCounts } from '@/types/models';
|
||||||
import { buildWhereClause, buildEqCondition } from '@/utils/helpers';
|
import { buildWhereClause, buildEqCondition } from '@/utils/helpers';
|
||||||
import { ERROR_MESSAGES } from '@/utils/constants';
|
import { ERROR_MESSAGES } from '@/utils/constants';
|
||||||
|
import { GenerationService } from './GenerationService';
|
||||||
|
import { ImageService } from './ImageService';
|
||||||
|
|
||||||
export class FlowService {
|
export class FlowService {
|
||||||
async create(data: NewFlow): Promise<FlowWithCounts> {
|
async create(data: NewFlow): Promise<FlowWithCounts> {
|
||||||
|
|
@ -163,7 +165,46 @@ export class FlowService {
|
||||||
return await this.getByIdWithCounts(id);
|
return await this.getByIdWithCounts(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cascade delete for flow with alias protection (Section 7.3)
|
||||||
|
* Operations:
|
||||||
|
* 1. Delete all generations associated with this flowId (follows conditional delete logic)
|
||||||
|
* 2. Delete all images associated with this flowId EXCEPT images with project alias
|
||||||
|
* 3. For images with alias: keep image, set flowId=NULL
|
||||||
|
* 4. Delete flow record from DB
|
||||||
|
*/
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
|
// Get all generations in this flow
|
||||||
|
const flowGenerations = await db.query.generations.findMany({
|
||||||
|
where: eq(generations.flowId, id),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete each generation (follows conditional delete logic from Section 7.2)
|
||||||
|
const generationService = new GenerationService();
|
||||||
|
for (const gen of flowGenerations) {
|
||||||
|
await generationService.delete(gen.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all images in this flow
|
||||||
|
const flowImages = await db.query.images.findMany({
|
||||||
|
where: eq(images.flowId, id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageService = new ImageService();
|
||||||
|
for (const img of flowImages) {
|
||||||
|
if (img.alias) {
|
||||||
|
// Image has project alias → keep, unlink from flow
|
||||||
|
await db
|
||||||
|
.update(images)
|
||||||
|
.set({ flowId: null, updatedAt: new Date() })
|
||||||
|
.where(eq(images.id, img.id));
|
||||||
|
} else {
|
||||||
|
// Image without alias → delete
|
||||||
|
await imageService.hardDelete(img.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete flow record
|
||||||
await db.delete(flows).where(eq(flows.id, id));
|
await db.delete(flows).where(eq(flows.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { eq, desc, count } from 'drizzle-orm';
|
import { eq, desc, count } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { generations, flows } from '@banatie/database';
|
import { generations, flows, images } from '@banatie/database';
|
||||||
import type {
|
import type {
|
||||||
Generation,
|
Generation,
|
||||||
NewGeneration,
|
NewGeneration,
|
||||||
|
|
@ -539,6 +539,11 @@ export class GenerationService {
|
||||||
return await this.getByIdWithRelations(id);
|
return await this.getByIdWithRelations(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditional delete for generation (Section 7.2)
|
||||||
|
* - If output image WITHOUT project alias → delete image + generation
|
||||||
|
* - If output image WITH project alias → keep image, delete generation only, set generationId=NULL
|
||||||
|
*/
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
const generation = await this.getById(id);
|
const generation = await this.getById(id);
|
||||||
if (!generation) {
|
if (!generation) {
|
||||||
|
|
@ -546,9 +551,25 @@ export class GenerationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (generation.outputImageId) {
|
if (generation.outputImageId) {
|
||||||
await this.imageService.softDelete(generation.outputImageId);
|
// Get the output image to check if it has a project alias
|
||||||
|
const outputImage = await this.imageService.getById(generation.outputImageId);
|
||||||
|
|
||||||
|
if (outputImage) {
|
||||||
|
if (outputImage.alias) {
|
||||||
|
// Case 2: Image has project alias → keep image, delete generation only
|
||||||
|
// Set generationId = NULL in image record
|
||||||
|
await db
|
||||||
|
.update(images)
|
||||||
|
.set({ generationId: null, updatedAt: new Date() })
|
||||||
|
.where(eq(images.id, outputImage.id));
|
||||||
|
} else {
|
||||||
|
// Case 1: Image has no alias → delete both image and generation
|
||||||
|
await this.imageService.hardDelete(generation.outputImageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete generation record (hard delete)
|
||||||
await db.delete(generations).where(eq(generations.id, id));
|
await db.delete(generations).where(eq(generations.id, id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { eq, and, isNull, desc, count, inArray } from 'drizzle-orm';
|
import { eq, and, isNull, desc, count, inArray, sql } from 'drizzle-orm';
|
||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { images, flows } from '@banatie/database';
|
import { images, flows, generations } 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';
|
||||||
import { AliasService } from './AliasService';
|
import { AliasService } from './AliasService';
|
||||||
|
import { StorageFactory } from '../StorageFactory';
|
||||||
|
|
||||||
export class ImageService {
|
export class ImageService {
|
||||||
private aliasService: AliasService;
|
private aliasService: AliasService;
|
||||||
|
|
@ -136,8 +137,96 @@ export class ImageService {
|
||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hard delete image with MinIO cleanup and cascades (Section 7.1)
|
||||||
|
* 1. Delete physical file from MinIO storage
|
||||||
|
* 2. Delete record from images table (hard delete)
|
||||||
|
* 3. Cascade: set outputImageId = NULL in related generations
|
||||||
|
* 4. Cascade: remove alias entries from flow.aliases
|
||||||
|
* 5. Cascade: remove imageId from generation.referencedImages arrays
|
||||||
|
*/
|
||||||
async hardDelete(id: string): Promise<void> {
|
async hardDelete(id: string): Promise<void> {
|
||||||
|
// Get image to retrieve storage info
|
||||||
|
const image = await this.getById(id, true); // Include deleted
|
||||||
|
if (!image) {
|
||||||
|
throw new Error(ERROR_MESSAGES.IMAGE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Delete physical file from MinIO storage
|
||||||
|
const storageService = await StorageFactory.getInstance();
|
||||||
|
const storageParts = image.storageKey.split('/');
|
||||||
|
|
||||||
|
if (storageParts.length >= 4) {
|
||||||
|
const orgId = storageParts[0]!;
|
||||||
|
const projectId = storageParts[1]!;
|
||||||
|
const category = storageParts[2]! as 'uploads' | 'generated' | 'references';
|
||||||
|
const filename = storageParts.slice(3).join('/');
|
||||||
|
|
||||||
|
await storageService.deleteFile(orgId, projectId, category, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Cascade: Set outputImageId = NULL in related generations
|
||||||
|
await db
|
||||||
|
.update(generations)
|
||||||
|
.set({ outputImageId: null })
|
||||||
|
.where(eq(generations.outputImageId, id));
|
||||||
|
|
||||||
|
// 3. Cascade: Remove alias entries from flow.aliases where this imageId is referenced
|
||||||
|
const allFlows = await db.query.flows.findMany();
|
||||||
|
for (const flow of allFlows) {
|
||||||
|
const aliases = (flow.aliases as Record<string, string>) || {};
|
||||||
|
let modified = false;
|
||||||
|
|
||||||
|
// Remove all entries where value equals this imageId
|
||||||
|
const newAliases: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(aliases)) {
|
||||||
|
if (value !== id) {
|
||||||
|
newAliases[key] = value;
|
||||||
|
} else {
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified) {
|
||||||
|
await db
|
||||||
|
.update(flows)
|
||||||
|
.set({ aliases: newAliases, updatedAt: new Date() })
|
||||||
|
.where(eq(flows.id, flow.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Cascade: Remove imageId from generation.referencedImages JSON arrays
|
||||||
|
const affectedGenerations = await db.query.generations.findMany({
|
||||||
|
where: sql`${generations.referencedImages}::jsonb @> ${JSON.stringify([{ imageId: id }])}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const gen of affectedGenerations) {
|
||||||
|
const refs = (gen.referencedImages as Array<{ imageId: string; alias: string }>) || [];
|
||||||
|
const filtered = refs.filter(ref => ref.imageId !== id);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(generations)
|
||||||
|
.set({ referencedImages: filtered })
|
||||||
|
.where(eq(generations.id, gen.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Delete record from images table
|
||||||
await db.delete(images).where(eq(images.id, id));
|
await db.delete(images).where(eq(images.id, id));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// If MinIO delete fails, still proceed with DB cleanup (MVP mindset)
|
||||||
|
// Log error but don't throw
|
||||||
|
console.error('MinIO delete failed, proceeding with DB cleanup:', error);
|
||||||
|
|
||||||
|
// Still perform DB cleanup
|
||||||
|
await db
|
||||||
|
.update(generations)
|
||||||
|
.set({ outputImageId: null })
|
||||||
|
.where(eq(generations.outputImageId, id));
|
||||||
|
|
||||||
|
await db.delete(images).where(eq(images.id, id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async assignProjectAlias(imageId: string, alias: string): Promise<Image> {
|
async assignProjectAlias(imageId: string, alias: string): Promise<Image> {
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export interface ListImagesQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateImageRequest {
|
export interface UpdateImageRequest {
|
||||||
alias?: string;
|
// Removed alias (Section 6.1) - use PUT /images/:id/alias instead
|
||||||
focalPoint?: {
|
focalPoint?: {
|
||||||
x: number; // 0.0 to 1.0
|
x: number; // 0.0 to 1.0
|
||||||
y: number; // 0.0 to 1.0
|
y: number; // 0.0 to 1.0
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@ export interface GenerationResponse {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
flowId: string | null;
|
flowId: string | null;
|
||||||
originalPrompt: string;
|
prompt: string; // Prompt actually used for generation
|
||||||
enhancedPrompt: string | null;
|
originalPrompt: string | null; // User's original (nullable, only if enhanced)
|
||||||
aspectRatio: string | null;
|
aspectRatio: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
|
|
@ -98,7 +98,7 @@ export type GetImageResponse = ApiResponse<ImageResponse>;
|
||||||
export type ListImagesResponse = PaginatedResponse<ImageResponse>;
|
export type ListImagesResponse = PaginatedResponse<ImageResponse>;
|
||||||
export type ResolveAliasResponse = ApiResponse<AliasResolutionResponse>;
|
export type ResolveAliasResponse = ApiResponse<AliasResolutionResponse>;
|
||||||
export type UpdateImageResponse = ApiResponse<ImageResponse>;
|
export type UpdateImageResponse = ApiResponse<ImageResponse>;
|
||||||
export type DeleteImageResponse = ApiResponse<{ id: string; deletedAt: string | null }>;
|
export type DeleteImageResponse = ApiResponse<{ id: string }>; // Hard delete, no deletedAt
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// FLOW RESPONSES
|
// FLOW RESPONSES
|
||||||
|
|
@ -217,8 +217,8 @@ export const toGenerationResponse = (gen: GenerationWithRelations): GenerationRe
|
||||||
id: gen.id,
|
id: gen.id,
|
||||||
projectId: gen.projectId,
|
projectId: gen.projectId,
|
||||||
flowId: gen.flowId,
|
flowId: gen.flowId,
|
||||||
originalPrompt: gen.originalPrompt,
|
prompt: gen.prompt, // Prompt actually used
|
||||||
enhancedPrompt: gen.enhancedPrompt,
|
originalPrompt: gen.originalPrompt, // User's original (null if not enhanced)
|
||||||
aspectRatio: gen.aspectRatio,
|
aspectRatio: gen.aspectRatio,
|
||||||
status: gen.status,
|
status: gen.status,
|
||||||
errorMessage: gen.errorMessage,
|
errorMessage: gen.errorMessage,
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,22 @@ export const ERROR_MESSAGES = {
|
||||||
// Resource Limits
|
// Resource Limits
|
||||||
MAX_REFERENCE_IMAGES_EXCEEDED: 'Maximum number of reference images exceeded',
|
MAX_REFERENCE_IMAGES_EXCEEDED: 'Maximum number of reference images exceeded',
|
||||||
MAX_FILE_SIZE_EXCEEDED: 'File size exceeds maximum allowed size',
|
MAX_FILE_SIZE_EXCEEDED: 'File size exceeds maximum allowed size',
|
||||||
MAX_RETRY_COUNT_EXCEEDED: 'Maximum retry count exceeded',
|
|
||||||
RATE_LIMIT_EXCEEDED: 'Rate limit exceeded',
|
RATE_LIMIT_EXCEEDED: 'Rate limit exceeded',
|
||||||
MAX_ALIASES_EXCEEDED: 'Maximum number of aliases per flow exceeded',
|
MAX_ALIASES_EXCEEDED: 'Maximum number of aliases per flow exceeded',
|
||||||
|
|
||||||
// Generation Errors
|
// Generation Errors
|
||||||
GENERATION_FAILED: 'Image generation failed',
|
GENERATION_FAILED: 'Image generation failed',
|
||||||
GENERATION_ALREADY_SUCCEEDED: 'Cannot retry a generation that already succeeded',
|
|
||||||
GENERATION_PENDING: 'Generation is still pending',
|
GENERATION_PENDING: 'Generation is still pending',
|
||||||
REFERENCE_IMAGE_RESOLUTION_FAILED: 'Failed to resolve reference image alias',
|
REFERENCE_IMAGE_RESOLUTION_FAILED: 'Failed to resolve reference image alias',
|
||||||
|
|
||||||
|
// Live Scope Errors
|
||||||
|
SCOPE_INVALID_FORMAT: 'Live scope format is invalid',
|
||||||
|
SCOPE_CREATION_DISABLED: 'Creation of new live scopes is disabled for this project',
|
||||||
|
SCOPE_GENERATION_LIMIT_EXCEEDED: 'Live scope generation limit exceeded',
|
||||||
|
|
||||||
|
// Storage Errors
|
||||||
|
STORAGE_DELETE_FAILED: 'Failed to delete file from storage',
|
||||||
|
|
||||||
// Flow Errors
|
// Flow Errors
|
||||||
TECHNICAL_ALIAS_REQUIRES_FLOW: 'Technical aliases (@last, @first, @upload) require a flowId',
|
TECHNICAL_ALIAS_REQUIRES_FLOW: 'Technical aliases (@last, @first, @upload) require a flowId',
|
||||||
FLOW_HAS_NO_GENERATIONS: 'Flow has no generations',
|
FLOW_HAS_NO_GENERATIONS: 'Flow has no generations',
|
||||||
|
|
@ -77,16 +83,22 @@ export const ERROR_CODES = {
|
||||||
RESOURCE_LIMIT_EXCEEDED: 'RESOURCE_LIMIT_EXCEEDED',
|
RESOURCE_LIMIT_EXCEEDED: 'RESOURCE_LIMIT_EXCEEDED',
|
||||||
MAX_REFERENCE_IMAGES_EXCEEDED: 'MAX_REFERENCE_IMAGES_EXCEEDED',
|
MAX_REFERENCE_IMAGES_EXCEEDED: 'MAX_REFERENCE_IMAGES_EXCEEDED',
|
||||||
MAX_FILE_SIZE_EXCEEDED: 'MAX_FILE_SIZE_EXCEEDED',
|
MAX_FILE_SIZE_EXCEEDED: 'MAX_FILE_SIZE_EXCEEDED',
|
||||||
MAX_RETRY_COUNT_EXCEEDED: 'MAX_RETRY_COUNT_EXCEEDED',
|
|
||||||
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
|
RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
|
||||||
MAX_ALIASES_EXCEEDED: 'MAX_ALIASES_EXCEEDED',
|
MAX_ALIASES_EXCEEDED: 'MAX_ALIASES_EXCEEDED',
|
||||||
|
|
||||||
// Generation Errors
|
// Generation Errors
|
||||||
GENERATION_FAILED: 'GENERATION_FAILED',
|
GENERATION_FAILED: 'GENERATION_FAILED',
|
||||||
GENERATION_ALREADY_SUCCEEDED: 'GENERATION_ALREADY_SUCCEEDED',
|
|
||||||
GENERATION_PENDING: 'GENERATION_PENDING',
|
GENERATION_PENDING: 'GENERATION_PENDING',
|
||||||
REFERENCE_IMAGE_RESOLUTION_FAILED: 'REFERENCE_IMAGE_RESOLUTION_FAILED',
|
REFERENCE_IMAGE_RESOLUTION_FAILED: 'REFERENCE_IMAGE_RESOLUTION_FAILED',
|
||||||
|
|
||||||
|
// Live Scope Errors
|
||||||
|
SCOPE_INVALID_FORMAT: 'SCOPE_INVALID_FORMAT',
|
||||||
|
SCOPE_CREATION_DISABLED: 'SCOPE_CREATION_DISABLED',
|
||||||
|
SCOPE_GENERATION_LIMIT_EXCEEDED: 'SCOPE_GENERATION_LIMIT_EXCEEDED',
|
||||||
|
|
||||||
|
// Storage Errors
|
||||||
|
STORAGE_DELETE_FAILED: 'STORAGE_DELETE_FAILED',
|
||||||
|
|
||||||
// Flow Errors
|
// Flow Errors
|
||||||
TECHNICAL_ALIAS_REQUIRES_FLOW: 'TECHNICAL_ALIAS_REQUIRES_FLOW',
|
TECHNICAL_ALIAS_REQUIRES_FLOW: 'TECHNICAL_ALIAS_REQUIRES_FLOW',
|
||||||
FLOW_HAS_NO_GENERATIONS: 'FLOW_HAS_NO_GENERATIONS',
|
FLOW_HAS_NO_GENERATIONS: 'FLOW_HAS_NO_GENERATIONS',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue