fix: api & docs
This commit is contained in:
parent
df3737ed44
commit
504b1f8395
|
|
@ -84,27 +84,34 @@ Project is in active development with no existing clients. All changes can be ma
|
||||||
1. **Rename field:** `enhancedPrompt` → `originalPrompt`
|
1. **Rename field:** `enhancedPrompt` → `originalPrompt`
|
||||||
2. **Change field semantics:**
|
2. **Change field semantics:**
|
||||||
- `prompt` - ALWAYS contains the prompt that was used for generation (enhanced or original)
|
- `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:**
|
**Field population logic:**
|
||||||
|
|
||||||
```
|
```
|
||||||
Case 1: autoEnhance = false
|
Case 1: autoEnhance = false
|
||||||
prompt = user input
|
prompt = user input
|
||||||
originalPrompt = NULL
|
originalPrompt = user input (same value, preserved for consistency)
|
||||||
|
|
||||||
Case 2: autoEnhance = true
|
Case 2: autoEnhance = true
|
||||||
prompt = enhanced prompt (used for generation)
|
prompt = enhanced prompt (used for generation)
|
||||||
originalPrompt = user input (preserved)
|
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
|
### 2.2 API Response Format
|
||||||
|
|
||||||
**Response structure:**
|
**Response structure:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"prompt": "detailed enhanced prompt...", // Always the prompt used for generation
|
"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:
|
* Logic:
|
||||||
* - Uses exact same parameters (prompt, aspect ratio, references)
|
* 1. Find the flow by ID
|
||||||
* - Replaces existing output image (preserves ID and URLs)
|
* 2. Query for the most recent generation (ordered by createdAt desc)
|
||||||
* - Returns error if flow has no generations
|
* 3. Trigger regeneration with exact same parameters
|
||||||
* - Ordered by creation date (newest first)
|
* 4. Replace existing output image (preserves ID and URLs)
|
||||||
*
|
*
|
||||||
* @route POST /api/v1/flows/:id/regenerate
|
* @route POST /api/v1/flows/:id/regenerate
|
||||||
* @authentication Project Key required
|
* @authentication Project Key required
|
||||||
* @rateLimit 100 requests per hour per API key
|
* @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} 404 - Flow not found or access denied
|
||||||
* @returns {object} 400 - Flow has no generations
|
* @returns {object} 400 - Flow has no generations
|
||||||
* @returns {object} 401 - Missing or invalid API key
|
* @returns {object} 401 - Missing or invalid API key
|
||||||
* @returns {object} 429 - Rate limit exceeded
|
* @returns {object} 429 - Rate limit exceeded
|
||||||
*
|
*
|
||||||
* @throws {Error} FLOW_NOT_FOUND - Flow does not exist
|
* @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
|
* @example
|
||||||
* POST /api/v1/flows/550e8400-e29b-41d4-a716-446655440000/regenerate
|
* 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:
|
* Permanently removes the flow with cascade behavior:
|
||||||
* - Flow record is hard deleted (no soft delete)
|
* - Flow record is hard deleted
|
||||||
* - Generations remain intact (not cascaded)
|
* - All generations in flow are hard deleted
|
||||||
* - Images remain intact (not cascaded)
|
* - Images WITHOUT project alias: hard deleted with MinIO cleanup
|
||||||
* - Flow-scoped aliases are removed with flow
|
* - 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
|
* @route DELETE /api/v1/flows/:id
|
||||||
* @authentication Project Key required
|
* @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:
|
* Performs deletion with alias protection:
|
||||||
* - Soft delete generation record (sets deletedAt timestamp)
|
* - Hard delete generation record (permanently removed from database)
|
||||||
* - Hard delete output image if no project/flow aliases exist
|
* - If output image has NO project alias: hard delete image with MinIO cleanup
|
||||||
* - Soft delete output image if aliases exist (preserves for CDN access)
|
* - If output image HAS project alias: keep image, set generationId=NULL
|
||||||
* - Cascades to remove generation-image relationships
|
*
|
||||||
|
* 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
|
* @route DELETE /api/v1/generations/:id
|
||||||
* @authentication Project Key required
|
* @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 { db } from '@/db';
|
||||||
import { images, flows } from '@banatie/database';
|
import { images, flows } from '@banatie/database';
|
||||||
import type { AliasResolution, Image } from '@/types/models';
|
import type { AliasResolution, Image } from '@/types/models';
|
||||||
|
|
@ -142,9 +142,11 @@ export class AliasService {
|
||||||
flowId: string,
|
flowId: string,
|
||||||
projectId: string
|
projectId: string
|
||||||
): Promise<Image | undefined> {
|
): 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({
|
return await db.query.images.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(images.flowId, flowId),
|
or(eq(images.flowId, flowId), eq(images.pendingFlowId, flowId)),
|
||||||
eq(images.projectId, projectId),
|
eq(images.projectId, projectId),
|
||||||
eq(images.source, 'generated'),
|
eq(images.source, 'generated'),
|
||||||
isNull(images.deletedAt)
|
isNull(images.deletedAt)
|
||||||
|
|
@ -157,9 +159,10 @@ export class AliasService {
|
||||||
flowId: string,
|
flowId: string,
|
||||||
projectId: string
|
projectId: string
|
||||||
): Promise<Image | undefined> {
|
): Promise<Image | undefined> {
|
||||||
|
// Check both flowId and pendingFlowId to support lazy flow pattern (Section 4.1)
|
||||||
const allImages = await db.query.images.findMany({
|
const allImages = await db.query.images.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
eq(images.flowId, flowId),
|
or(eq(images.flowId, flowId), eq(images.pendingFlowId, flowId)),
|
||||||
eq(images.projectId, projectId),
|
eq(images.projectId, projectId),
|
||||||
eq(images.source, 'generated'),
|
eq(images.source, 'generated'),
|
||||||
isNull(images.deletedAt)
|
isNull(images.deletedAt)
|
||||||
|
|
@ -175,9 +178,10 @@ export class AliasService {
|
||||||
flowId: string,
|
flowId: string,
|
||||||
projectId: string
|
projectId: string
|
||||||
): Promise<Image | undefined> {
|
): Promise<Image | undefined> {
|
||||||
|
// Check both flowId and pendingFlowId to support lazy flow pattern (Section 4.1)
|
||||||
return await db.query.images.findFirst({
|
return await db.query.images.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(images.flowId, flowId),
|
or(eq(images.flowId, flowId), eq(images.pendingFlowId, flowId)),
|
||||||
eq(images.projectId, projectId),
|
eq(images.projectId, projectId),
|
||||||
eq(images.source, 'uploaded'),
|
eq(images.source, 'uploaded'),
|
||||||
isNull(images.deletedAt)
|
isNull(images.deletedAt)
|
||||||
|
|
|
||||||
|
|
@ -215,17 +215,10 @@ export class ImageService {
|
||||||
await db.delete(images).where(eq(images.id, id));
|
await db.delete(images).where(eq(images.id, id));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If MinIO delete fails, still proceed with DB cleanup (MVP mindset)
|
// Per Section 7.4: If MinIO delete fails, do NOT proceed with DB cleanup
|
||||||
// Log error but don't throw
|
// This prevents orphaned files in MinIO
|
||||||
console.error('MinIO delete failed, proceeding with DB cleanup:', error);
|
console.error('MinIO delete failed, aborting image deletion:', error);
|
||||||
|
throw new Error(ERROR_MESSAGES.STORAGE_DELETE_FAILED || 'Failed to delete file from storage');
|
||||||
// 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ export const toGenerationResponse = (gen: GenerationWithRelations): GenerationRe
|
||||||
export const toImageResponse = (img: Image): ImageResponse => ({
|
export const toImageResponse = (img: Image): ImageResponse => ({
|
||||||
id: img.id,
|
id: img.id,
|
||||||
projectId: img.projectId,
|
projectId: img.projectId,
|
||||||
flowId: img.flowId,
|
flowId: img.flowId ?? img.pendingFlowId ?? null, // Return actual flowId or pendingFlowId for client
|
||||||
storageKey: img.storageKey,
|
storageKey: img.storageKey,
|
||||||
storageUrl: img.storageUrl,
|
storageUrl: img.storageUrl,
|
||||||
mimeType: img.mimeType,
|
mimeType: img.mimeType,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue