fix: flow creation

This commit is contained in:
Oleg Proskurin 2025-11-23 14:34:54 +07:00
parent 3cd7eb316d
commit 6235736f4f
4 changed files with 109 additions and 15 deletions

View File

@ -1,5 +1,5 @@
import { randomUUID } from 'crypto'; 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 { db } from '@/db';
import { generations, flows, images } from '@banatie/database'; import { generations, flows, images } from '@banatie/database';
import type { import type {
@ -58,17 +58,42 @@ export class GenerationService {
// Merge: manual references first, then auto-detected (remove duplicates) // Merge: manual references first, then auto-detected (remove duplicates)
const allReferences = Array.from(new Set([...manualReferences, ...autoDetectedAliases])); const allReferences = Array.from(new Set([...manualReferences, ...autoDetectedAliases]));
// FlowId logic (Section 10.1): // FlowId logic (Section 10.1 - UPDATED FOR LAZY PATTERN):
// - If undefined (not provided) → generate new UUID // - If undefined → generate UUID for pendingFlowId, flowId = null (lazy)
// - If null (explicitly null) → keep null // - If null → flowId = null, pendingFlowId = null (explicitly no flow)
// - If string (specific value) → use that value // - If string → flowId = string, pendingFlowId = null (use provided, create if needed)
let finalFlowId: string | null; let finalFlowId: string | null;
let pendingFlowId: string | null = null;
if (params.flowId === undefined) { if (params.flowId === undefined) {
finalFlowId = randomUUID(); // Lazy pattern: defer flow creation until needed
} else if (params.flowId === null) { pendingFlowId = randomUUID();
finalFlowId = null; finalFlowId = null;
} else if (params.flowId === null) {
// Explicitly no flow
finalFlowId = null;
pendingFlowId = null;
} else { } else {
// Specific flowId provided - ensure flow exists (eager creation)
finalFlowId = params.flowId; 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): // Prompt semantics (Section 2.1):
@ -80,6 +105,7 @@ export class GenerationService {
const generationRecord: NewGeneration = { const generationRecord: NewGeneration = {
projectId: params.projectId, projectId: params.projectId,
flowId: finalFlowId, flowId: finalFlowId,
pendingFlowId: pendingFlowId,
apiKeyId: params.apiKeyId, apiKeyId: params.apiKeyId,
status: 'pending', status: 'pending',
prompt: usedPrompt, // Prompt actually used for generation prompt: usedPrompt, // Prompt actually used for generation
@ -159,29 +185,41 @@ export class GenerationService {
}); });
// Eager flow creation if flowAlias is provided (Section 4.2) // 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 // Check if flow exists, create if not
const existingFlow = await db.query.flows.findFirst({ const existingFlow = await db.query.flows.findFirst({
where: eq(flows.id, finalFlowId), where: eq(flows.id, flowIdToUse),
}); });
if (!existingFlow) { if (!existingFlow) {
await db.insert(flows).values({ await db.insert(flows).values({
id: finalFlowId, id: flowIdToUse,
projectId: params.projectId, projectId: params.projectId,
aliases: {}, aliases: {},
meta: {}, 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 await db
.update(flows) .update(flows)
.set({ updatedAt: new Date() }) .set({ updatedAt: new Date() })
.where(eq(flows.id, finalFlowId)); .where(eq(flows.id, actualFlowId));
} }
const processingTime = Date.now() - startTime; const processingTime = Date.now() - startTime;
@ -278,6 +316,56 @@ export class GenerationService {
.where(eq(flows.id, flowId)); .where(eq(flows.id, flowId));
} }
private async linkPendingGenerationsToFlow(
flowId: string,
projectId: string
): Promise<void> {
// 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( private async updateStatus(
id: string, id: string,
status: 'pending' | 'processing' | 'success' | 'failed', status: 'pending' | 'processing' | 'success' | 'failed',

View File

@ -245,7 +245,7 @@ export interface ErrorResponse {
export const toGenerationResponse = (gen: GenerationWithRelations): GenerationResponse => ({ export const toGenerationResponse = (gen: GenerationWithRelations): GenerationResponse => ({
id: gen.id, id: gen.id,
projectId: gen.projectId, 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 prompt: gen.prompt, // Prompt actually used
originalPrompt: gen.originalPrompt, // User's original (null if not enhanced) originalPrompt: gen.originalPrompt, // User's original (null if not enhanced)
aspectRatio: gen.aspectRatio, aspectRatio: gen.aspectRatio,

View File

@ -40,6 +40,7 @@ export const generations = pgTable(
.notNull() .notNull()
.references(() => projects.id, { onDelete: 'cascade' }), .references(() => projects.id, { onDelete: 'cascade' }),
flowId: uuid('flow_id').references(() => flows.id, { onDelete: 'set null' }), 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' }), apiKeyId: uuid('api_key_id').references(() => apiKeys.id, { onDelete: 'set null' }),
// Status // Status
@ -127,6 +128,11 @@ export const generations = pgTable(
.on(table.flowId, table.createdAt.desc()) .on(table.flowId, table.createdAt.desc())
.where(sql`${table.flowId} IS NOT NULL`), .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 // Index for output image lookup
outputIdx: index('idx_generations_output').on(table.outputImageId), outputIdx: index('idx_generations_output').on(table.outputImageId),

View File

@ -3,7 +3,7 @@
export const config = { export const config = {
// API Configuration // API Configuration
baseURL: 'http://localhost:3000', baseURL: 'http://localhost:3000',
apiKey: 'bnt_71e7e16732ac5e21f597edc56e99e8c3696e713552ec9d1f44dfeffb2ef7c495', apiKey: 'bnt_727d2f4f72bd03ed96da5278bb971a00cb0a2454d4d70f9748b5c39f3f69d88d',
// Paths // Paths
resultsDir: '../../results', resultsDir: '../../results',