@alt-t4b/pm-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@alt-t4b/pm-server",
3
+ "version": "0.1.0",
4
+ "bin": {
5
+ "tab-pm": "./src/index.ts"
6
+ },
7
+ "files": ["src"],
8
+ "scripts": {
9
+ "dev": "bun run --hot src/index.ts",
10
+ "start": "bun run src/index.ts"
11
+ },
12
+ "dependencies": {
13
+ "@alt-t4b/pm-domain": "^0.1.0",
14
+ "hono": "^4"
15
+ },
16
+ "devDependencies": {
17
+ "@types/bun": "^1.2",
18
+ "typescript": "^5"
19
+ }
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bun
2
+ import { Hono } from "hono";
3
+ import { logger } from "hono/logger";
4
+ import { cors } from "hono/cors";
5
+ import { networkInterfaces } from "os";
6
+ import { bootstrap, ServiceError } from "@alt-t4b/pm-domain";
7
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
8
+ import { projectRoutes } from "./routes/projects";
9
+ import { taskRoutes } from "./routes/tasks";
10
+
11
+ // Parse CLI flags (--port, --host, --sqlite-path)
12
+ function parseArgs(): {
13
+ port: number;
14
+ host: string;
15
+ dbPath?: string;
16
+ } {
17
+ const args = process.argv.slice(2);
18
+ let port = Number(process.env.PM_PORT) || 3000;
19
+ let host = process.env.PM_HOST ?? "127.0.0.1";
20
+ let dbPath: string | undefined = process.env.SQLITE_PATH;
21
+
22
+ for (let i = 0; i < args.length; i++) {
23
+ if (args[i] === "--port" && args[i + 1]) port = Number(args[++i]);
24
+ if (args[i] === "--host" && args[i + 1]) host = args[++i];
25
+ if (args[i] === "--sqlite-path" && args[i + 1]) dbPath = args[++i];
26
+ }
27
+
28
+ return { port, host, dbPath };
29
+ }
30
+
31
+ // Bootstrap
32
+ const { port, host, dbPath } = parseArgs();
33
+ const ctx = bootstrap(dbPath);
34
+
35
+ // Web application
36
+ const app = new Hono();
37
+ app.use(
38
+ "*",
39
+ cors({
40
+ origin: (origin) =>
41
+ origin?.startsWith("http://localhost") || origin?.startsWith("http://127.0.0.1")
42
+ ? origin
43
+ : null,
44
+ allowMethods: ["GET", "POST", "PATCH", "DELETE"],
45
+ })
46
+ );
47
+ app.use("*", logger((str) => process.stderr.write(str + "\n")));
48
+ app.route("/api/projects/:projectSlug/tasks", taskRoutes(ctx.taskService));
49
+ app.route("/api/projects", projectRoutes(ctx.projectService));
50
+ app.get("/api/health", (c) => c.json({ status: "ok" }));
51
+
52
+ app.onError((err, c) => {
53
+ if (err instanceof SyntaxError) return c.json({ error: "invalid JSON body" }, 400);
54
+ if (err instanceof ServiceError) return c.json({ error: err.message }, err.statusCode as ContentfulStatusCode);
55
+ console.error(err);
56
+ return c.json({ error: "internal server error" }, 500);
57
+ });
58
+
59
+ function getNetworkAddress(): string | undefined {
60
+ for (const addrs of Object.values(networkInterfaces())) {
61
+ for (const addr of addrs ?? []) {
62
+ if (addr.family === "IPv4" && !addr.internal) return addr.address;
63
+ }
64
+ }
65
+ }
66
+
67
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
68
+ console.error(`tab-pm listening on http://${displayHost}:${port}`);
69
+ if (host === "0.0.0.0") {
70
+ const networkAddr = getNetworkAddress();
71
+ if (networkAddr) {
72
+ console.error(`tab-pm network: http://${networkAddr}:${port}`);
73
+ }
74
+ }
75
+
76
+ export default {
77
+ port,
78
+ hostname: host,
79
+ fetch: app.fetch,
80
+ };
@@ -0,0 +1,58 @@
1
+ import { Hono } from "hono";
2
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
3
+ import {
4
+ ServiceError,
5
+ type IProjectService,
6
+ type CreateProjectInput,
7
+ type UpdateProjectInput,
8
+ } from "@alt-t4b/pm-domain";
9
+
10
+ export function projectRoutes(service: IProjectService): Hono {
11
+ const app = new Hono();
12
+
13
+ // GET /api/projects
14
+ app.get("/", (c) => {
15
+ return c.json(service.findAll());
16
+ });
17
+
18
+ // POST /api/projects
19
+ app.post("/", async (c) => {
20
+ const body = await c.req.json<CreateProjectInput>();
21
+ try {
22
+ const project = service.create(body);
23
+ return c.json(project, 201);
24
+ } catch (e: unknown) {
25
+ if (e instanceof ServiceError) return c.json({ error: e.message }, e.statusCode as ContentfulStatusCode);
26
+ throw e;
27
+ }
28
+ });
29
+
30
+ // GET /api/projects/:slug
31
+ app.get("/:slug", (c) => {
32
+ const project = service.findBySlug(c.req.param("slug"));
33
+ if (!project) return c.json({ error: "project not found" }, 404);
34
+ return c.json(project);
35
+ });
36
+
37
+ // PATCH /api/projects/:slug
38
+ app.patch("/:slug", async (c) => {
39
+ const body = await c.req.json<UpdateProjectInput>();
40
+ try {
41
+ const project = service.update(c.req.param("slug"), body);
42
+ if (!project) return c.json({ error: "project not found" }, 404);
43
+ return c.json(project);
44
+ } catch (e: unknown) {
45
+ if (e instanceof ServiceError) return c.json({ error: e.message }, e.statusCode as ContentfulStatusCode);
46
+ throw e;
47
+ }
48
+ });
49
+
50
+ // DELETE /api/projects/:slug
51
+ app.delete("/:slug", (c) => {
52
+ const deleted = service.delete(c.req.param("slug"));
53
+ if (!deleted) return c.json({ error: "project not found" }, 404);
54
+ return c.json({ ok: true });
55
+ });
56
+
57
+ return app;
58
+ }
@@ -0,0 +1,58 @@
1
+ import { Hono } from "hono";
2
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
3
+ import {
4
+ ServiceError,
5
+ type ITaskService,
6
+ type CreateTaskInput,
7
+ type UpdateTaskInput,
8
+ } from "@alt-t4b/pm-domain";
9
+
10
+ export function taskRoutes(service: ITaskService): Hono {
11
+ const app = new Hono();
12
+
13
+ // GET /api/projects/:projectSlug/tasks
14
+ app.get("/", (c) => {
15
+ const projectSlug = c.req.param("projectSlug")!;
16
+ try {
17
+ return c.json(service.findByProjectSlug(projectSlug));
18
+ } catch (e: unknown) {
19
+ if (e instanceof ServiceError) return c.json({ error: e.message }, e.statusCode as ContentfulStatusCode);
20
+ throw e;
21
+ }
22
+ });
23
+
24
+ // POST /api/projects/:projectSlug/tasks
25
+ app.post("/", async (c) => {
26
+ const projectSlug = c.req.param("projectSlug")!;
27
+ const body = await c.req.json<CreateTaskInput>();
28
+ try {
29
+ const task = service.create(projectSlug, body);
30
+ return c.json(task, 201);
31
+ } catch (e: unknown) {
32
+ if (e instanceof ServiceError) return c.json({ error: e.message }, e.statusCode as ContentfulStatusCode);
33
+ throw e;
34
+ }
35
+ });
36
+
37
+ // PATCH /api/projects/:projectSlug/tasks/:id
38
+ app.patch("/:id", async (c) => {
39
+ const body = await c.req.json<UpdateTaskInput>();
40
+ try {
41
+ const task = service.update(c.req.param("id")!, body);
42
+ if (!task) return c.json({ error: "task not found" }, 404);
43
+ return c.json(task);
44
+ } catch (e: unknown) {
45
+ if (e instanceof ServiceError) return c.json({ error: e.message }, e.statusCode as ContentfulStatusCode);
46
+ throw e;
47
+ }
48
+ });
49
+
50
+ // DELETE /api/projects/:projectSlug/tasks/:id
51
+ app.delete("/:id", (c) => {
52
+ const deleted = service.delete(c.req.param("id")!);
53
+ if (!deleted) return c.json({ error: "task not found" }, 404);
54
+ return c.json({ ok: true });
55
+ });
56
+
57
+ return app;
58
+ }