@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.
- package/CHANGELOG.md +88 -0
- package/README.md +252 -0
- package/SERVER.md +25 -0
- package/dist/index.d.ts +1536 -0
- package/dist/index.js +4103 -0
- package/drizzle/0000_fresh_roxanne_simpson.sql +51 -0
- package/drizzle/0001_sprint_markdown_content.sql +1 -0
- package/drizzle/0002_task_touched_files.sql +8 -0
- package/drizzle/meta/0000_snapshot.json +372 -0
- package/drizzle/meta/0001_snapshot.json +379 -0
- package/drizzle/meta/_journal.json +27 -0
- package/drizzle.config.ts +14 -0
- package/package.json +40 -0
- package/src/application/container.ts +44 -0
- package/src/application/dto/plan-sprint-analytics.dto.ts +30 -0
- package/src/application/plan.service.ts +123 -0
- package/src/application/sprint.service.ts +118 -0
- package/src/application/task.service.ts +389 -0
- package/src/db/connection.ts +25 -0
- package/src/db/migrator.ts +46 -0
- package/src/db/schema/index.ts +14 -0
- package/src/db/schema/plans.ts +18 -0
- package/src/db/schema/relations.ts +36 -0
- package/src/db/schema/sprints.ts +33 -0
- package/src/db/schema/tasks.ts +62 -0
- package/src/domain/entities/index.ts +30 -0
- package/src/domain/entities/plan.entity.ts +33 -0
- package/src/domain/entities/sprint.entity.ts +44 -0
- package/src/domain/entities/task.entity.ts +80 -0
- package/src/domain/repositories/index.ts +9 -0
- package/src/domain/repositories/plan.repository.ts +21 -0
- package/src/domain/repositories/sprint.repository.ts +19 -0
- package/src/domain/repositories/task.repository.ts +35 -0
- package/src/domain/services/index.ts +9 -0
- package/src/domain/services/plan-domain.service.ts +44 -0
- package/src/domain/services/sprint-domain.service.ts +44 -0
- package/src/domain/services/task-domain.service.ts +136 -0
- package/src/errors/backend-errors.ts +75 -0
- package/src/http/app-factory.ts +55 -0
- package/src/http/controllers/health.controller.ts +33 -0
- package/src/http/controllers/plan.controller.ts +153 -0
- package/src/http/controllers/sprint.controller.ts +111 -0
- package/src/http/controllers/task.controller.ts +158 -0
- package/src/http/express-augmentation.d.ts +20 -0
- package/src/http/middleware/cors.ts +41 -0
- package/src/http/middleware/error-handler.ts +50 -0
- package/src/http/middleware/request-id.ts +28 -0
- package/src/http/middleware/validate.ts +54 -0
- package/src/http/routes/v1/index.ts +39 -0
- package/src/http/routes/v1/plan.routes.ts +51 -0
- package/src/http/routes/v1/schemas.ts +175 -0
- package/src/http/routes/v1/sprint.routes.ts +49 -0
- package/src/http/routes/v1/task.routes.ts +64 -0
- package/src/index.ts +34 -0
- package/src/infrastructure/observability/audit-log.ts +34 -0
- package/src/infrastructure/observability/request-correlation.ts +20 -0
- package/src/infrastructure/repositories/drizzle/drizzle-plan.repository.ts +138 -0
- package/src/infrastructure/repositories/drizzle/drizzle-sprint.repository.ts +137 -0
- package/src/infrastructure/repositories/drizzle/drizzle-task.repository.ts +403 -0
- package/src/infrastructure/repositories/drizzle/index.ts +16 -0
- package/src/infrastructure/repositories/drizzle/row-mappers.ts +106 -0
- package/src/infrastructure/repositories/drizzle/sqlite-db.ts +13 -0
- package/src/infrastructure/repositories/repository-factory.ts +54 -0
- package/src/infrastructure/security/auth-context.ts +35 -0
- package/src/infrastructure/security/input-guard.ts +21 -0
- package/src/infrastructure/security/rate-limiter.ts +65 -0
- package/src/mcp/bootstrap-sprintdock-sqlite.ts +45 -0
- package/src/mcp/mcp-query-helpers.ts +89 -0
- package/src/mcp/mcp-text-formatters.ts +204 -0
- package/src/mcp/mcp-tool-error.ts +24 -0
- package/src/mcp/plugins/context-tools.plugin.ts +107 -0
- package/src/mcp/plugins/default-plugins.ts +23 -0
- package/src/mcp/plugins/index.ts +21 -0
- package/src/mcp/plugins/mcp-tool-kit.ts +90 -0
- package/src/mcp/plugins/plan-tools.plugin.ts +426 -0
- package/src/mcp/plugins/sprint-tools.plugin.ts +396 -0
- package/src/mcp/plugins/task-tools.plugin.ts +528 -0
- package/src/mcp/plugins/task-workflow.plugin.ts +275 -0
- package/src/mcp/plugins/types.ts +45 -0
- package/src/mcp/register-sprintdock-mcp-tools.ts +50 -0
- package/src/mcp/sprintdock-mcp-capabilities.ts +14 -0
- package/src/mcp/sprintdock-mcp-runtime.ts +119 -0
- package/src/mcp/tool-guard.ts +58 -0
- package/src/mcp/transports/http-app-factory.ts +31 -0
- package/src/mcp/transports/http-entry.ts +27 -0
- package/src/mcp/transports/stdio-entry.ts +17 -0
- package/tests/application/container.test.ts +36 -0
- package/tests/application/plan.service.test.ts +114 -0
- package/tests/application/sprint.service.test.ts +138 -0
- package/tests/application/task.service.test.ts +325 -0
- package/tests/db/test-db.test.ts +112 -0
- package/tests/domain/plan-domain.service.test.ts +44 -0
- package/tests/domain/sprint-domain.service.test.ts +38 -0
- package/tests/domain/task-domain.service.test.ts +105 -0
- package/tests/errors/backend-errors.test.ts +44 -0
- package/tests/helpers/test-db.ts +43 -0
- package/tests/http/error-handler.test.ts +37 -0
- package/tests/http/plan.routes.test.ts +128 -0
- package/tests/http/sprint.routes.test.ts +72 -0
- package/tests/http/task.routes.test.ts +130 -0
- package/tests/http/test-app.ts +17 -0
- package/tests/infrastructure/drizzle-plan.repository.test.ts +62 -0
- package/tests/infrastructure/drizzle-sprint.repository.test.ts +49 -0
- package/tests/infrastructure/drizzle-task.repository.test.ts +132 -0
- package/tests/mcp/mcp-text-formatters.test.ts +246 -0
- package/tests/mcp/register-sprintdock-mcp-tools.test.ts +207 -0
- 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
|
+
}
|