feature/api-development #1
|
|
@ -84,27 +84,34 @@ Project is in active development with no existing clients. All changes can be ma
|
|||
1. **Rename field:** `enhancedPrompt` → `originalPrompt`
|
||||
2. **Change field semantics:**
|
||||
- `prompt` - ALWAYS contains the prompt that was used for generation (enhanced or original)
|
||||
- `originalPrompt` - contains user's original input ONLY if autoEnhance was used (nullable)
|
||||
- `originalPrompt` - ALWAYS contains user's original input (for transparency and audit trail)
|
||||
|
||||
**Field population logic:**
|
||||
|
||||
```
|
||||
Case 1: autoEnhance = false
|
||||
prompt = user input
|
||||
originalPrompt = NULL
|
||||
originalPrompt = user input (same value, preserved for consistency)
|
||||
|
||||
Case 2: autoEnhance = true
|
||||
prompt = enhanced prompt (used for generation)
|
||||
originalPrompt = user input (preserved)
|
||||
```
|
||||
|
||||
**Rationale:** Always storing `originalPrompt` provides:
|
||||
- Audit trail of user's actual input
|
||||
- Ability to compare original vs enhanced prompts
|
||||
- Consistent API response structure
|
||||
- Simplified client logic (no null checks needed)
|
||||
|
||||
### 2.2 API Response Format
|
||||
|
||||
**Response structure:**
|
||||
```json
|
||||
{
|
||||
"prompt": "detailed enhanced prompt...", // Always the prompt used for generation
|
||||
"originalPrompt": "sunset" // Only present if enhancement was used
|
||||
"originalPrompt": "sunset", // Always the user's original input
|
||||
"autoEnhance": true // True if prompt differs from originalPrompt
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -472,28 +472,28 @@ flowsRouter.delete(
|
|||
);
|
||||
|
||||
/**
|
||||
* Regenerate the most recent generation in a flow
|
||||
* Regenerate the most recent generation in a flow (Section 3.6)
|
||||
*
|
||||
* Identifies the latest generation in the flow and regenerates it:
|
||||
* - Uses exact same parameters (prompt, aspect ratio, references)
|
||||
* - Replaces existing output image (preserves ID and URLs)
|
||||
* - Returns error if flow has no generations
|
||||
* - Ordered by creation date (newest first)
|
||||
* Logic:
|
||||
* 1. Find the flow by ID
|
||||
* 2. Query for the most recent generation (ordered by createdAt desc)
|
||||
* 3. Trigger regeneration with exact same parameters
|
||||
* 4. Replace existing output image (preserves ID and URLs)
|
||||
*
|
||||
* @route POST /api/v1/flows/:id/regenerate
|
||||
* @authentication Project Key required
|
||||
* @rateLimit 100 requests per hour per API key
|
||||
*
|
||||
* @param {string} req.params.id - Flow ID (UUID)
|
||||
* @param {string} req.params.id - Flow ID (affects: determines which flow's latest generation to regenerate)
|
||||
*
|
||||
* @returns {object} 200 - Regenerated generation response
|
||||
* @returns {object} 200 - Regenerated generation with updated output image
|
||||
* @returns {object} 404 - Flow not found or access denied
|
||||
* @returns {object} 400 - Flow has no generations
|
||||
* @returns {object} 401 - Missing or invalid API key
|
||||
* @returns {object} 429 - Rate limit exceeded
|
||||
*
|
||||
* @throws {Error} FLOW_NOT_FOUND - Flow does not exist
|
||||
* @throws {Error} FLOW_HAS_NO_GENERATIONS - Flow contains no generations
|
||||
* @throws {Error} FLOW_HAS_NO_GENERATIONS - Flow contains no generations to regenerate
|
||||
*
|
||||
* @example
|
||||
* POST /api/v1/flows/550e8400-e29b-41d4-a716-446655440000/regenerate
|
||||
|
|
@ -558,15 +558,16 @@ flowsRouter.post(
|
|||
);
|
||||
|
||||
/**
|
||||
* Delete a flow (hard delete)
|
||||
* Delete a flow with cascade deletion (Section 7.3)
|
||||
*
|
||||
* Permanently removes the flow record:
|
||||
* - Flow record is hard deleted (no soft delete)
|
||||
* - Generations remain intact (not cascaded)
|
||||
* - Images remain intact (not cascaded)
|
||||
* - Flow-scoped aliases are removed with flow
|
||||
* Permanently removes the flow with cascade behavior:
|
||||
* - Flow record is hard deleted
|
||||
* - All generations in flow are hard deleted
|
||||
* - Images WITHOUT project alias: hard deleted with MinIO cleanup
|
||||
* - Images WITH project alias: kept, but flowId set to NULL (unlinked)
|
||||
*
|
||||
* Note: Generations and images lose their flow association but remain accessible.
|
||||
* Rationale: Images with project aliases are used globally and should be preserved.
|
||||
* Flow deletion removes the organizational structure but protects important assets.
|
||||
*
|
||||
* @route DELETE /api/v1/flows/:id
|
||||
* @authentication Project Key required
|
||||
|
|
|
|||
|
|
@ -478,13 +478,15 @@ generationsRouter.post(
|
|||
);
|
||||
|
||||
/**
|
||||
* Delete a generation and its output image
|
||||
* Delete a generation and conditionally its output image (Section 7.2)
|
||||
*
|
||||
* Performs deletion with alias protection:
|
||||
* - Soft delete generation record (sets deletedAt timestamp)
|
||||
* - Hard delete output image if no project/flow aliases exist
|
||||
* - Soft delete output image if aliases exist (preserves for CDN access)
|
||||
* - Cascades to remove generation-image relationships
|
||||
* - Hard delete generation record (permanently removed from database)
|
||||
* - If output image has NO project alias: hard delete image with MinIO cleanup
|
||||
* - If output image HAS project alias: keep image, set generationId=NULL
|
||||
*
|
||||
* Rationale: Images with aliases are used as standalone assets and should be preserved.
|
||||
* Images without aliases were created only for this generation and can be deleted together.
|
||||
*
|
||||
* @route DELETE /api/v1/generations/:id
|
||||
* @authentication Project Key required
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { eq, and, isNull, desc } from 'drizzle-orm';
|
||||
import { eq, and, isNull, desc, or } from 'drizzle-orm';
|
||||
import { db } from '@/db';
|
||||
import { images, flows } from '@banatie/database';
|
||||
import type { AliasResolution, Image } from '@/types/models';
|
||||
|
|
@ -142,9 +142,11 @@ export class AliasService {
|
|||
flowId: string,
|
||||
projectId: string
|
||||
): Promise<Image | undefined> {
|
||||
// Check both flowId and pendingFlowId to support lazy flow pattern (Section 4.1)
|
||||
// Images may have pendingFlowId before the flow record is created
|
||||
return await db.query.images.findFirst({
|
||||
where: and(
|
||||
eq(images.flowId, flowId),
|
||||
or(eq(images.flowId, flowId), eq(images.pendingFlowId, flowId)),
|
||||
eq(images.projectId, projectId),
|
||||
eq(images.source, 'generated'),
|
||||
isNull(images.deletedAt)
|
||||
|
|
@ -157,9 +159,10 @@ export class AliasService {
|
|||
flowId: string,
|
||||
projectId: string
|
||||
): Promise<Image | undefined> {
|
||||
// Check both flowId and pendingFlowId to support lazy flow pattern (Section 4.1)
|
||||
const allImages = await db.query.images.findMany({
|
||||
where: and(
|
||||
eq(images.flowId, flowId),
|
||||
or(eq(images.flowId, flowId), eq(images.pendingFlowId, flowId)),
|
||||
eq(images.projectId, projectId),
|
||||
eq(images.source, 'generated'),
|
||||
isNull(images.deletedAt)
|
||||
|
|
@ -175,9 +178,10 @@ export class AliasService {
|
|||
flowId: string,
|
||||
projectId: string
|
||||
): Promise<Image | undefined> {
|
||||
// Check both flowId and pendingFlowId to support lazy flow pattern (Section 4.1)
|
||||
return await db.query.images.findFirst({
|
||||
where: and(
|
||||
eq(images.flowId, flowId),
|
||||
or(eq(images.flowId, flowId), eq(images.pendingFlowId, flowId)),
|
||||
eq(images.projectId, projectId),
|
||||
eq(images.source, 'uploaded'),
|
||||
isNull(images.deletedAt)
|
||||
|
|
|
|||
|
|
@ -215,17 +215,10 @@ export class ImageService {
|
|||
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));
|
||||
// Per Section 7.4: If MinIO delete fails, do NOT proceed with DB cleanup
|
||||
// This prevents orphaned files in MinIO
|
||||
console.error('MinIO delete failed, aborting image deletion:', error);
|
||||
throw new Error(ERROR_MESSAGES.STORAGE_DELETE_FAILED || 'Failed to delete file from storage');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ export const toGenerationResponse = (gen: GenerationWithRelations): GenerationRe
|
|||
export const toImageResponse = (img: Image): ImageResponse => ({
|
||||
id: img.id,
|
||||
projectId: img.projectId,
|
||||
flowId: img.flowId,
|
||||
flowId: img.flowId ?? img.pendingFlowId ?? null, // Return actual flowId or pendingFlowId for client
|
||||
storageKey: img.storageKey,
|
||||
storageUrl: img.storageUrl,
|
||||
mimeType: img.mimeType,
|
||||
|
|
|
|||
Loading…
Reference in New Issue