added repo

This commit is contained in:
2026-03-03 14:25:43 +00:00
commit 4328ada595
15 changed files with 2118 additions and 0 deletions

223
src/index.ts Normal file
View File

@@ -0,0 +1,223 @@
import type { Database } from "bun:sqlite";
import { getConfig } from "./config";
import { ensureSeededDatabase } from "./database";
import { openApiDocument } from "./openapi";
import {
createTask,
getOrganizationSummary,
getTaskDetail,
listOrganizations,
listProjects,
listTasks,
listUsers,
updateTask,
} from "./repository";
function json(body: unknown, init: ResponseInit = {}) {
const headers = new Headers(init.headers);
headers.set("content-type", "application/json; charset=utf-8");
headers.set("access-control-allow-origin", "*");
headers.set("access-control-allow-methods", "GET,POST,PATCH,OPTIONS");
headers.set("access-control-allow-headers", "content-type");
return new Response(JSON.stringify(body, null, 2), { ...init, headers });
}
function notFound(message: string) {
return json({ error: message }, { status: 404 });
}
function badRequest(message: string) {
return json({ error: message }, { status: 400 });
}
async function readJson(request: Request) {
try {
return await request.json();
} catch {
return null;
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function validateCreateTask(body: unknown) {
if (!isRecord(body)) {
return { error: "Request body must be a JSON object." };
}
if (typeof body.orgId !== "string" || body.orgId.trim() === "") {
return { error: "orgId is required." };
}
if (typeof body.projectId !== "string" || body.projectId.trim() === "") {
return { error: "projectId is required." };
}
if (typeof body.title !== "string" || body.title.trim() === "") {
return { error: "title is required." };
}
return {
value: {
orgId: body.orgId.trim(),
projectId: body.projectId.trim(),
assigneeUserId:
typeof body.assigneeUserId === "string" ? body.assigneeUserId.trim() : null,
title: body.title.trim(),
description:
typeof body.description === "string" ? body.description.trim() : undefined,
status: typeof body.status === "string" ? body.status.trim() : undefined,
priority: typeof body.priority === "string" ? body.priority.trim() : undefined,
storyPoints:
typeof body.storyPoints === "number" ? Math.trunc(body.storyPoints) : undefined,
dueDate: typeof body.dueDate === "string" ? body.dueDate : null,
},
};
}
function validateUpdateTask(body: unknown) {
if (!isRecord(body)) {
return { error: "Request body must be a JSON object." };
}
return {
value: {
title: typeof body.title === "string" ? body.title.trim() : undefined,
description:
typeof body.description === "string" ? body.description.trim() : undefined,
status: typeof body.status === "string" ? body.status.trim() : undefined,
priority: typeof body.priority === "string" ? body.priority.trim() : undefined,
assigneeUserId:
typeof body.assigneeUserId === "string"
? body.assigneeUserId.trim()
: body.assigneeUserId === null
? null
: undefined,
storyPoints:
typeof body.storyPoints === "number"
? Math.trunc(body.storyPoints)
: body.storyPoints === null
? null
: undefined,
dueDate:
typeof body.dueDate === "string"
? body.dueDate
: body.dueDate === null
? null
: undefined,
},
};
}
export function createServer(db: Database) {
return {
port: getConfig().port,
fetch: async (request: Request) => {
if (request.method === "OPTIONS") {
return json({ ok: true });
}
const url = new URL(request.url);
const parts = url.pathname.split("/").filter(Boolean);
if (request.method === "GET" && url.pathname === "/health") {
return json({ status: "ok" });
}
if (
request.method === "GET" &&
(url.pathname === "/openapi.json" ||
url.pathname === "/swagger.json" ||
url.pathname === "/api-docs")
) {
return json(openApiDocument);
}
if (request.method === "GET" && url.pathname === "/orgs") {
return json({ organizations: listOrganizations(db) });
}
if (request.method === "GET" && url.pathname === "/users") {
return json({ users: listUsers(db, url.searchParams.get("orgId")) });
}
if (request.method === "GET" && url.pathname === "/projects") {
return json({ projects: listProjects(db, url.searchParams.get("orgId")) });
}
if (request.method === "GET" && url.pathname === "/tasks") {
return json({
tasks: listTasks(db, {
orgId: url.searchParams.get("orgId"),
projectId: url.searchParams.get("projectId"),
status: url.searchParams.get("status"),
priority: url.searchParams.get("priority"),
assigneeUserId: url.searchParams.get("assigneeUserId"),
}),
});
}
if (request.method === "POST" && url.pathname === "/tasks") {
const payload = validateCreateTask(await readJson(request));
if ("error" in payload) {
return badRequest(payload.error);
}
const task = createTask(db, payload.value);
return json({ task }, { status: 201 });
}
if (parts[0] === "orgs" && parts.length === 2 && request.method === "GET") {
const summary = getOrganizationSummary(db, parts[1]!);
return summary ? json({ organization: summary }) : notFound("Organization not found.");
}
if (parts[0] === "orgs" && parts[2] === "projects" && request.method === "GET") {
return json({ projects: listProjects(db, parts[1]!) });
}
if (parts[0] === "orgs" && parts[2] === "tasks" && request.method === "GET") {
return json({
tasks: listTasks(db, {
orgId: parts[1]!,
projectId: url.searchParams.get("projectId"),
status: url.searchParams.get("status"),
priority: url.searchParams.get("priority"),
assigneeUserId: url.searchParams.get("assigneeUserId"),
}),
});
}
if (parts[0] === "tasks" && parts.length === 2 && request.method === "GET") {
const task = getTaskDetail(db, parts[1]!);
return task ? json({ task }) : notFound("Task not found.");
}
if (parts[0] === "tasks" && parts.length === 2 && request.method === "PATCH") {
const payload = validateUpdateTask(await readJson(request));
if ("error" in payload) {
return badRequest(payload.error);
}
const task = updateTask(db, parts[1]!, payload.value);
return task ? json({ task }) : notFound("Task not found.");
}
return notFound("Route not found.");
},
};
}
function main() {
const config = getConfig();
const db = ensureSeededDatabase(config.dbPath);
Bun.serve(createServer(db));
console.info("mock-task-api:listening", {
port: config.port,
dbPath: config.dbPath,
});
}
if (import.meta.main) {
main();
}