@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,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
+ }