feature/api-development #1
|
|
@ -0,0 +1,378 @@
|
|||
import { Response, Router } from 'express';
|
||||
import type { Router as RouterType } from 'express';
|
||||
import { FlowService } from '@/services/core';
|
||||
import { asyncHandler } from '@/middleware/errorHandler';
|
||||
import { validateApiKey } from '@/middleware/auth/validateApiKey';
|
||||
import { requireProjectKey } from '@/middleware/auth/requireProjectKey';
|
||||
import { validateAndNormalizePagination } from '@/utils/validators';
|
||||
import { buildPaginatedResponse } from '@/utils/helpers';
|
||||
import { toFlowResponse, toGenerationResponse, toImageResponse } from '@/types/responses';
|
||||
import type {
|
||||
CreateFlowResponse,
|
||||
ListFlowsResponse,
|
||||
GetFlowResponse,
|
||||
UpdateFlowAliasesResponse,
|
||||
ListFlowGenerationsResponse,
|
||||
ListFlowImagesResponse,
|
||||
} from '@/types/responses';
|
||||
|
||||
export const flowsRouter: RouterType = Router();
|
||||
|
||||
let flowService: FlowService;
|
||||
|
||||
const getFlowService = (): FlowService => {
|
||||
if (!flowService) {
|
||||
flowService = new FlowService();
|
||||
}
|
||||
return flowService;
|
||||
};
|
||||
|
||||
/**
|
||||
* POST /api/v1/flows
|
||||
* Create a new flow for organizing generation chains
|
||||
*/
|
||||
flowsRouter.post(
|
||||
'/',
|
||||
validateApiKey,
|
||||
requireProjectKey,
|
||||
asyncHandler(async (req: any, res: Response<CreateFlowResponse>) => {
|
||||
const service = getFlowService();
|
||||
const { meta } = req.body;
|
||||
|
||||
const projectId = req.apiKey.projectId;
|
||||
|
||||
const flow = await service.create({
|
||||
projectId,
|
||||
aliases: {},
|
||||
meta: meta || {},
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: toFlowResponse(flow),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/v1/flows
|
||||
* List all flows for a project with pagination
|
||||
*/
|
||||
flowsRouter.get(
|
||||
'/',
|
||||
validateApiKey,
|
||||
requireProjectKey,
|
||||
asyncHandler(async (req: any, res: Response<ListFlowsResponse>) => {
|
||||
const service = getFlowService();
|
||||
const { limit, offset } = req.query;
|
||||
|
||||
const paginationResult = validateAndNormalizePagination(limit, offset);
|
||||
if (!paginationResult.valid) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { total: 0, limit: 20, offset: 0, hasMore: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { limit: validatedLimit, offset: validatedOffset } = paginationResult.params!;
|
||||
const projectId = req.apiKey.projectId;
|
||||
|
||||
const result = await service.list(
|
||||
{ projectId },
|
||||
validatedLimit,
|
||||
validatedOffset
|
||||
);
|
||||
|
||||
const responseData = result.flows.map((flow) => toFlowResponse(flow));
|
||||
|
||||
res.json(
|
||||
buildPaginatedResponse(responseData, result.total, validatedLimit, validatedOffset)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/v1/flows/:id
|
||||
* Get a single flow by ID with computed counts
|
||||
*/
|
||||
flowsRouter.get(
|
||||
'/:id',
|
||||
validateApiKey,
|
||||
requireProjectKey,
|
||||
asyncHandler(async (req: any, res: Response<GetFlowResponse>) => {
|
||||
const service = getFlowService();
|
||||
const { id } = req.params;
|
||||
|
||||
const flow = await service.getByIdWithCounts(id);
|
||||
|
||||
if (flow.projectId !== req.apiKey.projectId) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Flow not found',
|
||||
code: 'FLOW_NOT_FOUND',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: toFlowResponse(flow),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/v1/flows/:id/generations
|
||||
* List all generations in a flow with pagination
|
||||
*/
|
||||
flowsRouter.get(
|
||||
'/:id/generations',
|
||||
validateApiKey,
|
||||
requireProjectKey,
|
||||
asyncHandler(async (req: any, res: Response<ListFlowGenerationsResponse>) => {
|
||||
const service = getFlowService();
|
||||
const { id } = req.params;
|
||||
const { limit, offset } = req.query;
|
||||
|
||||
const flow = await service.getById(id);
|
||||
if (!flow) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { total: 0, limit: 20, offset: 0, hasMore: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (flow.projectId !== req.apiKey.projectId) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { total: 0, limit: 20, offset: 0, hasMore: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const paginationResult = validateAndNormalizePagination(limit, offset);
|
||||
if (!paginationResult.valid) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { total: 0, limit: 20, offset: 0, hasMore: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { limit: validatedLimit, offset: validatedOffset } = paginationResult.params!;
|
||||
|
||||
const result = await service.getFlowGenerations(id, validatedLimit, validatedOffset);
|
||||
|
||||
const responseData = result.generations.map((gen) => toGenerationResponse(gen));
|
||||
|
||||
res.json(
|
||||
buildPaginatedResponse(responseData, result.total, validatedLimit, validatedOffset)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/v1/flows/:id/images
|
||||
* List all images in a flow with pagination
|
||||
*/
|
||||
flowsRouter.get(
|
||||
'/:id/images',
|
||||
validateApiKey,
|
||||
requireProjectKey,
|
||||
asyncHandler(async (req: any, res: Response<ListFlowImagesResponse>) => {
|
||||
const service = getFlowService();
|
||||
const { id } = req.params;
|
||||
const { limit, offset } = req.query;
|
||||
|
||||
const flow = await service.getById(id);
|
||||
if (!flow) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { total: 0, limit: 20, offset: 0, hasMore: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (flow.projectId !== req.apiKey.projectId) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { total: 0, limit: 20, offset: 0, hasMore: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const paginationResult = validateAndNormalizePagination(limit, offset);
|
||||
if (!paginationResult.valid) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
data: [],
|
||||
pagination: { total: 0, limit: 20, offset: 0, hasMore: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { limit: validatedLimit, offset: validatedOffset } = paginationResult.params!;
|
||||
|
||||
const result = await service.getFlowImages(id, validatedLimit, validatedOffset);
|
||||
|
||||
const responseData = result.images.map((img) => toImageResponse(img));
|
||||
|
||||
res.json(
|
||||
buildPaginatedResponse(responseData, result.total, validatedLimit, validatedOffset)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* PUT /api/v1/flows/:id/aliases
|
||||
* Update aliases in a flow (add or update existing aliases)
|
||||
*/
|
||||
flowsRouter.put(
|
||||
'/:id/aliases',
|
||||
validateApiKey,
|
||||
requireProjectKey,
|
||||
asyncHandler(async (req: any, res: Response<UpdateFlowAliasesResponse>) => {
|
||||
const service = getFlowService();
|
||||
const { id } = req.params;
|
||||
const { aliases } = req.body;
|
||||
|
||||
if (!aliases || typeof aliases !== 'object' || Array.isArray(aliases)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Aliases must be an object with key-value pairs',
|
||||
code: 'VALIDATION_ERROR',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const flow = await service.getById(id);
|
||||
if (!flow) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Flow not found',
|
||||
code: 'FLOW_NOT_FOUND',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (flow.projectId !== req.apiKey.projectId) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Flow not found',
|
||||
code: 'FLOW_NOT_FOUND',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedFlow = await service.updateAliases(id, aliases);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: toFlowResponse(updatedFlow),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/flows/:id/aliases/:alias
|
||||
* Remove a specific alias from a flow
|
||||
*/
|
||||
flowsRouter.delete(
|
||||
'/:id/aliases/:alias',
|
||||
validateApiKey,
|
||||
requireProjectKey,
|
||||
asyncHandler(async (req: any, res: Response) => {
|
||||
const service = getFlowService();
|
||||
const { id, alias } = req.params;
|
||||
|
||||
const flow = await service.getById(id);
|
||||
if (!flow) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Flow not found',
|
||||
code: 'FLOW_NOT_FOUND',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (flow.projectId !== req.apiKey.projectId) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Flow not found',
|
||||
code: 'FLOW_NOT_FOUND',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedFlow = await service.removeAlias(id, alias);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: toFlowResponse(updatedFlow),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/flows/:id
|
||||
* Delete a flow
|
||||
*/
|
||||
flowsRouter.delete(
|
||||
'/:id',
|
||||
validateApiKey,
|
||||
requireProjectKey,
|
||||
asyncHandler(async (req: any, res: Response) => {
|
||||
const service = getFlowService();
|
||||
const { id } = req.params;
|
||||
|
||||
const flow = await service.getById(id);
|
||||
if (!flow) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Flow not found',
|
||||
code: 'FLOW_NOT_FOUND',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (flow.projectId !== req.apiKey.projectId) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Flow not found',
|
||||
code: 'FLOW_NOT_FOUND',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await service.delete(id);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { id },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { Router } from 'express';
|
||||
import type { Router as RouterType } from 'express';
|
||||
import { generationsRouter } from './generations';
|
||||
import { flowsRouter } from './flows';
|
||||
|
||||
export const v1Router: RouterType = Router();
|
||||
|
||||
// Mount v1 routes
|
||||
v1Router.use('/generations', generationsRouter);
|
||||
v1Router.use('/flows', flowsRouter);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,228 @@
|
|||
import { eq, desc, count } from 'drizzle-orm';
|
||||
import { db } from '@/db';
|
||||
import { flows, generations, images } from '@banatie/database';
|
||||
import type { Flow, NewFlow, FlowFilters, FlowWithCounts } from '@/types/models';
|
||||
import { buildWhereClause, buildEqCondition } from '@/utils/helpers';
|
||||
import { ERROR_MESSAGES } from '@/utils/constants';
|
||||
|
||||
export class FlowService {
|
||||
async create(data: NewFlow): Promise<FlowWithCounts> {
|
||||
const [flow] = await db.insert(flows).values(data).returning();
|
||||
if (!flow) {
|
||||
throw new Error('Failed to create flow record');
|
||||
}
|
||||
|
||||
return {
|
||||
...flow,
|
||||
generationCount: 0,
|
||||
imageCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<Flow | null> {
|
||||
const flow = await db.query.flows.findFirst({
|
||||
where: eq(flows.id, id),
|
||||
});
|
||||
|
||||
return flow || null;
|
||||
}
|
||||
|
||||
async getByIdOrThrow(id: string): Promise<Flow> {
|
||||
const flow = await this.getById(id);
|
||||
if (!flow) {
|
||||
throw new Error(ERROR_MESSAGES.FLOW_NOT_FOUND);
|
||||
}
|
||||
return flow;
|
||||
}
|
||||
|
||||
async getByIdWithCounts(id: string): Promise<FlowWithCounts> {
|
||||
const flow = await this.getByIdOrThrow(id);
|
||||
|
||||
const [genCountResult, imgCountResult] = await Promise.all([
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(generations)
|
||||
.where(eq(generations.flowId, id)),
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(images)
|
||||
.where(eq(images.flowId, id)),
|
||||
]);
|
||||
|
||||
const generationCount = Number(genCountResult[0]?.count || 0);
|
||||
const imageCount = Number(imgCountResult[0]?.count || 0);
|
||||
|
||||
return {
|
||||
...flow,
|
||||
generationCount,
|
||||
imageCount,
|
||||
};
|
||||
}
|
||||
|
||||
async list(
|
||||
filters: FlowFilters,
|
||||
limit: number,
|
||||
offset: number
|
||||
): Promise<{ flows: FlowWithCounts[]; total: number }> {
|
||||
const conditions = [
|
||||
buildEqCondition(flows, 'projectId', filters.projectId),
|
||||
];
|
||||
|
||||
const whereClause = buildWhereClause(conditions);
|
||||
|
||||
const [flowsList, countResult] = await Promise.all([
|
||||
db.query.flows.findMany({
|
||||
where: whereClause,
|
||||
orderBy: [desc(flows.updatedAt)],
|
||||
limit,
|
||||
offset,
|
||||
}),
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(flows)
|
||||
.where(whereClause),
|
||||
]);
|
||||
|
||||
const totalCount = countResult[0]?.count || 0;
|
||||
|
||||
const flowsWithCounts = await Promise.all(
|
||||
flowsList.map(async (flow) => {
|
||||
const [genCountResult, imgCountResult] = await Promise.all([
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(generations)
|
||||
.where(eq(generations.flowId, flow.id)),
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(images)
|
||||
.where(eq(images.flowId, flow.id)),
|
||||
]);
|
||||
|
||||
return {
|
||||
...flow,
|
||||
generationCount: Number(genCountResult[0]?.count || 0),
|
||||
imageCount: Number(imgCountResult[0]?.count || 0),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
flows: flowsWithCounts,
|
||||
total: Number(totalCount),
|
||||
};
|
||||
}
|
||||
|
||||
async updateAliases(
|
||||
id: string,
|
||||
aliasUpdates: Record<string, string>
|
||||
): Promise<FlowWithCounts> {
|
||||
const flow = await this.getByIdOrThrow(id);
|
||||
|
||||
const currentAliases = (flow.aliases as Record<string, string>) || {};
|
||||
const updatedAliases = { ...currentAliases, ...aliasUpdates };
|
||||
|
||||
const [updated] = await db
|
||||
.update(flows)
|
||||
.set({
|
||||
aliases: updatedAliases,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(flows.id, id))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
throw new Error(ERROR_MESSAGES.FLOW_NOT_FOUND);
|
||||
}
|
||||
|
||||
return await this.getByIdWithCounts(id);
|
||||
}
|
||||
|
||||
async removeAlias(id: string, alias: string): Promise<FlowWithCounts> {
|
||||
const flow = await this.getByIdOrThrow(id);
|
||||
|
||||
const currentAliases = (flow.aliases as Record<string, string>) || {};
|
||||
const { [alias]: removed, ...remainingAliases } = currentAliases;
|
||||
|
||||
if (removed === undefined) {
|
||||
throw new Error(`Alias '${alias}' not found in flow`);
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(flows)
|
||||
.set({
|
||||
aliases: remainingAliases,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(flows.id, id))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
throw new Error(ERROR_MESSAGES.FLOW_NOT_FOUND);
|
||||
}
|
||||
|
||||
return await this.getByIdWithCounts(id);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await db.delete(flows).where(eq(flows.id, id));
|
||||
}
|
||||
|
||||
async getFlowGenerations(
|
||||
flowId: string,
|
||||
limit: number,
|
||||
offset: number
|
||||
): Promise<{ generations: any[]; total: number }> {
|
||||
const whereClause = eq(generations.flowId, flowId);
|
||||
|
||||
const [generationsList, countResult] = await Promise.all([
|
||||
db.query.generations.findMany({
|
||||
where: whereClause,
|
||||
orderBy: [desc(generations.createdAt)],
|
||||
limit,
|
||||
offset,
|
||||
with: {
|
||||
outputImage: true,
|
||||
},
|
||||
}),
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(generations)
|
||||
.where(whereClause),
|
||||
]);
|
||||
|
||||
const totalCount = countResult[0]?.count || 0;
|
||||
|
||||
return {
|
||||
generations: generationsList,
|
||||
total: Number(totalCount),
|
||||
};
|
||||
}
|
||||
|
||||
async getFlowImages(
|
||||
flowId: string,
|
||||
limit: number,
|
||||
offset: number
|
||||
): Promise<{ images: any[]; total: number }> {
|
||||
const whereClause = eq(images.flowId, flowId);
|
||||
|
||||
const [imagesList, countResult] = await Promise.all([
|
||||
db.query.images.findMany({
|
||||
where: whereClause,
|
||||
orderBy: [desc(images.createdAt)],
|
||||
limit,
|
||||
offset,
|
||||
}),
|
||||
db
|
||||
.select({ count: count() })
|
||||
.from(images)
|
||||
.where(whereClause),
|
||||
]);
|
||||
|
||||
const totalCount = countResult[0]?.count || 0;
|
||||
|
||||
return {
|
||||
images: imagesList,
|
||||
total: Number(totalCount),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './AliasService';
|
||||
export * from './ImageService';
|
||||
export * from './GenerationService';
|
||||
export * from './FlowService';
|
||||
|
|
|
|||
|
|
@ -122,11 +122,13 @@ export interface FlowWithDetailsResponse extends FlowResponse {
|
|||
}
|
||||
|
||||
export type CreateFlowResponse = ApiResponse<FlowResponse>;
|
||||
export type GetFlowResponse = ApiResponse<FlowWithDetailsResponse>;
|
||||
export type GetFlowResponse = ApiResponse<FlowResponse>;
|
||||
export type ListFlowsResponse = PaginatedResponse<FlowResponse>;
|
||||
export type UpdateFlowAliasesResponse = ApiResponse<FlowResponse>;
|
||||
export type DeleteFlowAliasResponse = ApiResponse<FlowResponse>;
|
||||
export type DeleteFlowResponse = ApiResponse<{ id: string }>;
|
||||
export type ListFlowGenerationsResponse = PaginatedResponse<GenerationResponse>;
|
||||
export type ListFlowImagesResponse = PaginatedResponse<ImageResponse>;
|
||||
|
||||
// ========================================
|
||||
// LIVE GENERATION RESPONSE
|
||||
|
|
|
|||
Loading…
Reference in New Issue