fix: api & docs

This commit is contained in:
Oleg Proskurin 2025-11-28 00:07:06 +07:00
parent df3737ed44
commit 504b1f8395
6 changed files with 47 additions and 40 deletions

View File

@ -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
}
```

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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');
}
}

View File

@ -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,