Add API server with routes, services, and middleware

Express server with CRUD routes for agents, goals, issues, projects,
and activity log. Includes validation middleware, structured error
handling, request logging, and health check endpoint with tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-16 13:31:58 -06:00
parent 948e8e8c94
commit c9d7cbfe44
24 changed files with 624 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import { Router } from "express";
import { z } from "zod";
import type { Db } from "@paperclip/db";
import { validate } from "../middleware/validate.js";
import { activityService } from "../services/activity.js";
const createActivitySchema = z.object({
action: z.string().min(1),
entityType: z.string().min(1),
entityId: z.string().uuid(),
agentId: z.string().uuid().optional().nullable(),
details: z.record(z.unknown()).optional().nullable(),
});
export function activityRoutes(db: Db) {
const router = Router();
const svc = activityService(db);
router.get("/", async (req, res) => {
const filters = {
agentId: req.query.agentId as string | undefined,
entityType: req.query.entityType as string | undefined,
entityId: req.query.entityId as string | undefined,
};
const result = await svc.list(filters);
res.json(result);
});
router.post("/", validate(createActivitySchema), async (req, res) => {
const event = await svc.create(req.body);
res.status(201).json(event);
});
return router;
}

View File

@@ -0,0 +1,52 @@
import { Router } from "express";
import type { Db } from "@paperclip/db";
import { createAgentSchema, updateAgentSchema } from "@paperclip/shared";
import { validate } from "../middleware/validate.js";
import { agentService } from "../services/agents.js";
export function agentRoutes(db: Db) {
const router = Router();
const svc = agentService(db);
router.get("/", async (_req, res) => {
const result = await svc.list();
res.json(result);
});
router.get("/:id", async (req, res) => {
const id = req.params.id as string;
const agent = await svc.getById(id);
if (!agent) {
res.status(404).json({ error: "Agent not found" });
return;
}
res.json(agent);
});
router.post("/", validate(createAgentSchema), async (req, res) => {
const agent = await svc.create(req.body);
res.status(201).json(agent);
});
router.patch("/:id", validate(updateAgentSchema), async (req, res) => {
const id = req.params.id as string;
const agent = await svc.update(id, req.body);
if (!agent) {
res.status(404).json({ error: "Agent not found" });
return;
}
res.json(agent);
});
router.delete("/:id", async (req, res) => {
const id = req.params.id as string;
const agent = await svc.remove(id);
if (!agent) {
res.status(404).json({ error: "Agent not found" });
return;
}
res.json(agent);
});
return router;
}

View File

@@ -0,0 +1,52 @@
import { Router } from "express";
import type { Db } from "@paperclip/db";
import { createGoalSchema, updateGoalSchema } from "@paperclip/shared";
import { validate } from "../middleware/validate.js";
import { goalService } from "../services/goals.js";
export function goalRoutes(db: Db) {
const router = Router();
const svc = goalService(db);
router.get("/", async (_req, res) => {
const result = await svc.list();
res.json(result);
});
router.get("/:id", async (req, res) => {
const id = req.params.id as string;
const goal = await svc.getById(id);
if (!goal) {
res.status(404).json({ error: "Goal not found" });
return;
}
res.json(goal);
});
router.post("/", validate(createGoalSchema), async (req, res) => {
const goal = await svc.create(req.body);
res.status(201).json(goal);
});
router.patch("/:id", validate(updateGoalSchema), async (req, res) => {
const id = req.params.id as string;
const goal = await svc.update(id, req.body);
if (!goal) {
res.status(404).json({ error: "Goal not found" });
return;
}
res.json(goal);
});
router.delete("/:id", async (req, res) => {
const id = req.params.id as string;
const goal = await svc.remove(id);
if (!goal) {
res.status(404).json({ error: "Goal not found" });
return;
}
res.json(goal);
});
return router;
}

View File

@@ -0,0 +1,11 @@
import { Router } from "express";
export function healthRoutes() {
const router = Router();
router.get("/", (_req, res) => {
res.json({ status: "ok" });
});
return router;
}

View File

@@ -0,0 +1,6 @@
export { healthRoutes } from "./health.js";
export { agentRoutes } from "./agents.js";
export { projectRoutes } from "./projects.js";
export { issueRoutes } from "./issues.js";
export { goalRoutes } from "./goals.js";
export { activityRoutes } from "./activity.js";

View File

@@ -0,0 +1,52 @@
import { Router } from "express";
import type { Db } from "@paperclip/db";
import { createIssueSchema, updateIssueSchema } from "@paperclip/shared";
import { validate } from "../middleware/validate.js";
import { issueService } from "../services/issues.js";
export function issueRoutes(db: Db) {
const router = Router();
const svc = issueService(db);
router.get("/", async (_req, res) => {
const result = await svc.list();
res.json(result);
});
router.get("/:id", async (req, res) => {
const id = req.params.id as string;
const issue = await svc.getById(id);
if (!issue) {
res.status(404).json({ error: "Issue not found" });
return;
}
res.json(issue);
});
router.post("/", validate(createIssueSchema), async (req, res) => {
const issue = await svc.create(req.body);
res.status(201).json(issue);
});
router.patch("/:id", validate(updateIssueSchema), async (req, res) => {
const id = req.params.id as string;
const issue = await svc.update(id, req.body);
if (!issue) {
res.status(404).json({ error: "Issue not found" });
return;
}
res.json(issue);
});
router.delete("/:id", async (req, res) => {
const id = req.params.id as string;
const issue = await svc.remove(id);
if (!issue) {
res.status(404).json({ error: "Issue not found" });
return;
}
res.json(issue);
});
return router;
}

View File

@@ -0,0 +1,52 @@
import { Router } from "express";
import type { Db } from "@paperclip/db";
import { createProjectSchema, updateProjectSchema } from "@paperclip/shared";
import { validate } from "../middleware/validate.js";
import { projectService } from "../services/projects.js";
export function projectRoutes(db: Db) {
const router = Router();
const svc = projectService(db);
router.get("/", async (_req, res) => {
const result = await svc.list();
res.json(result);
});
router.get("/:id", async (req, res) => {
const id = req.params.id as string;
const project = await svc.getById(id);
if (!project) {
res.status(404).json({ error: "Project not found" });
return;
}
res.json(project);
});
router.post("/", validate(createProjectSchema), async (req, res) => {
const project = await svc.create(req.body);
res.status(201).json(project);
});
router.patch("/:id", validate(updateProjectSchema), async (req, res) => {
const id = req.params.id as string;
const project = await svc.update(id, req.body);
if (!project) {
res.status(404).json({ error: "Project not found" });
return;
}
res.json(project);
});
router.delete("/:id", async (req, res) => {
const id = req.params.id as string;
const project = await svc.remove(id);
if (!project) {
res.status(404).json({ error: "Project not found" });
return;
}
res.json(project);
});
return router;
}