@sprintdock/backend 0.4.2

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.
Files changed (107) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +252 -0
  3. package/SERVER.md +25 -0
  4. package/dist/index.d.ts +1536 -0
  5. package/dist/index.js +4103 -0
  6. package/drizzle/0000_fresh_roxanne_simpson.sql +51 -0
  7. package/drizzle/0001_sprint_markdown_content.sql +1 -0
  8. package/drizzle/0002_task_touched_files.sql +8 -0
  9. package/drizzle/meta/0000_snapshot.json +372 -0
  10. package/drizzle/meta/0001_snapshot.json +379 -0
  11. package/drizzle/meta/_journal.json +27 -0
  12. package/drizzle.config.ts +14 -0
  13. package/package.json +40 -0
  14. package/src/application/container.ts +44 -0
  15. package/src/application/dto/plan-sprint-analytics.dto.ts +30 -0
  16. package/src/application/plan.service.ts +123 -0
  17. package/src/application/sprint.service.ts +118 -0
  18. package/src/application/task.service.ts +389 -0
  19. package/src/db/connection.ts +25 -0
  20. package/src/db/migrator.ts +46 -0
  21. package/src/db/schema/index.ts +14 -0
  22. package/src/db/schema/plans.ts +18 -0
  23. package/src/db/schema/relations.ts +36 -0
  24. package/src/db/schema/sprints.ts +33 -0
  25. package/src/db/schema/tasks.ts +62 -0
  26. package/src/domain/entities/index.ts +30 -0
  27. package/src/domain/entities/plan.entity.ts +33 -0
  28. package/src/domain/entities/sprint.entity.ts +44 -0
  29. package/src/domain/entities/task.entity.ts +80 -0
  30. package/src/domain/repositories/index.ts +9 -0
  31. package/src/domain/repositories/plan.repository.ts +21 -0
  32. package/src/domain/repositories/sprint.repository.ts +19 -0
  33. package/src/domain/repositories/task.repository.ts +35 -0
  34. package/src/domain/services/index.ts +9 -0
  35. package/src/domain/services/plan-domain.service.ts +44 -0
  36. package/src/domain/services/sprint-domain.service.ts +44 -0
  37. package/src/domain/services/task-domain.service.ts +136 -0
  38. package/src/errors/backend-errors.ts +75 -0
  39. package/src/http/app-factory.ts +55 -0
  40. package/src/http/controllers/health.controller.ts +33 -0
  41. package/src/http/controllers/plan.controller.ts +153 -0
  42. package/src/http/controllers/sprint.controller.ts +111 -0
  43. package/src/http/controllers/task.controller.ts +158 -0
  44. package/src/http/express-augmentation.d.ts +20 -0
  45. package/src/http/middleware/cors.ts +41 -0
  46. package/src/http/middleware/error-handler.ts +50 -0
  47. package/src/http/middleware/request-id.ts +28 -0
  48. package/src/http/middleware/validate.ts +54 -0
  49. package/src/http/routes/v1/index.ts +39 -0
  50. package/src/http/routes/v1/plan.routes.ts +51 -0
  51. package/src/http/routes/v1/schemas.ts +175 -0
  52. package/src/http/routes/v1/sprint.routes.ts +49 -0
  53. package/src/http/routes/v1/task.routes.ts +64 -0
  54. package/src/index.ts +34 -0
  55. package/src/infrastructure/observability/audit-log.ts +34 -0
  56. package/src/infrastructure/observability/request-correlation.ts +20 -0
  57. package/src/infrastructure/repositories/drizzle/drizzle-plan.repository.ts +138 -0
  58. package/src/infrastructure/repositories/drizzle/drizzle-sprint.repository.ts +137 -0
  59. package/src/infrastructure/repositories/drizzle/drizzle-task.repository.ts +403 -0
  60. package/src/infrastructure/repositories/drizzle/index.ts +16 -0
  61. package/src/infrastructure/repositories/drizzle/row-mappers.ts +106 -0
  62. package/src/infrastructure/repositories/drizzle/sqlite-db.ts +13 -0
  63. package/src/infrastructure/repositories/repository-factory.ts +54 -0
  64. package/src/infrastructure/security/auth-context.ts +35 -0
  65. package/src/infrastructure/security/input-guard.ts +21 -0
  66. package/src/infrastructure/security/rate-limiter.ts +65 -0
  67. package/src/mcp/bootstrap-sprintdock-sqlite.ts +45 -0
  68. package/src/mcp/mcp-query-helpers.ts +89 -0
  69. package/src/mcp/mcp-text-formatters.ts +204 -0
  70. package/src/mcp/mcp-tool-error.ts +24 -0
  71. package/src/mcp/plugins/context-tools.plugin.ts +107 -0
  72. package/src/mcp/plugins/default-plugins.ts +23 -0
  73. package/src/mcp/plugins/index.ts +21 -0
  74. package/src/mcp/plugins/mcp-tool-kit.ts +90 -0
  75. package/src/mcp/plugins/plan-tools.plugin.ts +426 -0
  76. package/src/mcp/plugins/sprint-tools.plugin.ts +396 -0
  77. package/src/mcp/plugins/task-tools.plugin.ts +528 -0
  78. package/src/mcp/plugins/task-workflow.plugin.ts +275 -0
  79. package/src/mcp/plugins/types.ts +45 -0
  80. package/src/mcp/register-sprintdock-mcp-tools.ts +50 -0
  81. package/src/mcp/sprintdock-mcp-capabilities.ts +14 -0
  82. package/src/mcp/sprintdock-mcp-runtime.ts +119 -0
  83. package/src/mcp/tool-guard.ts +58 -0
  84. package/src/mcp/transports/http-app-factory.ts +31 -0
  85. package/src/mcp/transports/http-entry.ts +27 -0
  86. package/src/mcp/transports/stdio-entry.ts +17 -0
  87. package/tests/application/container.test.ts +36 -0
  88. package/tests/application/plan.service.test.ts +114 -0
  89. package/tests/application/sprint.service.test.ts +138 -0
  90. package/tests/application/task.service.test.ts +325 -0
  91. package/tests/db/test-db.test.ts +112 -0
  92. package/tests/domain/plan-domain.service.test.ts +44 -0
  93. package/tests/domain/sprint-domain.service.test.ts +38 -0
  94. package/tests/domain/task-domain.service.test.ts +105 -0
  95. package/tests/errors/backend-errors.test.ts +44 -0
  96. package/tests/helpers/test-db.ts +43 -0
  97. package/tests/http/error-handler.test.ts +37 -0
  98. package/tests/http/plan.routes.test.ts +128 -0
  99. package/tests/http/sprint.routes.test.ts +72 -0
  100. package/tests/http/task.routes.test.ts +130 -0
  101. package/tests/http/test-app.ts +17 -0
  102. package/tests/infrastructure/drizzle-plan.repository.test.ts +62 -0
  103. package/tests/infrastructure/drizzle-sprint.repository.test.ts +49 -0
  104. package/tests/infrastructure/drizzle-task.repository.test.ts +132 -0
  105. package/tests/mcp/mcp-text-formatters.test.ts +246 -0
  106. package/tests/mcp/register-sprintdock-mcp-tools.test.ts +207 -0
  107. package/tsconfig.json +9 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Express application factory wiring middleware and versioned API routes.
5
+ */
6
+ import express, { type Express } from "express";
7
+ import type { ServiceSet } from "../application/container.js";
8
+ import { createV1Router } from "./routes/v1/index.js";
9
+ import { corsMiddleware } from "./middleware/cors.js";
10
+ import { errorHandler } from "./middleware/error-handler.js";
11
+ import { requestIdMiddleware } from "./middleware/request-id.js";
12
+
13
+ export interface HttpAppOptions {
14
+ /** When true (default), applies permissive CORS. */
15
+ cors?: boolean;
16
+ /** Forwarded to Express `trust proxy`. */
17
+ trustProxy?: boolean;
18
+ }
19
+
20
+ /**
21
+ * Assembles JSON parsing, optional CORS, request id propagation, `GET /` discovery payload, `/api/v1` routes, and the error handler.
22
+ */
23
+ export function createHttpApp(
24
+ services: ServiceSet,
25
+ options?: HttpAppOptions
26
+ ): Express {
27
+ const app = express();
28
+
29
+ if (options?.trustProxy) {
30
+ app.set("trust proxy", true);
31
+ }
32
+
33
+ app.use(express.json());
34
+ if (options?.cors !== false) {
35
+ app.use(corsMiddleware());
36
+ }
37
+ app.use(requestIdMiddleware);
38
+
39
+ app.get("/", (_req, res) => {
40
+ res.type("application/json").status(200).send({
41
+ service: "sprintdock-rest",
42
+ message:
43
+ "JSON API lives under /api/v1. Open /api/v1/health to verify the server.",
44
+ apiBase: "/api/v1",
45
+ health: "/api/v1/health",
46
+ webUi:
47
+ "The browser UI is a separate Vite app (@sprintdock/webui); run pnpm dev:web with SPRINTDOCK_API_BASE_URL pointing at this origin."
48
+ });
49
+ });
50
+
51
+ app.use("/api/v1", createV1Router(services));
52
+ app.use(errorHandler);
53
+
54
+ return app;
55
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Lightweight health probe for load balancers and smoke tests.
5
+ */
6
+ import { readFileSync } from "node:fs";
7
+ import type { NextFunction, Request, Response } from "express";
8
+
9
+ function readPackageVersion(): string {
10
+ const path = new URL("../../../package.json", import.meta.url);
11
+ const raw = readFileSync(path, "utf8");
12
+ const pkg = JSON.parse(raw) as { version?: string };
13
+ return pkg.version ?? "0.0.0";
14
+ }
15
+
16
+ /**
17
+ * Exposes process liveness and package version.
18
+ */
19
+ export class HealthController {
20
+ private readonly version = readPackageVersion();
21
+
22
+ public check = (
23
+ _req: Request,
24
+ res: Response,
25
+ _next: NextFunction
26
+ ): void => {
27
+ res.json({
28
+ status: "ok",
29
+ version: this.version,
30
+ timestamp: new Date().toISOString()
31
+ });
32
+ };
33
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: HTTP handlers for plans and execution context.
5
+ */
6
+ import type { NextFunction, Request, Response } from "express";
7
+ import type { PlanService } from "../../application/plan.service.js";
8
+ import type { SprintService } from "../../application/sprint.service.js";
9
+ import type { TaskService } from "../../application/task.service.js";
10
+ import { NotFoundError } from "../../errors/backend-errors.js";
11
+
12
+ /**
13
+ * Thin adapter over {@link PlanService}; uses {@link SprintService} for execution context
14
+ * and {@link TaskService} for plan-scoped analytics.
15
+ */
16
+ export class PlanController {
17
+ public constructor(
18
+ private readonly planService: PlanService,
19
+ private readonly sprintService: SprintService,
20
+ private readonly taskService: TaskService
21
+ ) {}
22
+
23
+ public list = async (
24
+ _req: Request,
25
+ res: Response,
26
+ next: NextFunction
27
+ ): Promise<void> => {
28
+ try {
29
+ const plans = await this.planService.listPlans();
30
+ res.json({ plans });
31
+ } catch (e) {
32
+ next(e);
33
+ }
34
+ };
35
+
36
+ public create = async (
37
+ req: Request,
38
+ res: Response,
39
+ next: NextFunction
40
+ ): Promise<void> => {
41
+ try {
42
+ const plan = await this.planService.createPlan(req.body);
43
+ res.status(201).json({ plan });
44
+ } catch (e) {
45
+ next(e);
46
+ }
47
+ };
48
+
49
+ public getPlanAnalytics = async (
50
+ req: Request,
51
+ res: Response,
52
+ next: NextFunction
53
+ ): Promise<void> => {
54
+ try {
55
+ const { idOrSlug } = req.params as { idOrSlug: string };
56
+ const analytics = await this.taskService.getPlanSprintTaskAnalytics(
57
+ idOrSlug
58
+ );
59
+ res.json({ analytics });
60
+ } catch (e) {
61
+ next(e);
62
+ }
63
+ };
64
+
65
+ public getOne = async (
66
+ req: Request,
67
+ res: Response,
68
+ next: NextFunction
69
+ ): Promise<void> => {
70
+ try {
71
+ const { idOrSlug } = req.params as { idOrSlug: string };
72
+ const plan = await this.planService.getPlan(idOrSlug);
73
+ res.json({ plan });
74
+ } catch (e) {
75
+ next(e);
76
+ }
77
+ };
78
+
79
+ public update = async (
80
+ req: Request,
81
+ res: Response,
82
+ next: NextFunction
83
+ ): Promise<void> => {
84
+ try {
85
+ const { idOrSlug } = req.params as { idOrSlug: string };
86
+ const plan = await this.planService.updatePlan(idOrSlug, req.body);
87
+ res.json({ plan });
88
+ } catch (e) {
89
+ next(e);
90
+ }
91
+ };
92
+
93
+ public activate = async (
94
+ req: Request,
95
+ res: Response,
96
+ next: NextFunction
97
+ ): Promise<void> => {
98
+ try {
99
+ const { idOrSlug } = req.params as { idOrSlug: string };
100
+ await this.planService.setActivePlan(idOrSlug);
101
+ res.json({ ok: true });
102
+ } catch (e) {
103
+ next(e);
104
+ }
105
+ };
106
+
107
+ public getExecutionContext = async (
108
+ _req: Request,
109
+ res: Response,
110
+ next: NextFunction
111
+ ): Promise<void> => {
112
+ try {
113
+ const plan = await this.planService.getActivePlan();
114
+ const sprints = await this.sprintService.listSprints(plan.id);
115
+ const activeSprints = sprints.filter((s) => s.status === "active");
116
+ res.json({ plan, activeSprints });
117
+ } catch (e) {
118
+ next(e);
119
+ }
120
+ };
121
+
122
+ /** Composes execution context with global task throughput for the dashboard. */
123
+ public getDashboardOverview = async (
124
+ _req: Request,
125
+ res: Response,
126
+ next: NextFunction
127
+ ): Promise<void> => {
128
+ try {
129
+ const throughput = await this.taskService.getGlobalTaskThroughput();
130
+ let plan;
131
+ try {
132
+ plan = await this.planService.getActivePlan();
133
+ } catch (e) {
134
+ if (e instanceof NotFoundError) {
135
+ res.json({
136
+ execution: { plan: null, activeSprints: [] },
137
+ throughput
138
+ });
139
+ return;
140
+ }
141
+ throw e;
142
+ }
143
+ const sprints = await this.sprintService.listSprints(plan.id);
144
+ const activeSprints = sprints.filter((s) => s.status === "active");
145
+ res.json({
146
+ execution: { plan, activeSprints },
147
+ throughput
148
+ });
149
+ } catch (e) {
150
+ next(e);
151
+ }
152
+ };
153
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: HTTP handlers for sprints scoped to plans.
5
+ */
6
+ import type { NextFunction, Request, Response } from "express";
7
+ import type { SprintService } from "../../application/sprint.service.js";
8
+ import type { TaskService } from "../../application/task.service.js";
9
+ import type { SprintStatus } from "../../domain/entities/sprint.entity";
10
+
11
+ /**
12
+ * Thin adapter combining sprint reads with task listings for detail responses.
13
+ */
14
+ export class SprintController {
15
+ public constructor(
16
+ private readonly sprintService: SprintService,
17
+ private readonly taskService: TaskService
18
+ ) {}
19
+
20
+ public listByPlan = async (
21
+ req: Request,
22
+ res: Response,
23
+ next: NextFunction
24
+ ): Promise<void> => {
25
+ try {
26
+ const { planId } = req.params as { planId: string };
27
+ const sprints = await this.sprintService.listSprints(planId);
28
+ res.json({ sprints });
29
+ } catch (e) {
30
+ next(e);
31
+ }
32
+ };
33
+
34
+ public create = async (
35
+ req: Request,
36
+ res: Response,
37
+ next: NextFunction
38
+ ): Promise<void> => {
39
+ try {
40
+ const { planId } = req.params as { planId: string };
41
+ const sprint = await this.sprintService.createSprint(planId, {
42
+ ...req.body,
43
+ planId
44
+ });
45
+ res.status(201).json({ sprint });
46
+ } catch (e) {
47
+ next(e);
48
+ }
49
+ };
50
+
51
+ public getOne = async (
52
+ req: Request,
53
+ res: Response,
54
+ next: NextFunction
55
+ ): Promise<void> => {
56
+ try {
57
+ const { id } = req.params as { id: string };
58
+ const sprint = await this.sprintService.getSprint(id);
59
+ const tasks = await this.taskService.listTasks(sprint.id);
60
+ res.json({ sprint, tasks });
61
+ } catch (e) {
62
+ next(e);
63
+ }
64
+ };
65
+
66
+ public patch = async (
67
+ req: Request,
68
+ res: Response,
69
+ next: NextFunction
70
+ ): Promise<void> => {
71
+ try {
72
+ const { id } = req.params as { id: string };
73
+ const b = req.body as {
74
+ name?: string;
75
+ goal?: string;
76
+ markdownContent?: string | null;
77
+ startDate?: string | null;
78
+ endDate?: string | null;
79
+ order?: number;
80
+ };
81
+ const sprint = await this.sprintService.updateSprint(id, {
82
+ ...(b.name !== undefined ? { name: b.name } : {}),
83
+ ...(b.goal !== undefined ? { goal: b.goal } : {}),
84
+ ...(b.markdownContent !== undefined
85
+ ? { markdownContent: b.markdownContent }
86
+ : {}),
87
+ ...(b.startDate !== undefined ? { startDate: b.startDate } : {}),
88
+ ...(b.endDate !== undefined ? { endDate: b.endDate } : {}),
89
+ ...(b.order !== undefined ? { order: b.order } : {})
90
+ });
91
+ res.json({ sprint });
92
+ } catch (e) {
93
+ next(e);
94
+ }
95
+ };
96
+
97
+ public updateStatus = async (
98
+ req: Request,
99
+ res: Response,
100
+ next: NextFunction
101
+ ): Promise<void> => {
102
+ try {
103
+ const { id } = req.params as { id: string };
104
+ const { status } = req.body as { status: SprintStatus };
105
+ const sprint = await this.sprintService.updateSprintStatus(id, status);
106
+ res.json({ sprint });
107
+ } catch (e) {
108
+ next(e);
109
+ }
110
+ };
111
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: HTTP handlers for tasks within sprints.
5
+ */
6
+ import type { NextFunction, Request, Response } from "express";
7
+ import type {
8
+ TaskFieldUpdateInput,
9
+ TaskService
10
+ } from "../../application/task.service.js";
11
+ import type { TaskPriority, TaskStatus } from "../../domain/entities/task.entity";
12
+
13
+ /**
14
+ * Thin adapter over {@link TaskService}.
15
+ */
16
+ export class TaskController {
17
+ public constructor(private readonly taskService: TaskService) {}
18
+
19
+ public listBySprint = async (
20
+ req: Request,
21
+ res: Response,
22
+ next: NextFunction
23
+ ): Promise<void> => {
24
+ try {
25
+ const { sprintId } = req.params as { sprintId: string };
26
+ const q = (req.validatedQuery ?? {}) as {
27
+ status?: TaskStatus;
28
+ priority?: TaskPriority;
29
+ assignee?: string;
30
+ };
31
+ const filter =
32
+ q.status || q.priority || q.assignee !== undefined
33
+ ? {
34
+ ...(q.status ? { status: q.status } : {}),
35
+ ...(q.priority ? { priority: q.priority } : {}),
36
+ ...(q.assignee !== undefined ? { assignee: q.assignee } : {})
37
+ }
38
+ : undefined;
39
+ const tasks = await this.taskService.listTasks(sprintId, undefined, filter);
40
+ res.json({ tasks });
41
+ } catch (e) {
42
+ next(e);
43
+ }
44
+ };
45
+
46
+ public create = async (
47
+ req: Request,
48
+ res: Response,
49
+ next: NextFunction
50
+ ): Promise<void> => {
51
+ try {
52
+ const { sprintId } = req.params as { sprintId: string };
53
+ const task = await this.taskService.createTask(
54
+ { ...req.body, sprintId },
55
+ undefined
56
+ );
57
+ res.status(201).json({ task });
58
+ } catch (e) {
59
+ next(e);
60
+ }
61
+ };
62
+
63
+ public getOne = async (
64
+ req: Request,
65
+ res: Response,
66
+ next: NextFunction
67
+ ): Promise<void> => {
68
+ try {
69
+ const { id } = req.params as { id: string };
70
+ const task = await this.taskService.getTask(id);
71
+ res.json({ task });
72
+ } catch (e) {
73
+ next(e);
74
+ }
75
+ };
76
+
77
+ public patch = async (
78
+ req: Request,
79
+ res: Response,
80
+ next: NextFunction
81
+ ): Promise<void> => {
82
+ try {
83
+ const { id } = req.params as { id: string };
84
+ const b = req.body as TaskFieldUpdateInput;
85
+ const task = await this.taskService.updateTask(id, {
86
+ ...(b.title !== undefined ? { title: b.title } : {}),
87
+ ...(b.description !== undefined ? { description: b.description } : {}),
88
+ ...(b.priority !== undefined ? { priority: b.priority } : {}),
89
+ ...(b.assignee !== undefined ? { assignee: b.assignee } : {}),
90
+ ...(b.tags !== undefined ? { tags: b.tags } : {}),
91
+ ...(b.order !== undefined ? { order: b.order } : {}),
92
+ ...(b.touchedFiles !== undefined ? { touchedFiles: b.touchedFiles } : {})
93
+ });
94
+ res.json({ task });
95
+ } catch (e) {
96
+ next(e);
97
+ }
98
+ };
99
+
100
+ public updateStatus = async (
101
+ req: Request,
102
+ res: Response,
103
+ next: NextFunction
104
+ ): Promise<void> => {
105
+ try {
106
+ const { id } = req.params as { id: string };
107
+ const { status } = req.body as { status: TaskStatus };
108
+ const task = await this.taskService.updateTaskStatus(id, status);
109
+ res.json({ task });
110
+ } catch (e) {
111
+ next(e);
112
+ }
113
+ };
114
+
115
+ public assign = async (
116
+ req: Request,
117
+ res: Response,
118
+ next: NextFunction
119
+ ): Promise<void> => {
120
+ try {
121
+ const { id } = req.params as { id: string };
122
+ const { assignee } = req.body as { assignee: string | null };
123
+ const task = await this.taskService.assignTask(id, assignee);
124
+ res.json({ task });
125
+ } catch (e) {
126
+ next(e);
127
+ }
128
+ };
129
+
130
+ public move = async (
131
+ req: Request,
132
+ res: Response,
133
+ next: NextFunction
134
+ ): Promise<void> => {
135
+ try {
136
+ const { id } = req.params as { id: string };
137
+ const { targetSprintId } = req.body as { targetSprintId: string };
138
+ const task = await this.taskService.moveTask(id, targetSprintId);
139
+ res.json({ task });
140
+ } catch (e) {
141
+ next(e);
142
+ }
143
+ };
144
+
145
+ public remove = async (
146
+ req: Request,
147
+ res: Response,
148
+ next: NextFunction
149
+ ): Promise<void> => {
150
+ try {
151
+ const { id } = req.params as { id: string };
152
+ await this.taskService.deleteTask(id);
153
+ res.json({ deleted: true });
154
+ } catch (e) {
155
+ next(e);
156
+ }
157
+ };
158
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Express Request fields set by HTTP middleware.
5
+ */
6
+ declare global {
7
+ namespace Express {
8
+ interface Request {
9
+ /** Correlation id from {@link X-Request-Id} or generated in middleware. */
10
+ id: string;
11
+ /**
12
+ * Populated by {@link validate} when a query schema is provided. Express 5 keeps
13
+ * {@link Request.query} read-only, so parsed query values live here.
14
+ */
15
+ validatedQuery?: Record<string, unknown>;
16
+ }
17
+ }
18
+ }
19
+
20
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Configurable CORS middleware for REST responses and preflight.
5
+ */
6
+
7
+ import type { NextFunction, Request, Response } from "express";
8
+
9
+ export interface CorsOptions {
10
+ /** Allowed Origin header value. Default `*`. */
11
+ origin?: string;
12
+ /** Value for Access-Control-Allow-Methods. */
13
+ methods?: string;
14
+ /** Value for Access-Control-Allow-Headers. */
15
+ allowedHeaders?: string;
16
+ }
17
+
18
+ const DEFAULT_METHODS = "GET,POST,PATCH,DELETE,OPTIONS,HEAD";
19
+ const DEFAULT_ALLOWED_HEADERS = "Content-Type, Authorization, X-Request-Id";
20
+
21
+ /**
22
+ * Applies CORS headers and answers OPTIONS with 204.
23
+ */
24
+ export function corsMiddleware(options?: CorsOptions) {
25
+ const origin = options?.origin ?? "*";
26
+ const methods = options?.methods ?? DEFAULT_METHODS;
27
+ const allowedHeaders = options?.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS;
28
+
29
+ return (req: Request, res: Response, next: NextFunction): void => {
30
+ res.setHeader("Access-Control-Allow-Origin", origin);
31
+ res.setHeader("Access-Control-Allow-Methods", methods);
32
+ res.setHeader("Access-Control-Allow-Headers", allowedHeaders);
33
+
34
+ if (req.method === "OPTIONS") {
35
+ res.status(204).end();
36
+ return;
37
+ }
38
+
39
+ next();
40
+ };
41
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Express error middleware mapping domain errors to JSON responses.
5
+ */
6
+ import type { NextFunction, Request, Response } from "express";
7
+ import { BackendError } from "../../errors/backend-errors.js";
8
+
9
+ /**
10
+ * Maps {@link BackendError} subclasses to HTTP status; unknown errors become 500.
11
+ * In non-production, includes `stack` on the error payload for unknown failures.
12
+ */
13
+ export function errorHandler(
14
+ err: unknown,
15
+ _req: Request,
16
+ res: Response,
17
+ next: NextFunction
18
+ ): void {
19
+ if (res.headersSent) {
20
+ next(err);
21
+ return;
22
+ }
23
+
24
+ if (err instanceof BackendError) {
25
+ res.status(err.statusCode).json({
26
+ error: {
27
+ message: err.message,
28
+ statusCode: err.statusCode
29
+ }
30
+ });
31
+ return;
32
+ }
33
+
34
+ const message =
35
+ err instanceof Error ? err.message : "Internal server error";
36
+ const payload: {
37
+ error: { message: string; statusCode: number; stack?: string };
38
+ } = {
39
+ error: {
40
+ message,
41
+ statusCode: 500
42
+ }
43
+ };
44
+
45
+ if (process.env.NODE_ENV !== "production" && err instanceof Error && err.stack) {
46
+ payload.error.stack = err.stack;
47
+ }
48
+
49
+ res.status(500).json(payload);
50
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Propagates X-Request-Id through the request lifecycle and response headers.
5
+ */
6
+ import { randomUUID } from "node:crypto";
7
+ import type { NextFunction, Request, Response } from "express";
8
+
9
+ const HEADER = "x-request-id";
10
+
11
+ /**
12
+ * Uses incoming {@link HEADER} when present; otherwise generates a UUID.
13
+ * Sets {@link Request.id} and echoes the id on the response.
14
+ */
15
+ export function requestIdMiddleware(
16
+ req: Request,
17
+ res: Response,
18
+ next: NextFunction
19
+ ): void {
20
+ const incoming = req.get(HEADER);
21
+ const id =
22
+ typeof incoming === "string" && incoming.trim().length > 0
23
+ ? incoming.trim()
24
+ : randomUUID();
25
+ req.id = id;
26
+ res.setHeader("X-Request-Id", id);
27
+ next();
28
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * package: @sprintdock/backend
3
+ * author: vikash sharma
4
+ * description: Zod-based validation middleware for body, query, and route params.
5
+ */
6
+ import type { NextFunction, Request, Response } from "express";
7
+ import { z, type ZodTypeAny } from "zod";
8
+
9
+ export interface ValidateSchemas {
10
+ body?: ZodTypeAny;
11
+ query?: ZodTypeAny;
12
+ params?: ZodTypeAny;
13
+ }
14
+
15
+ /**
16
+ * Returns middleware that parses {@link Request.body}, {@link Request.query}, and/or
17
+ * {@link Request.params} with the given Zod schemas and replaces them with parsed values.
18
+ */
19
+ export function validate(schemas: ValidateSchemas) {
20
+ return (req: Request, res: Response, next: NextFunction): void => {
21
+ try {
22
+ if (schemas.body) {
23
+ req.body = schemas.body.parse(req.body) as Request["body"];
24
+ }
25
+ if (schemas.query) {
26
+ const rawQuery =
27
+ req.query && typeof req.query === "object"
28
+ ? { ...(req.query as Record<string, unknown>) }
29
+ : {};
30
+ req.validatedQuery = schemas.query.parse(rawQuery) as Record<
31
+ string,
32
+ unknown
33
+ >;
34
+ }
35
+ if (schemas.params) {
36
+ req.params = schemas.params.parse({
37
+ ...(req.params as Record<string, unknown>)
38
+ }) as Request["params"];
39
+ }
40
+ next();
41
+ } catch (e) {
42
+ if (e instanceof z.ZodError) {
43
+ res.status(400).json({
44
+ error: {
45
+ message: "Validation failed",
46
+ details: e.issues
47
+ }
48
+ });
49
+ return;
50
+ }
51
+ next(e);
52
+ }
53
+ };
54
+ }