import type { FastifyInstance } from "fastify"; import { eq, and } from "drizzle-orm"; import { db } from "../db/index.js"; import { wms, packages, projects } from "../db/schema.js"; export async function wmRoutes(app: FastifyInstance) { // GET /projects/:projectId/wms — list WMs for a project app.get<{ Params: { projectId: string } }>( "/projects/:projectId/wms", { preHandler: [app.authenticate, app.requireAccount], schema: { description: "List all WMs for a project", tags: ["WMs"], security: [{ bearerAuth: [] }], params: { type: "object", properties: { accountId: { type: "string", format: "uuid" }, projectId: { type: "string", format: "uuid" }, }, }, response: { 200: { description: "List of WMs", type: "array", items: { type: "object", properties: { id: { type: "string" }, projectId: { type: "string" }, name: { type: "string" }, packageId: { type: "string", nullable: true }, vcpu: { type: "number" }, ram: { type: "integer" }, disk: { type: "integer" }, createdAt: { type: "string", format: "date-time" }, updatedAt: { type: "string", format: "date-time" }, }, }, }, 404: { description: "Project not found", type: "object", properties: { error: { type: "string" } }, }, }, }, }, async (request, reply) => { // Verify project belongs to the account const [project] = await db .select() .from(projects) .where( and(eq(projects.id, request.params.projectId), eq(projects.accountId, request.accountId)), ); if (!project) { return reply.status(404).send({ error: "Project not found" }); } return db.select().from(wms).where(eq(wms.projectId, request.params.projectId)); }, ); // POST /projects/:projectId/wms — create a WM app.post<{ Params: { projectId: string }; Body: { name: string; packageId?: string; vcpu?: number; ram?: number; disk?: number }; }>( "/projects/:projectId/wms", { preHandler: [app.authenticate, app.requireAccount], schema: { description: "Create a WM from a package or with custom settings. Provide packageId to use a package, or vcpu/ram/disk for custom settings.", tags: ["WMs"], security: [{ bearerAuth: [] }], params: { type: "object", properties: { accountId: { type: "string", format: "uuid" }, projectId: { type: "string", format: "uuid" }, }, }, body: { type: "object", required: ["name"], properties: { name: { type: "string", minLength: 1 }, packageId: { type: "string", format: "uuid" }, vcpu: { type: "number", minimum: 0.25 }, ram: { type: "integer", minimum: 1 }, disk: { type: "integer", minimum: 1 }, }, }, response: { 201: { description: "WM created successfully", type: "object", properties: { id: { type: "string" }, projectId: { type: "string" }, name: { type: "string" }, packageId: { type: "string", nullable: true }, vcpu: { type: "number" }, ram: { type: "integer" }, disk: { type: "integer" }, createdAt: { type: "string", format: "date-time" }, updatedAt: { type: "string", format: "date-time" }, }, }, 400: { description: "Invalid input", type: "object", properties: { error: { type: "string" } }, }, 404: { description: "Project or package not found", type: "object", properties: { error: { type: "string" } }, }, }, }, }, async (request, reply) => { const { name, packageId, vcpu, ram, disk } = request.body; // Verify project belongs to the account const [project] = await db .select() .from(projects) .where( and(eq(projects.id, request.params.projectId), eq(projects.accountId, request.accountId)), ); if (!project) { return reply.status(404).send({ error: "Project not found" }); } let settings: { vcpu: number; ram: number; disk: number; packageId: string | null }; if (packageId) { const [pkg] = await db.select().from(packages).where(eq(packages.id, packageId)); if (!pkg) { return reply.status(404).send({ error: "Package not found" }); } settings = { vcpu: pkg.vcpu, ram: pkg.ram, disk: pkg.disk, packageId: pkg.id }; } else if (vcpu !== undefined && ram !== undefined && disk !== undefined) { settings = { vcpu, ram, disk, packageId: null }; } else { return reply .status(400) .send({ error: "Provide either packageId or all of vcpu, ram, and disk" }); } const [wm] = await db .insert(wms) .values({ projectId: request.params.projectId, name, ...settings, }) .returning(); return reply.status(201).send(wm); }, ); // PATCH /projects/:projectId/wms/:id — update a WM app.patch<{ Params: { projectId: string; id: string }; Body: { name?: string; vcpu?: number; ram?: number; disk?: number }; }>( "/projects/:projectId/wms/:id", { preHandler: [app.authenticate, app.requireAccount], schema: { description: "Update a WM's name or settings", tags: ["WMs"], security: [{ bearerAuth: [] }], params: { type: "object", properties: { accountId: { type: "string", format: "uuid" }, projectId: { type: "string", format: "uuid" }, id: { type: "string", format: "uuid" }, }, }, body: { type: "object", properties: { name: { type: "string", minLength: 1 }, vcpu: { type: "number", minimum: 0.25 }, ram: { type: "integer", minimum: 1 }, disk: { type: "integer", minimum: 1 }, }, }, response: { 200: { description: "WM updated successfully", type: "object", properties: { id: { type: "string" }, projectId: { type: "string" }, name: { type: "string" }, packageId: { type: "string", nullable: true }, vcpu: { type: "number" }, ram: { type: "integer" }, disk: { type: "integer" }, createdAt: { type: "string", format: "date-time" }, updatedAt: { type: "string", format: "date-time" }, }, }, 404: { description: "WM not found", type: "object", properties: { error: { type: "string" } }, }, }, }, }, async (request, reply) => { // Verify project belongs to the account const [project] = await db .select() .from(projects) .where( and(eq(projects.id, request.params.projectId), eq(projects.accountId, request.accountId)), ); if (!project) { return reply.status(404).send({ error: "Project not found" }); } const { name, vcpu, ram, disk } = request.body; const updates: Record = { updatedAt: new Date() }; if (name !== undefined) updates.name = name; if (vcpu !== undefined) updates.vcpu = vcpu; if (ram !== undefined) updates.ram = ram; if (disk !== undefined) updates.disk = disk; const [wm] = await db .update(wms) .set(updates) .where(and(eq(wms.id, request.params.id), eq(wms.projectId, request.params.projectId))) .returning(); if (!wm) { return reply.status(404).send({ error: "WM not found" }); } return wm; }, ); // DELETE /projects/:projectId/wms/:id — delete a WM app.delete<{ Params: { projectId: string; id: string } }>( "/projects/:projectId/wms/:id", { preHandler: [app.authenticate, app.requireAccount], schema: { description: "Delete a WM", tags: ["WMs"], security: [{ bearerAuth: [] }], params: { type: "object", properties: { accountId: { type: "string", format: "uuid" }, projectId: { type: "string", format: "uuid" }, id: { type: "string", format: "uuid" }, }, }, response: { 204: { description: "WM deleted successfully", }, 404: { description: "WM not found", type: "object", properties: { error: { type: "string" } }, }, }, }, }, async (request, reply) => { // Verify project belongs to the account const [project] = await db .select() .from(projects) .where( and(eq(projects.id, request.params.projectId), eq(projects.accountId, request.accountId)), ); if (!project) { return reply.status(404).send({ error: "Project not found" }); } const [deleted] = await db .delete(wms) .where(and(eq(wms.id, request.params.id), eq(wms.projectId, request.params.projectId))) .returning(); if (!deleted) { return reply.status(404).send({ error: "WM not found" }); } return reply.status(204).send(); }, ); }