feature/api-development #1

Merged
usulpro merged 47 commits from feature/api-development into main 2025-11-29 23:03:01 +07:00
4 changed files with 109 additions and 15 deletions
Showing only changes of commit 6235736f4f - Show all commits

View File

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

View File

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

View File

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

View File

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