From 6235736f4fbc5587111c9fad52373daaa675a1a7 Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Sun, 23 Nov 2025 14:34:54 +0700 Subject: [PATCH] fix: flow creation --- .../src/services/core/GenerationService.ts | 114 ++++++++++++++++-- apps/api-service/src/types/responses.ts | 2 +- packages/database/src/schema/generations.ts | 6 + tests/api/config.ts | 2 +- 4 files changed, 109 insertions(+), 15 deletions(-) diff --git a/apps/api-service/src/services/core/GenerationService.ts b/apps/api-service/src/services/core/GenerationService.ts index 80da8ae..f541ee8 100644 --- a/apps/api-service/src/services/core/GenerationService.ts +++ b/apps/api-service/src/services/core/GenerationService.ts @@ -1,5 +1,5 @@ import { randomUUID } from 'crypto'; -import { eq, desc, count } from 'drizzle-orm'; +import { eq, desc, count, and, isNull, inArray } from 'drizzle-orm'; import { db } from '@/db'; import { generations, flows, images } from '@banatie/database'; import type { @@ -58,17 +58,42 @@ export class GenerationService { // Merge: manual references first, then auto-detected (remove duplicates) const allReferences = Array.from(new Set([...manualReferences, ...autoDetectedAliases])); - // FlowId logic (Section 10.1): - // - If undefined (not provided) → generate new UUID - // - If null (explicitly null) → keep null - // - If string (specific value) → use that value + // FlowId logic (Section 10.1 - UPDATED FOR LAZY PATTERN): + // - If undefined → generate UUID for pendingFlowId, flowId = null (lazy) + // - If null → flowId = null, pendingFlowId = null (explicitly no flow) + // - If string → flowId = string, pendingFlowId = null (use provided, create if needed) let finalFlowId: string | null; + let pendingFlowId: string | null = null; + if (params.flowId === undefined) { - finalFlowId = randomUUID(); - } else if (params.flowId === null) { + // Lazy pattern: defer flow creation until needed + pendingFlowId = randomUUID(); finalFlowId = null; + } else if (params.flowId === null) { + // Explicitly no flow + finalFlowId = null; + pendingFlowId = null; } else { + // Specific flowId provided - ensure flow exists (eager creation) finalFlowId = params.flowId; + pendingFlowId = null; + + // 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: params.projectId, + aliases: {}, + meta: {}, + }); + + // Link any pending generations to this new flow + await this.linkPendingGenerationsToFlow(finalFlowId, params.projectId); + } } // Prompt semantics (Section 2.1): @@ -80,6 +105,7 @@ export class GenerationService { const generationRecord: NewGeneration = { projectId: params.projectId, flowId: finalFlowId, + pendingFlowId: pendingFlowId, apiKeyId: params.apiKeyId, status: 'pending', prompt: usedPrompt, // Prompt actually used for generation @@ -159,29 +185,41 @@ export class GenerationService { }); // Eager flow creation if flowAlias is provided (Section 4.2) - if (params.flowAlias && finalFlowId) { + if (params.flowAlias) { + // If we have pendingFlowId, create flow and link pending generations + const flowIdToUse = pendingFlowId || finalFlowId; + + if (!flowIdToUse) { + throw new Error('Cannot create flow: no flowId available'); + } + // Check if flow exists, create if not const existingFlow = await db.query.flows.findFirst({ - where: eq(flows.id, finalFlowId), + where: eq(flows.id, flowIdToUse), }); if (!existingFlow) { await db.insert(flows).values({ - id: finalFlowId, + id: flowIdToUse, projectId: params.projectId, aliases: {}, meta: {}, }); + + // Link any pending generations to this new flow + await this.linkPendingGenerationsToFlow(flowIdToUse, params.projectId); } - await this.assignFlowAlias(finalFlowId, params.flowAlias, imageRecord.id); + await this.assignFlowAlias(flowIdToUse, params.flowAlias, imageRecord.id); } - if (finalFlowId) { + // Update flow timestamp if flow was created (either from finalFlowId or pendingFlowId converted to flow) + const actualFlowId = finalFlowId || (pendingFlowId && params.flowAlias ? pendingFlowId : null); + if (actualFlowId) { await db .update(flows) .set({ updatedAt: new Date() }) - .where(eq(flows.id, finalFlowId)); + .where(eq(flows.id, actualFlowId)); } const processingTime = Date.now() - startTime; @@ -278,6 +316,56 @@ export class GenerationService { .where(eq(flows.id, flowId)); } + private async linkPendingGenerationsToFlow( + flowId: string, + projectId: string + ): Promise { + // Find all generations with pendingFlowId matching this flowId + const pendingGens = await db.query.generations.findMany({ + where: and( + eq(generations.pendingFlowId, flowId), + eq(generations.projectId, projectId) + ), + }); + + if (pendingGens.length === 0) { + return; + } + + // Update generations: set flowId and clear pendingFlowId + await db + .update(generations) + .set({ + flowId: flowId, + pendingFlowId: null, + updatedAt: new Date(), + }) + .where( + and( + eq(generations.pendingFlowId, flowId), + eq(generations.projectId, projectId) + ) + ); + + // Also update associated images to have the flowId + const generationIds = pendingGens.map(g => g.id); + if (generationIds.length > 0) { + await db + .update(images) + .set({ + flowId: flowId, + updatedAt: new Date(), + }) + .where( + and( + eq(images.projectId, projectId), + isNull(images.flowId), + inArray(images.generationId, generationIds) + ) + ); + } + } + private async updateStatus( id: string, status: 'pending' | 'processing' | 'success' | 'failed', diff --git a/apps/api-service/src/types/responses.ts b/apps/api-service/src/types/responses.ts index e298a1f..f99c5b4 100644 --- a/apps/api-service/src/types/responses.ts +++ b/apps/api-service/src/types/responses.ts @@ -245,7 +245,7 @@ export interface ErrorResponse { export const toGenerationResponse = (gen: GenerationWithRelations): GenerationResponse => ({ id: gen.id, projectId: gen.projectId, - flowId: gen.flowId, + flowId: gen.flowId ?? gen.pendingFlowId ?? null, // Return actual flowId or pendingFlowId for client prompt: gen.prompt, // Prompt actually used originalPrompt: gen.originalPrompt, // User's original (null if not enhanced) aspectRatio: gen.aspectRatio, diff --git a/packages/database/src/schema/generations.ts b/packages/database/src/schema/generations.ts index 890f94f..6375853 100644 --- a/packages/database/src/schema/generations.ts +++ b/packages/database/src/schema/generations.ts @@ -40,6 +40,7 @@ export const generations = pgTable( .notNull() .references(() => projects.id, { onDelete: 'cascade' }), flowId: uuid('flow_id').references(() => flows.id, { onDelete: 'set null' }), + pendingFlowId: text('pending_flow_id'), // Temporary UUID for lazy flow pattern apiKeyId: uuid('api_key_id').references(() => apiKeys.id, { onDelete: 'set null' }), // Status @@ -127,6 +128,11 @@ export const generations = pgTable( .on(table.flowId, table.createdAt.desc()) .where(sql`${table.flowId} IS NOT NULL`), + // Index for pending flow-scoped generations (partial index) + pendingFlowIdx: index('idx_generations_pending_flow') + .on(table.pendingFlowId, table.createdAt.desc()) + .where(sql`${table.pendingFlowId} IS NOT NULL`), + // Index for output image lookup outputIdx: index('idx_generations_output').on(table.outputImageId), diff --git a/tests/api/config.ts b/tests/api/config.ts index 61d2640..d3b88b0 100644 --- a/tests/api/config.ts +++ b/tests/api/config.ts @@ -3,7 +3,7 @@ export const config = { // API Configuration baseURL: 'http://localhost:3000', - apiKey: 'bnt_71e7e16732ac5e21f597edc56e99e8c3696e713552ec9d1f44dfeffb2ef7c495', + apiKey: 'bnt_727d2f4f72bd03ed96da5278bb971a00cb0a2454d4d70f9748b5c39f3f69d88d', // Paths resultsDir: '../../results',