@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,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Composes all `/api/v1` routers onto a single Express Router.
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from "express";
|
|
7
|
+
import type { ServiceSet } from "../../../application/container.js";
|
|
8
|
+
import { PlanController } from "../../controllers/plan.controller.js";
|
|
9
|
+
import { SprintController } from "../../controllers/sprint.controller.js";
|
|
10
|
+
import { TaskController } from "../../controllers/task.controller.js";
|
|
11
|
+
import { HealthController } from "../../controllers/health.controller.js";
|
|
12
|
+
import { registerPlanRoutes } from "./plan.routes.js";
|
|
13
|
+
import { registerSprintRoutes } from "./sprint.routes.js";
|
|
14
|
+
import { registerTaskRoutes } from "./task.routes.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds the `/api/v1` router with health, plans, sprints, and tasks.
|
|
18
|
+
*/
|
|
19
|
+
export function createV1Router(services: ServiceSet): Router {
|
|
20
|
+
const router = Router();
|
|
21
|
+
const health = new HealthController();
|
|
22
|
+
const planController = new PlanController(
|
|
23
|
+
services.planService,
|
|
24
|
+
services.sprintService,
|
|
25
|
+
services.taskService
|
|
26
|
+
);
|
|
27
|
+
const sprintController = new SprintController(
|
|
28
|
+
services.sprintService,
|
|
29
|
+
services.taskService
|
|
30
|
+
);
|
|
31
|
+
const taskController = new TaskController(services.taskService);
|
|
32
|
+
|
|
33
|
+
router.get("/health", health.check);
|
|
34
|
+
registerPlanRoutes(router, planController);
|
|
35
|
+
registerSprintRoutes(router, sprintController);
|
|
36
|
+
registerTaskRoutes(router, taskController);
|
|
37
|
+
|
|
38
|
+
return router;
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Versioned routes for plans and execution context.
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from "express";
|
|
7
|
+
import type { PlanController } from "../../controllers/plan.controller.js";
|
|
8
|
+
import { validate } from "../../middleware/validate.js";
|
|
9
|
+
import {
|
|
10
|
+
createPlanBody,
|
|
11
|
+
idOrSlugParams,
|
|
12
|
+
updatePlanBody
|
|
13
|
+
} from "./schemas.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registers plan and execution-context endpoints on {@link Router}.
|
|
17
|
+
*/
|
|
18
|
+
export function registerPlanRoutes(router: Router, c: PlanController): void {
|
|
19
|
+
router.get("/dashboard-overview", (req, res, next) =>
|
|
20
|
+
void c.getDashboardOverview(req, res, next)
|
|
21
|
+
);
|
|
22
|
+
router.get("/plans", (req, res, next) => void c.list(req, res, next));
|
|
23
|
+
router.post(
|
|
24
|
+
"/plans",
|
|
25
|
+
validate({ body: createPlanBody }),
|
|
26
|
+
(req, res, next) => void c.create(req, res, next)
|
|
27
|
+
);
|
|
28
|
+
router.get(
|
|
29
|
+
"/plans/:idOrSlug/analytics",
|
|
30
|
+
validate({ params: idOrSlugParams }),
|
|
31
|
+
(req, res, next) => void c.getPlanAnalytics(req, res, next)
|
|
32
|
+
);
|
|
33
|
+
router.get(
|
|
34
|
+
"/plans/:idOrSlug",
|
|
35
|
+
validate({ params: idOrSlugParams }),
|
|
36
|
+
(req, res, next) => void c.getOne(req, res, next)
|
|
37
|
+
);
|
|
38
|
+
router.patch(
|
|
39
|
+
"/plans/:idOrSlug",
|
|
40
|
+
validate({ params: idOrSlugParams, body: updatePlanBody }),
|
|
41
|
+
(req, res, next) => void c.update(req, res, next)
|
|
42
|
+
);
|
|
43
|
+
router.post(
|
|
44
|
+
"/plans/:idOrSlug/activate",
|
|
45
|
+
validate({ params: idOrSlugParams }),
|
|
46
|
+
(req, res, next) => void c.activate(req, res, next)
|
|
47
|
+
);
|
|
48
|
+
router.get("/execution-context", (req, res, next) =>
|
|
49
|
+
void c.getExecutionContext(req, res, next)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Shared Zod schemas for v1 REST validation.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
export const planStatusSchema = z.enum([
|
|
9
|
+
"draft",
|
|
10
|
+
"active",
|
|
11
|
+
"completed",
|
|
12
|
+
"archived"
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export const sprintStatusSchema = z.enum([
|
|
16
|
+
"planned",
|
|
17
|
+
"active",
|
|
18
|
+
"completed",
|
|
19
|
+
"archived"
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export const taskStatusSchema = z.enum([
|
|
23
|
+
"todo",
|
|
24
|
+
"in_progress",
|
|
25
|
+
"blocked",
|
|
26
|
+
"done"
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const taskPrioritySchema = z.enum([
|
|
30
|
+
"low",
|
|
31
|
+
"medium",
|
|
32
|
+
"high",
|
|
33
|
+
"critical"
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export const idOrSlugParams = z.object({
|
|
37
|
+
idOrSlug: z.string().min(1)
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const planIdParams = z.object({
|
|
41
|
+
planId: z.string().min(1)
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const sprintIdParams = z.object({
|
|
45
|
+
sprintId: z.string().min(1)
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const sprintIdPathParams = z.object({
|
|
49
|
+
id: z.string().min(1)
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const taskIdPathParams = z.object({
|
|
53
|
+
id: z.string().min(1)
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const createPlanBody = z.object({
|
|
57
|
+
slug: z.string().min(1),
|
|
58
|
+
title: z.string().min(1),
|
|
59
|
+
description: z.string().nullable().optional(),
|
|
60
|
+
markdownContent: z.string().nullable().optional()
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const updatePlanBody = z
|
|
64
|
+
.object({
|
|
65
|
+
title: z.string().min(1).optional(),
|
|
66
|
+
description: z.string().nullable().optional(),
|
|
67
|
+
markdownContent: z.string().nullable().optional(),
|
|
68
|
+
status: planStatusSchema.optional()
|
|
69
|
+
})
|
|
70
|
+
.refine(
|
|
71
|
+
(v) =>
|
|
72
|
+
v.title !== undefined ||
|
|
73
|
+
v.description !== undefined ||
|
|
74
|
+
v.markdownContent !== undefined ||
|
|
75
|
+
v.status !== undefined,
|
|
76
|
+
{ message: "At least one field is required" }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export const createSprintBody = z.object({
|
|
80
|
+
slug: z.string().min(1),
|
|
81
|
+
name: z.string().min(1),
|
|
82
|
+
goal: z.string(),
|
|
83
|
+
markdownContent: z.string().nullable().optional(),
|
|
84
|
+
order: z.number().int().optional(),
|
|
85
|
+
startDate: z.string().nullable().optional(),
|
|
86
|
+
endDate: z.string().nullable().optional()
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const updateSprintBody = z
|
|
90
|
+
.object({
|
|
91
|
+
name: z.string().min(1).optional(),
|
|
92
|
+
goal: z.string().optional(),
|
|
93
|
+
markdownContent: z.string().nullable().optional(),
|
|
94
|
+
startDate: z.string().nullable().optional(),
|
|
95
|
+
endDate: z.string().nullable().optional(),
|
|
96
|
+
order: z.number().int().optional()
|
|
97
|
+
})
|
|
98
|
+
.refine(
|
|
99
|
+
(v) =>
|
|
100
|
+
v.name !== undefined ||
|
|
101
|
+
v.goal !== undefined ||
|
|
102
|
+
v.markdownContent !== undefined ||
|
|
103
|
+
v.startDate !== undefined ||
|
|
104
|
+
v.endDate !== undefined ||
|
|
105
|
+
v.order !== undefined,
|
|
106
|
+
{ message: "At least one field is required" }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
export const updateSprintStatusBody = z.object({
|
|
110
|
+
status: sprintStatusSchema
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const taskListQuery = z.object({
|
|
114
|
+
status: taskStatusSchema.optional(),
|
|
115
|
+
priority: taskPrioritySchema.optional(),
|
|
116
|
+
assignee: z.string().optional()
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const taskTouchedFileTypeSchema = z.enum([
|
|
120
|
+
"test",
|
|
121
|
+
"implementation",
|
|
122
|
+
"doc",
|
|
123
|
+
"config",
|
|
124
|
+
"other"
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
export const taskTouchedFileItemSchema = z.object({
|
|
128
|
+
path: z.string().min(1).max(2048),
|
|
129
|
+
fileType: taskTouchedFileTypeSchema
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export const createTaskBody = z.object({
|
|
133
|
+
title: z.string().min(1),
|
|
134
|
+
priority: taskPrioritySchema,
|
|
135
|
+
description: z.string().nullable().optional(),
|
|
136
|
+
order: z.number().int().optional(),
|
|
137
|
+
assignee: z.string().nullable().optional(),
|
|
138
|
+
tags: z.array(z.string()).nullable().optional(),
|
|
139
|
+
dependsOnTaskIds: z.array(z.string().min(1)).optional(),
|
|
140
|
+
touchedFiles: z.array(taskTouchedFileItemSchema).optional()
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export const updateTaskBody = z
|
|
144
|
+
.object({
|
|
145
|
+
title: z.string().min(1).optional(),
|
|
146
|
+
description: z.string().nullable().optional(),
|
|
147
|
+
priority: taskPrioritySchema.optional(),
|
|
148
|
+
assignee: z.string().nullable().optional(),
|
|
149
|
+
tags: z.array(z.string()).nullable().optional(),
|
|
150
|
+
order: z.number().int().optional(),
|
|
151
|
+
touchedFiles: z.array(taskTouchedFileItemSchema).optional()
|
|
152
|
+
})
|
|
153
|
+
.refine(
|
|
154
|
+
(v) =>
|
|
155
|
+
v.title !== undefined ||
|
|
156
|
+
v.description !== undefined ||
|
|
157
|
+
v.priority !== undefined ||
|
|
158
|
+
v.assignee !== undefined ||
|
|
159
|
+
v.tags !== undefined ||
|
|
160
|
+
v.order !== undefined ||
|
|
161
|
+
v.touchedFiles !== undefined,
|
|
162
|
+
{ message: "At least one field is required" }
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
export const updateTaskStatusBody = z.object({
|
|
166
|
+
status: taskStatusSchema
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export const assignTaskBody = z.object({
|
|
170
|
+
assignee: z.union([z.string().min(1), z.null()])
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export const moveTaskBody = z.object({
|
|
174
|
+
targetSprintId: z.string().min(1)
|
|
175
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Versioned routes for sprints.
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from "express";
|
|
7
|
+
import type { SprintController } from "../../controllers/sprint.controller.js";
|
|
8
|
+
import { validate } from "../../middleware/validate.js";
|
|
9
|
+
import {
|
|
10
|
+
createSprintBody,
|
|
11
|
+
planIdParams,
|
|
12
|
+
sprintIdPathParams,
|
|
13
|
+
updateSprintBody,
|
|
14
|
+
updateSprintStatusBody
|
|
15
|
+
} from "./schemas.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Registers sprint endpoints on {@link Router}.
|
|
19
|
+
*/
|
|
20
|
+
export function registerSprintRoutes(
|
|
21
|
+
router: Router,
|
|
22
|
+
c: SprintController
|
|
23
|
+
): void {
|
|
24
|
+
router.get(
|
|
25
|
+
"/plans/:planId/sprints",
|
|
26
|
+
validate({ params: planIdParams }),
|
|
27
|
+
(req, res, next) => void c.listByPlan(req, res, next)
|
|
28
|
+
);
|
|
29
|
+
router.post(
|
|
30
|
+
"/plans/:planId/sprints",
|
|
31
|
+
validate({ params: planIdParams, body: createSprintBody }),
|
|
32
|
+
(req, res, next) => void c.create(req, res, next)
|
|
33
|
+
);
|
|
34
|
+
router.get(
|
|
35
|
+
"/sprints/:id",
|
|
36
|
+
validate({ params: sprintIdPathParams }),
|
|
37
|
+
(req, res, next) => void c.getOne(req, res, next)
|
|
38
|
+
);
|
|
39
|
+
router.patch(
|
|
40
|
+
"/sprints/:id",
|
|
41
|
+
validate({ params: sprintIdPathParams, body: updateSprintBody }),
|
|
42
|
+
(req, res, next) => void c.patch(req, res, next)
|
|
43
|
+
);
|
|
44
|
+
router.patch(
|
|
45
|
+
"/sprints/:id/status",
|
|
46
|
+
validate({ params: sprintIdPathParams, body: updateSprintStatusBody }),
|
|
47
|
+
(req, res, next) => void c.updateStatus(req, res, next)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Versioned routes for tasks.
|
|
5
|
+
*/
|
|
6
|
+
import { Router } from "express";
|
|
7
|
+
import type { TaskController } from "../../controllers/task.controller.js";
|
|
8
|
+
import { validate } from "../../middleware/validate.js";
|
|
9
|
+
import {
|
|
10
|
+
assignTaskBody,
|
|
11
|
+
createTaskBody,
|
|
12
|
+
moveTaskBody,
|
|
13
|
+
sprintIdParams,
|
|
14
|
+
taskIdPathParams,
|
|
15
|
+
taskListQuery,
|
|
16
|
+
updateTaskBody,
|
|
17
|
+
updateTaskStatusBody
|
|
18
|
+
} from "./schemas.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Registers task endpoints on {@link Router}.
|
|
22
|
+
*/
|
|
23
|
+
export function registerTaskRoutes(router: Router, c: TaskController): void {
|
|
24
|
+
router.get(
|
|
25
|
+
"/sprints/:sprintId/tasks",
|
|
26
|
+
validate({ params: sprintIdParams, query: taskListQuery }),
|
|
27
|
+
(req, res, next) => void c.listBySprint(req, res, next)
|
|
28
|
+
);
|
|
29
|
+
router.post(
|
|
30
|
+
"/sprints/:sprintId/tasks",
|
|
31
|
+
validate({ params: sprintIdParams, body: createTaskBody }),
|
|
32
|
+
(req, res, next) => void c.create(req, res, next)
|
|
33
|
+
);
|
|
34
|
+
router.get(
|
|
35
|
+
"/tasks/:id",
|
|
36
|
+
validate({ params: taskIdPathParams }),
|
|
37
|
+
(req, res, next) => void c.getOne(req, res, next)
|
|
38
|
+
);
|
|
39
|
+
router.patch(
|
|
40
|
+
"/tasks/:id",
|
|
41
|
+
validate({ params: taskIdPathParams, body: updateTaskBody }),
|
|
42
|
+
(req, res, next) => void c.patch(req, res, next)
|
|
43
|
+
);
|
|
44
|
+
router.patch(
|
|
45
|
+
"/tasks/:id/status",
|
|
46
|
+
validate({ params: taskIdPathParams, body: updateTaskStatusBody }),
|
|
47
|
+
(req, res, next) => void c.updateStatus(req, res, next)
|
|
48
|
+
);
|
|
49
|
+
router.patch(
|
|
50
|
+
"/tasks/:id/assign",
|
|
51
|
+
validate({ params: taskIdPathParams, body: assignTaskBody }),
|
|
52
|
+
(req, res, next) => void c.assign(req, res, next)
|
|
53
|
+
);
|
|
54
|
+
router.post(
|
|
55
|
+
"/tasks/:id/move",
|
|
56
|
+
validate({ params: taskIdPathParams, body: moveTaskBody }),
|
|
57
|
+
(req, res, next) => void c.move(req, res, next)
|
|
58
|
+
);
|
|
59
|
+
router.delete(
|
|
60
|
+
"/tasks/:id",
|
|
61
|
+
validate({ params: taskIdPathParams }),
|
|
62
|
+
(req, res, next) => void c.remove(req, res, next)
|
|
63
|
+
);
|
|
64
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Public API barrel for Sprintdock backend (SQLite, REST, MCP).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { createApplicationServices, type ServiceSet } from "./application/container.js";
|
|
8
|
+
export {
|
|
9
|
+
createRepositories,
|
|
10
|
+
type RepositorySet,
|
|
11
|
+
type StorageAdapterType,
|
|
12
|
+
type StorageConfig
|
|
13
|
+
} from "./infrastructure/repositories/repository-factory.js";
|
|
14
|
+
export { createHttpApp, type HttpAppOptions } from "./http/app-factory.js";
|
|
15
|
+
export {
|
|
16
|
+
runSprintdockMcpServer,
|
|
17
|
+
SprintdockMcpRuntime,
|
|
18
|
+
type SprintdockMcpRuntimeOptions,
|
|
19
|
+
type SprintdockMcpTransportMode
|
|
20
|
+
} from "./mcp/sprintdock-mcp-runtime.js";
|
|
21
|
+
export { bootstrapSprintdockSqlite } from "./mcp/bootstrap-sprintdock-sqlite.js";
|
|
22
|
+
export type { SprintdockSqliteBootstrapResult } from "./mcp/bootstrap-sprintdock-sqlite.js";
|
|
23
|
+
export {
|
|
24
|
+
createSprintdockMcpToolKit,
|
|
25
|
+
defaultSprintdockMcpPlugins,
|
|
26
|
+
registerSprintdockMcpTools
|
|
27
|
+
} from "./mcp/register-sprintdock-mcp-tools.js";
|
|
28
|
+
export type {
|
|
29
|
+
RegisterSprintdockMcpToolsOptions,
|
|
30
|
+
SprintdockMcpPluginContext,
|
|
31
|
+
SprintdockMcpToolDependencies,
|
|
32
|
+
SprintdockMcpToolKit,
|
|
33
|
+
SprintdockMcpToolPlugin
|
|
34
|
+
} from "./mcp/register-sprintdock-mcp-tools.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Structured audit logger contract for mutating MCP tool operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Structured audit event model.
|
|
9
|
+
*/
|
|
10
|
+
export interface AuditEvent {
|
|
11
|
+
action: string;
|
|
12
|
+
principalId: string;
|
|
13
|
+
transport: "stdio" | "http";
|
|
14
|
+
resourceId?: string;
|
|
15
|
+
correlationId: string;
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Contract for audit event emission.
|
|
21
|
+
*/
|
|
22
|
+
export interface AuditLog {
|
|
23
|
+
write(event: AuditEvent): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Console-backed audit logger for local and default runtime.
|
|
28
|
+
*/
|
|
29
|
+
export class ConsoleAuditLog implements AuditLog {
|
|
30
|
+
/** @inheritdoc */
|
|
31
|
+
public write(event: AuditEvent): void {
|
|
32
|
+
process.stderr.write(`[audit] ${JSON.stringify(event)}\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Correlation id provider for request tracking and diagnostics.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Correlation id provider used across tool and resource handlers.
|
|
10
|
+
*/
|
|
11
|
+
export class RequestCorrelation {
|
|
12
|
+
/**
|
|
13
|
+
* Creates a new unique correlation identifier.
|
|
14
|
+
*
|
|
15
|
+
* @returns Correlation id string.
|
|
16
|
+
*/
|
|
17
|
+
public create(): string {
|
|
18
|
+
return randomUUID();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: SQLite-backed PlanRepository using Drizzle ORM.
|
|
5
|
+
*/
|
|
6
|
+
import { asc, eq } from "drizzle-orm";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import type {
|
|
9
|
+
CreatePlanInput,
|
|
10
|
+
Plan,
|
|
11
|
+
UpdatePlanInput
|
|
12
|
+
} from "../../../domain/entities/plan.entity";
|
|
13
|
+
import type { PlanRepository } from "../../../domain/repositories/plan.repository";
|
|
14
|
+
import { StorageError } from "../../../errors/backend-errors.js";
|
|
15
|
+
import { plans } from "../../../db/schema/plans";
|
|
16
|
+
import type { DrizzleSqliteDb } from "./sqlite-db";
|
|
17
|
+
import { toPlanEntity } from "./row-mappers";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Persists plans with Drizzle SQLite.
|
|
21
|
+
*/
|
|
22
|
+
export class DrizzlePlanRepository implements PlanRepository {
|
|
23
|
+
public constructor(private readonly db: DrizzleSqliteDb) {}
|
|
24
|
+
|
|
25
|
+
/** @inheritdoc */
|
|
26
|
+
public async create(input: CreatePlanInput): Promise<Plan> {
|
|
27
|
+
const id = randomUUID();
|
|
28
|
+
const now = new Date().toISOString();
|
|
29
|
+
this.db
|
|
30
|
+
.insert(plans)
|
|
31
|
+
.values({
|
|
32
|
+
id,
|
|
33
|
+
slug: input.slug,
|
|
34
|
+
title: input.title,
|
|
35
|
+
description: input.description ?? null,
|
|
36
|
+
markdownContent: input.markdownContent ?? null,
|
|
37
|
+
status: "draft",
|
|
38
|
+
isActive: false,
|
|
39
|
+
createdAt: now,
|
|
40
|
+
updatedAt: now
|
|
41
|
+
})
|
|
42
|
+
.run();
|
|
43
|
+
const created = this.db.select().from(plans).where(eq(plans.id, id)).get();
|
|
44
|
+
if (!created) {
|
|
45
|
+
throw new StorageError("Failed to read plan after insert");
|
|
46
|
+
}
|
|
47
|
+
return toPlanEntity(created);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @inheritdoc */
|
|
51
|
+
public async findById(id: string): Promise<Plan | null> {
|
|
52
|
+
const row = this.db.select().from(plans).where(eq(plans.id, id)).get();
|
|
53
|
+
return row ? toPlanEntity(row) : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @inheritdoc */
|
|
57
|
+
public async findBySlug(slug: string): Promise<Plan | null> {
|
|
58
|
+
const row = this.db.select().from(plans).where(eq(plans.slug, slug)).get();
|
|
59
|
+
return row ? toPlanEntity(row) : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @inheritdoc */
|
|
63
|
+
public async findActive(): Promise<Plan | null> {
|
|
64
|
+
const row = this.db
|
|
65
|
+
.select()
|
|
66
|
+
.from(plans)
|
|
67
|
+
.where(eq(plans.isActive, true))
|
|
68
|
+
.orderBy(asc(plans.createdAt))
|
|
69
|
+
.limit(1)
|
|
70
|
+
.get();
|
|
71
|
+
return row ? toPlanEntity(row) : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @inheritdoc */
|
|
75
|
+
public async list(): Promise<Plan[]> {
|
|
76
|
+
const rows = this.db
|
|
77
|
+
.select()
|
|
78
|
+
.from(plans)
|
|
79
|
+
.orderBy(asc(plans.createdAt))
|
|
80
|
+
.all();
|
|
81
|
+
return rows.map(toPlanEntity);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @inheritdoc */
|
|
85
|
+
public async update(
|
|
86
|
+
id: string,
|
|
87
|
+
input: UpdatePlanInput
|
|
88
|
+
): Promise<Plan | null> {
|
|
89
|
+
const existing = await this.findById(id);
|
|
90
|
+
if (!existing) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
const now = new Date().toISOString();
|
|
94
|
+
this.db
|
|
95
|
+
.update(plans)
|
|
96
|
+
.set({
|
|
97
|
+
title: input.title ?? existing.title,
|
|
98
|
+
description:
|
|
99
|
+
input.description !== undefined
|
|
100
|
+
? input.description
|
|
101
|
+
: existing.description,
|
|
102
|
+
markdownContent:
|
|
103
|
+
input.markdownContent !== undefined
|
|
104
|
+
? input.markdownContent
|
|
105
|
+
: existing.markdownContent,
|
|
106
|
+
status: input.status ?? existing.status,
|
|
107
|
+
updatedAt: now
|
|
108
|
+
})
|
|
109
|
+
.where(eq(plans.id, id))
|
|
110
|
+
.run();
|
|
111
|
+
return this.findById(id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @inheritdoc */
|
|
115
|
+
public async setActive(id: string): Promise<void> {
|
|
116
|
+
this.db.transaction((tx) => {
|
|
117
|
+
tx.update(plans).set({ isActive: false }).run();
|
|
118
|
+
tx
|
|
119
|
+
.update(plans)
|
|
120
|
+
.set({
|
|
121
|
+
isActive: true,
|
|
122
|
+
updatedAt: new Date().toISOString()
|
|
123
|
+
})
|
|
124
|
+
.where(eq(plans.id, id))
|
|
125
|
+
.run();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @inheritdoc */
|
|
130
|
+
public async delete(id: string): Promise<boolean> {
|
|
131
|
+
const before = await this.findById(id);
|
|
132
|
+
if (!before) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
this.db.delete(plans).where(eq(plans.id, id)).run();
|
|
136
|
+
return (await this.findById(id)) === null;
|
|
137
|
+
}
|
|
138
|
+
}
|