feat: add project CRUD, OpenAPI import/parsing, module and endpoint management routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
152
packages/server/src/routes/projects.ts
Normal file
152
packages/server/src/routes/projects.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Router, type Router as RouterType } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { prisma } from '@agent-fox/shared';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
import { generateApiKey } from '../lib/api-key.js';
|
||||
import { parseOpenApiDocument } from '../services/openapi-parser.js';
|
||||
|
||||
const router: RouterType = Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const { spec, specUrl } = req.body;
|
||||
if (!spec && !specUrl) {
|
||||
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Provide spec (JSON object) or specUrl (URL string)' } });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const input = specUrl || spec;
|
||||
const parsed = await parseOpenApiDocument(input);
|
||||
const { raw: apiKey, hash: apiKeyHash } = generateApiKey();
|
||||
|
||||
const project = await prisma.$transaction(async (tx) => {
|
||||
const proj = await tx.project.create({
|
||||
data: {
|
||||
userId: req.user!.userId,
|
||||
name: parsed.name,
|
||||
description: parsed.description,
|
||||
baseUrl: parsed.baseUrl,
|
||||
openApiSpec: parsed.spec as any,
|
||||
openApiVersion: parsed.openApiVersion,
|
||||
apiKeyHash,
|
||||
},
|
||||
});
|
||||
|
||||
const moduleIdMap = new Map<string, string>();
|
||||
for (let i = 0; i < parsed.modules.length; i++) {
|
||||
const mod = parsed.modules[i];
|
||||
const created = await tx.module.create({
|
||||
data: {
|
||||
projectId: proj.id, name: mod.name, description: mod.description,
|
||||
sortOrder: i, source: mod.source,
|
||||
},
|
||||
});
|
||||
moduleIdMap.set(mod.name, created.id);
|
||||
}
|
||||
|
||||
for (const ep of parsed.endpoints) {
|
||||
const moduleId = moduleIdMap.get(ep.moduleName);
|
||||
if (!moduleId) continue;
|
||||
await tx.endpoint.create({
|
||||
data: {
|
||||
projectId: proj.id, moduleId, method: ep.method, path: ep.path,
|
||||
summary: ep.summary, description: ep.description, operationId: ep.operationId,
|
||||
parameters: ep.parameters as any, requestBody: ep.requestBody as any,
|
||||
responses: ep.responses as any, tags: ep.tags, deprecated: ep.deprecated,
|
||||
},
|
||||
});
|
||||
}
|
||||
return proj;
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
project: { id: project.id, name: project.name },
|
||||
apiKey,
|
||||
stats: { modules: parsed.modules.length, endpoints: parsed.endpoints.length },
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to parse OpenAPI document';
|
||||
res.status(400).json({ success: false, error: { code: 'PARSE_ERROR', message } });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const projects = await prisma.project.findMany({
|
||||
where: { userId: req.user!.userId },
|
||||
include: { _count: { select: { endpoints: true, modules: true } } },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
res.json({ success: true, data: projects });
|
||||
});
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
const project = await prisma.project.findFirst({
|
||||
where: { id: req.params.id, userId: req.user!.userId },
|
||||
include: {
|
||||
modules: {
|
||||
include: { _count: { select: { endpoints: true } } },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
_count: { select: { endpoints: true } },
|
||||
},
|
||||
});
|
||||
if (!project) {
|
||||
res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'Project not found' } });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: project });
|
||||
});
|
||||
|
||||
const updateSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
baseUrl: z.string().url().optional(),
|
||||
});
|
||||
|
||||
router.put('/:id', async (req, res) => {
|
||||
const parsed = updateSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: parsed.error.issues[0].message } });
|
||||
return;
|
||||
}
|
||||
const project = await prisma.project.updateMany({
|
||||
where: { id: req.params.id, userId: req.user!.userId },
|
||||
data: parsed.data,
|
||||
});
|
||||
if (project.count === 0) {
|
||||
res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'Project not found' } });
|
||||
return;
|
||||
}
|
||||
const updated = await prisma.project.findUnique({ where: { id: req.params.id } });
|
||||
res.json({ success: true, data: updated });
|
||||
});
|
||||
|
||||
router.delete('/:id', async (req, res) => {
|
||||
const result = await prisma.project.deleteMany({
|
||||
where: { id: req.params.id, userId: req.user!.userId },
|
||||
});
|
||||
if (result.count === 0) {
|
||||
res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'Project not found' } });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: { deleted: true } });
|
||||
});
|
||||
|
||||
router.post('/:id/api-key/rotate', async (req, res) => {
|
||||
const { raw, hash } = generateApiKey();
|
||||
const result = await prisma.project.updateMany({
|
||||
where: { id: req.params.id, userId: req.user!.userId },
|
||||
data: { apiKeyHash: hash },
|
||||
});
|
||||
if (result.count === 0) {
|
||||
res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'Project not found' } });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: { apiKey: raw } });
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user