fix: upload
This commit is contained in:
parent
fba243cfbd
commit
88cb1f2c61
|
|
@ -1,4 +1,5 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
import sizeOf from 'image-size';
|
||||
import { Response, Router } from 'express';
|
||||
import type { Router as RouterType } from 'express';
|
||||
import { ImageService, AliasService } from '@/services/core';
|
||||
|
|
@ -46,16 +47,16 @@ const getAliasService = (): AliasService => {
|
|||
* Upload a single image file to project storage
|
||||
*
|
||||
* Uploads an image file to MinIO storage and creates a database record with support for:
|
||||
* - Automatic flow creation when flowId is undefined (lazy creation)
|
||||
* - Lazy flow creation using pendingFlowId when flowId is undefined
|
||||
* - Eager flow creation when flowAlias is provided
|
||||
* - Project-scoped alias assignment
|
||||
* - Custom metadata storage
|
||||
* - Multiple file formats (JPEG, PNG, WebP, etc.)
|
||||
*
|
||||
* FlowId behavior:
|
||||
* - undefined (not provided) → generates new UUID for automatic flow creation
|
||||
* - undefined (not provided) → generates pendingFlowId, defers flow creation (lazy)
|
||||
* - null (explicitly null) → no flow association
|
||||
* - string (specific value) → uses provided flow ID
|
||||
* - string (specific value) → uses provided flow ID, creates if needed
|
||||
*
|
||||
* @route POST /api/v1/images/upload
|
||||
* @authentication Project Key required
|
||||
|
|
@ -118,17 +119,42 @@ imagesRouter.post(
|
|||
const projectSlug = req.apiKey.projectSlug;
|
||||
const file = req.file;
|
||||
|
||||
// FlowId logic (Section 10.1 & 5.1):
|
||||
// - If undefined (not provided) → generate new UUID
|
||||
// - If null (explicitly null) → keep null
|
||||
// - If string (specific value) → use that value
|
||||
// FlowId logic (matching GenerationService 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 (flowId === undefined) {
|
||||
finalFlowId = randomUUID();
|
||||
} else if (flowId === null) {
|
||||
// Lazy pattern: defer flow creation until needed
|
||||
pendingFlowId = randomUUID();
|
||||
finalFlowId = null;
|
||||
} else if (flowId === null) {
|
||||
// Explicitly no flow
|
||||
finalFlowId = null;
|
||||
pendingFlowId = null;
|
||||
} else {
|
||||
// Specific flowId provided - ensure flow exists (eager creation)
|
||||
finalFlowId = 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,
|
||||
aliases: {},
|
||||
meta: {},
|
||||
});
|
||||
|
||||
// Link any pending images to this new flow
|
||||
await service.linkPendingImagesToFlow(finalFlowId, projectId);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -155,9 +181,23 @@ imagesRouter.post(
|
|||
return;
|
||||
}
|
||||
|
||||
// Extract image dimensions from uploaded file buffer
|
||||
let width: number | null = null;
|
||||
let height: number | null = null;
|
||||
try {
|
||||
const dimensions = sizeOf(file.buffer);
|
||||
if (dimensions.width && dimensions.height) {
|
||||
width = dimensions.width;
|
||||
height = dimensions.height;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to extract image dimensions:', error);
|
||||
}
|
||||
|
||||
const imageRecord = await service.create({
|
||||
projectId,
|
||||
flowId: finalFlowId,
|
||||
pendingFlowId: pendingFlowId,
|
||||
generationId: null,
|
||||
apiKeyId,
|
||||
storageKey: uploadResult.path!,
|
||||
|
|
@ -168,27 +208,41 @@ imagesRouter.post(
|
|||
source: 'uploaded',
|
||||
alias: alias || null,
|
||||
meta: meta ? JSON.parse(meta) : {},
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
// Eager flow creation if flowAlias is provided (Section 5.1)
|
||||
if (flowAlias && finalFlowId) {
|
||||
// Eager flow creation if flowAlias is provided
|
||||
if (flowAlias) {
|
||||
// Use pendingFlowId if available, otherwise finalFlowId
|
||||
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,
|
||||
aliases: {},
|
||||
meta: {},
|
||||
});
|
||||
|
||||
// Link pending images if this was a lazy flow
|
||||
if (pendingFlowId) {
|
||||
await service.linkPendingImagesToFlow(flowIdToUse, projectId);
|
||||
}
|
||||
}
|
||||
|
||||
// Assign flow alias to uploaded image
|
||||
const flow = await db.query.flows.findFirst({
|
||||
where: eq(flows.id, finalFlowId),
|
||||
where: eq(flows.id, flowIdToUse),
|
||||
});
|
||||
|
||||
if (flow) {
|
||||
|
|
@ -199,7 +253,7 @@ imagesRouter.post(
|
|||
await db
|
||||
.update(flows)
|
||||
.set({ aliases: updatedAliases, updatedAt: new Date() })
|
||||
.where(eq(flows.id, finalFlowId));
|
||||
.where(eq(flows.id, flowIdToUse));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -292,4 +292,40 @@ export class ImageService {
|
|||
),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Link all pending images to a flow
|
||||
* Called when flow is created to attach all images with matching pendingFlowId
|
||||
*/
|
||||
async linkPendingImagesToFlow(
|
||||
flowId: string,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
// Find all images with pendingFlowId matching this flowId
|
||||
const pendingImages = await db.query.images.findMany({
|
||||
where: and(
|
||||
eq(images.pendingFlowId, flowId),
|
||||
eq(images.projectId, projectId)
|
||||
),
|
||||
});
|
||||
|
||||
if (pendingImages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update images: set flowId and clear pendingFlowId
|
||||
await db
|
||||
.update(images)
|
||||
.set({
|
||||
flowId: flowId,
|
||||
pendingFlowId: null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(images.pendingFlowId, flowId),
|
||||
eq(images.projectId, projectId)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export const images = pgTable(
|
|||
{ onDelete: 'set null' },
|
||||
),
|
||||
flowId: uuid('flow_id').references(() => flows.id, { onDelete: 'cascade' }),
|
||||
pendingFlowId: text('pending_flow_id'), // Temporary UUID for lazy flow pattern
|
||||
apiKeyId: uuid('api_key_id').references(() => apiKeys.id, { onDelete: 'set null' }),
|
||||
|
||||
// Storage (MinIO path format: orgSlug/projectSlug/category/YYYY-MM/filename.ext)
|
||||
|
|
@ -119,6 +120,11 @@ export const images = pgTable(
|
|||
.on(table.flowId)
|
||||
.where(sql`${table.flowId} IS NOT NULL`),
|
||||
|
||||
// Index for pending flow lookups (lazy pattern)
|
||||
pendingFlowIdx: index('idx_images_pending_flow')
|
||||
.on(table.pendingFlowId, table.createdAt.desc())
|
||||
.where(sql`${table.pendingFlowId} IS NOT NULL`),
|
||||
|
||||
// Index for generation lookup
|
||||
generationIdx: index('idx_images_generation').on(table.generationId),
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ X-API-Key: {{apiKey}}
|
|||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="file"; filename="test-image.png"
|
||||
Content-Disposition: form-data; name="file"; filename="test-image2.png"
|
||||
Content-Type: image/png
|
||||
|
||||
< ./fixture/test-image.png
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
Loading…
Reference in New Issue