feature/api-development #1
|
|
@ -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<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(
|
||||
id: string,
|
||||
status: 'pending' | 'processing' | 'success' | 'failed',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
export const config = {
|
||||
// API Configuration
|
||||
baseURL: 'http://localhost:3000',
|
||||
apiKey: 'bnt_71e7e16732ac5e21f597edc56e99e8c3696e713552ec9d1f44dfeffb2ef7c495',
|
||||
apiKey: 'bnt_727d2f4f72bd03ed96da5278bb971a00cb0a2454d4d70f9748b5c39f3f69d88d',
|
||||
|
||||
// Paths
|
||||
resultsDir: '../../results',
|
||||
|
|
|
|||
Loading…
Reference in New Issue