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:
30
server/package.json
Normal file
30
server/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "@paperclip/server",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@paperclip/db": "workspace:*",
|
||||||
|
"@paperclip/shared": "workspace:*",
|
||||||
|
"drizzle-orm": "^0.38.4",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"pino": "^9.6.0",
|
||||||
|
"pino-http": "^10.4.0",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/express-serve-static-core": "^5.0.0",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"vitest": "^3.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
server/src/__tests__/health.test.ts
Normal file
15
server/src/__tests__/health.test.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import express from "express";
|
||||||
|
import request from "supertest";
|
||||||
|
import { healthRoutes } from "../routes/health.js";
|
||||||
|
|
||||||
|
describe("GET /health", () => {
|
||||||
|
const app = express();
|
||||||
|
app.use("/health", healthRoutes());
|
||||||
|
|
||||||
|
it("returns 200 with status ok", async () => {
|
||||||
|
const res = await request(app).get("/health");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toEqual({ status: "ok" });
|
||||||
|
});
|
||||||
|
});
|
||||||
42
server/src/app.ts
Normal file
42
server/src/app.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import express, { Router } from "express";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import type { Db } from "@paperclip/db";
|
||||||
|
import { httpLogger, errorHandler } from "./middleware/index.js";
|
||||||
|
import { healthRoutes } from "./routes/health.js";
|
||||||
|
import { agentRoutes } from "./routes/agents.js";
|
||||||
|
import { projectRoutes } from "./routes/projects.js";
|
||||||
|
import { issueRoutes } from "./routes/issues.js";
|
||||||
|
import { goalRoutes } from "./routes/goals.js";
|
||||||
|
import { activityRoutes } from "./routes/activity.js";
|
||||||
|
|
||||||
|
export function createApp(db: Db, opts: { serveUi: boolean }) {
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(httpLogger);
|
||||||
|
|
||||||
|
// Mount API routes
|
||||||
|
const api = Router();
|
||||||
|
api.use("/health", healthRoutes());
|
||||||
|
api.use("/agents", agentRoutes(db));
|
||||||
|
api.use("/projects", projectRoutes(db));
|
||||||
|
api.use("/issues", issueRoutes(db));
|
||||||
|
api.use("/goals", goalRoutes(db));
|
||||||
|
api.use("/activity", activityRoutes(db));
|
||||||
|
app.use("/api", api);
|
||||||
|
|
||||||
|
// SPA fallback for serving the UI build
|
||||||
|
if (opts.serveUi) {
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const uiDist = path.resolve(__dirname, "../../ui/dist");
|
||||||
|
app.use(express.static(uiDist));
|
||||||
|
app.get("*", (_req, res) => {
|
||||||
|
res.sendFile(path.join(uiDist, "index.html"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(errorHandler);
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
16
server/src/config.ts
Normal file
16
server/src/config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export interface Config {
|
||||||
|
port: number;
|
||||||
|
databaseUrl: string;
|
||||||
|
serveUi: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfig(): Config {
|
||||||
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
|
if (!databaseUrl) throw new Error("DATABASE_URL is required");
|
||||||
|
|
||||||
|
return {
|
||||||
|
port: Number(process.env.PORT) || 3100,
|
||||||
|
databaseUrl,
|
||||||
|
serveUi: process.env.SERVE_UI === "true",
|
||||||
|
};
|
||||||
|
}
|
||||||
12
server/src/index.ts
Normal file
12
server/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createDb } from "@paperclip/db";
|
||||||
|
import { createApp } from "./app.js";
|
||||||
|
import { loadConfig } from "./config.js";
|
||||||
|
import { logger } from "./middleware/logger.js";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const db = createDb(config.databaseUrl);
|
||||||
|
const app = createApp(db, { serveUi: config.serveUi });
|
||||||
|
|
||||||
|
app.listen(config.port, () => {
|
||||||
|
logger.info(`Server listening on :${config.port}`);
|
||||||
|
});
|
||||||
18
server/src/middleware/error-handler.ts
Normal file
18
server/src/middleware/error-handler.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Request, Response, NextFunction } from "express";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
|
export function errorHandler(
|
||||||
|
err: unknown,
|
||||||
|
_req: Request,
|
||||||
|
res: Response,
|
||||||
|
_next: NextFunction,
|
||||||
|
) {
|
||||||
|
if (err instanceof ZodError) {
|
||||||
|
res.status(400).json({ error: "Validation error", details: err.errors });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(err, "Unhandled error");
|
||||||
|
res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
3
server/src/middleware/index.ts
Normal file
3
server/src/middleware/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { logger, httpLogger } from "./logger.js";
|
||||||
|
export { errorHandler } from "./error-handler.js";
|
||||||
|
export { validate } from "./validate.js";
|
||||||
5
server/src/middleware/logger.ts
Normal file
5
server/src/middleware/logger.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import pino from "pino";
|
||||||
|
import { pinoHttp } from "pino-http";
|
||||||
|
|
||||||
|
export const logger = pino();
|
||||||
|
export const httpLogger = pinoHttp({ logger });
|
||||||
9
server/src/middleware/validate.ts
Normal file
9
server/src/middleware/validate.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Request, Response, NextFunction } from "express";
|
||||||
|
import type { ZodSchema } from "zod";
|
||||||
|
|
||||||
|
export function validate(schema: ZodSchema) {
|
||||||
|
return (req: Request, _res: Response, next: NextFunction) => {
|
||||||
|
req.body = schema.parse(req.body);
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
35
server/src/routes/activity.ts
Normal file
35
server/src/routes/activity.ts
Normal 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;
|
||||||
|
}
|
||||||
52
server/src/routes/agents.ts
Normal file
52
server/src/routes/agents.ts
Normal 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;
|
||||||
|
}
|
||||||
52
server/src/routes/goals.ts
Normal file
52
server/src/routes/goals.ts
Normal 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;
|
||||||
|
}
|
||||||
11
server/src/routes/health.ts
Normal file
11
server/src/routes/health.ts
Normal 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;
|
||||||
|
}
|
||||||
6
server/src/routes/index.ts
Normal file
6
server/src/routes/index.ts
Normal 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";
|
||||||
52
server/src/routes/issues.ts
Normal file
52
server/src/routes/issues.ts
Normal 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;
|
||||||
|
}
|
||||||
52
server/src/routes/projects.ts
Normal file
52
server/src/routes/projects.ts
Normal 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;
|
||||||
|
}
|
||||||
42
server/src/services/activity.ts
Normal file
42
server/src/services/activity.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { eq, and, desc } from "drizzle-orm";
|
||||||
|
import type { Db } from "@paperclip/db";
|
||||||
|
import { activityLog } from "@paperclip/db";
|
||||||
|
|
||||||
|
export interface ActivityFilters {
|
||||||
|
agentId?: string;
|
||||||
|
entityType?: string;
|
||||||
|
entityId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activityService(db: Db) {
|
||||||
|
return {
|
||||||
|
list: (filters?: ActivityFilters) => {
|
||||||
|
const conditions = [];
|
||||||
|
|
||||||
|
if (filters?.agentId) {
|
||||||
|
conditions.push(eq(activityLog.agentId, filters.agentId));
|
||||||
|
}
|
||||||
|
if (filters?.entityType) {
|
||||||
|
conditions.push(eq(activityLog.entityType, filters.entityType));
|
||||||
|
}
|
||||||
|
if (filters?.entityId) {
|
||||||
|
conditions.push(eq(activityLog.entityId, filters.entityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = db.select().from(activityLog);
|
||||||
|
|
||||||
|
if (conditions.length > 0) {
|
||||||
|
return query.where(and(...conditions)).orderBy(desc(activityLog.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.orderBy(desc(activityLog.createdAt));
|
||||||
|
},
|
||||||
|
|
||||||
|
create: (data: typeof activityLog.$inferInsert) =>
|
||||||
|
db
|
||||||
|
.insert(activityLog)
|
||||||
|
.values(data)
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
};
|
||||||
|
}
|
||||||
38
server/src/services/agents.ts
Normal file
38
server/src/services/agents.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Db } from "@paperclip/db";
|
||||||
|
import { agents } from "@paperclip/db";
|
||||||
|
|
||||||
|
export function agentService(db: Db) {
|
||||||
|
return {
|
||||||
|
list: () => db.select().from(agents),
|
||||||
|
|
||||||
|
getById: (id: string) =>
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(agents)
|
||||||
|
.where(eq(agents.id, id))
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
|
||||||
|
create: (data: typeof agents.$inferInsert) =>
|
||||||
|
db
|
||||||
|
.insert(agents)
|
||||||
|
.values(data)
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
|
||||||
|
update: (id: string, data: Partial<typeof agents.$inferInsert>) =>
|
||||||
|
db
|
||||||
|
.update(agents)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(eq(agents.id, id))
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
|
||||||
|
remove: (id: string) =>
|
||||||
|
db
|
||||||
|
.delete(agents)
|
||||||
|
.where(eq(agents.id, id))
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
};
|
||||||
|
}
|
||||||
38
server/src/services/goals.ts
Normal file
38
server/src/services/goals.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Db } from "@paperclip/db";
|
||||||
|
import { goals } from "@paperclip/db";
|
||||||
|
|
||||||
|
export function goalService(db: Db) {
|
||||||
|
return {
|
||||||
|
list: () => db.select().from(goals),
|
||||||
|
|
||||||
|
getById: (id: string) =>
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(goals)
|
||||||
|
.where(eq(goals.id, id))
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
|
||||||
|
create: (data: typeof goals.$inferInsert) =>
|
||||||
|
db
|
||||||
|
.insert(goals)
|
||||||
|
.values(data)
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
|
||||||
|
update: (id: string, data: Partial<typeof goals.$inferInsert>) =>
|
||||||
|
db
|
||||||
|
.update(goals)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(eq(goals.id, id))
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
|
||||||
|
remove: (id: string) =>
|
||||||
|
db
|
||||||
|
.delete(goals)
|
||||||
|
.where(eq(goals.id, id))
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
};
|
||||||
|
}
|
||||||
5
server/src/services/index.ts
Normal file
5
server/src/services/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { agentService } from "./agents.js";
|
||||||
|
export { projectService } from "./projects.js";
|
||||||
|
export { issueService } from "./issues.js";
|
||||||
|
export { goalService } from "./goals.js";
|
||||||
|
export { activityService, type ActivityFilters } from "./activity.js";
|
||||||
38
server/src/services/issues.ts
Normal file
38
server/src/services/issues.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Db } from "@paperclip/db";
|
||||||
|
import { issues } from "@paperclip/db";
|
||||||
|
|
||||||
|
export function issueService(db: Db) {
|
||||||
|
return {
|
||||||
|
list: () => db.select().from(issues),
|
||||||
|
|
||||||
|
getById: (id: string) =>
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(issues)
|
||||||
|
.where(eq(issues.id, id))
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
|
||||||
|
create: (data: typeof issues.$inferInsert) =>
|
||||||
|
db
|
||||||
|
.insert(issues)
|
||||||
|
.values(data)
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
|
||||||
|
update: (id: string, data: Partial<typeof issues.$inferInsert>) =>
|
||||||
|
db
|
||||||
|
.update(issues)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(eq(issues.id, id))
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
|
||||||
|
remove: (id: string) =>
|
||||||
|
db
|
||||||
|
.delete(issues)
|
||||||
|
.where(eq(issues.id, id))
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
};
|
||||||
|
}
|
||||||
38
server/src/services/projects.ts
Normal file
38
server/src/services/projects.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { Db } from "@paperclip/db";
|
||||||
|
import { projects } from "@paperclip/db";
|
||||||
|
|
||||||
|
export function projectService(db: Db) {
|
||||||
|
return {
|
||||||
|
list: () => db.select().from(projects),
|
||||||
|
|
||||||
|
getById: (id: string) =>
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(projects)
|
||||||
|
.where(eq(projects.id, id))
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
|
||||||
|
create: (data: typeof projects.$inferInsert) =>
|
||||||
|
db
|
||||||
|
.insert(projects)
|
||||||
|
.values(data)
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
|
||||||
|
update: (id: string, data: Partial<typeof projects.$inferInsert>) =>
|
||||||
|
db
|
||||||
|
.update(projects)
|
||||||
|
.set({ ...data, updatedAt: new Date() })
|
||||||
|
.where(eq(projects.id, id))
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
|
||||||
|
remove: (id: string) =>
|
||||||
|
db
|
||||||
|
.delete(projects)
|
||||||
|
.where(eq(projects.id, id))
|
||||||
|
.returning()
|
||||||
|
.then((rows) => rows[0] ?? null),
|
||||||
|
};
|
||||||
|
}
|
||||||
8
server/tsconfig.json
Normal file
8
server/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
server/vitest.config.ts
Normal file
7
server/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: "node",
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user