@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,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Drizzle schema for tasks and task dependency edges.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
sqliteTable,
|
|
8
|
+
text,
|
|
9
|
+
integer,
|
|
10
|
+
primaryKey,
|
|
11
|
+
uniqueIndex
|
|
12
|
+
} from "drizzle-orm/sqlite-core";
|
|
13
|
+
import { sprints } from "./sprints";
|
|
14
|
+
|
|
15
|
+
export const tasks = sqliteTable("tasks", {
|
|
16
|
+
id: text("id").primaryKey(),
|
|
17
|
+
sprintId: text("sprint_id")
|
|
18
|
+
.notNull()
|
|
19
|
+
.references(() => sprints.id, { onDelete: "cascade" }),
|
|
20
|
+
title: text("title").notNull(),
|
|
21
|
+
description: text("description"),
|
|
22
|
+
status: text("status").notNull(),
|
|
23
|
+
priority: text("priority").notNull(),
|
|
24
|
+
taskOrder: integer("order").notNull().default(0),
|
|
25
|
+
assignee: text("assignee"),
|
|
26
|
+
tags: text("tags", { mode: "json" }).$type<string[]>(),
|
|
27
|
+
createdAt: text("created_at").notNull(),
|
|
28
|
+
updatedAt: text("updated_at").notNull()
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const taskDependencies = sqliteTable(
|
|
32
|
+
"task_dependencies",
|
|
33
|
+
{
|
|
34
|
+
taskId: text("task_id")
|
|
35
|
+
.notNull()
|
|
36
|
+
.references(() => tasks.id, { onDelete: "cascade" }),
|
|
37
|
+
dependsOnTaskId: text("depends_on_task_id")
|
|
38
|
+
.notNull()
|
|
39
|
+
.references(() => tasks.id, { onDelete: "cascade" })
|
|
40
|
+
},
|
|
41
|
+
(t) => ({
|
|
42
|
+
pk: primaryKey({ columns: [t.taskId, t.dependsOnTaskId] })
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export const taskTouchedFiles = sqliteTable(
|
|
47
|
+
"task_touched_files",
|
|
48
|
+
{
|
|
49
|
+
id: text("id").primaryKey(),
|
|
50
|
+
taskId: text("task_id")
|
|
51
|
+
.notNull()
|
|
52
|
+
.references(() => tasks.id, { onDelete: "cascade" }),
|
|
53
|
+
path: text("path").notNull(),
|
|
54
|
+
fileType: text("file_type").notNull()
|
|
55
|
+
},
|
|
56
|
+
(t) => ({
|
|
57
|
+
taskPathUnique: uniqueIndex("task_touched_files_task_id_path_unique").on(
|
|
58
|
+
t.taskId,
|
|
59
|
+
t.path
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Barrel export for domain entities and value types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type {
|
|
8
|
+
CreatePlanInput,
|
|
9
|
+
Plan,
|
|
10
|
+
PlanStatus,
|
|
11
|
+
UpdatePlanInput
|
|
12
|
+
} from "./plan.entity";
|
|
13
|
+
export type {
|
|
14
|
+
CreateSprintInput,
|
|
15
|
+
Sprint,
|
|
16
|
+
SprintStatus,
|
|
17
|
+
UpdateSprintInput
|
|
18
|
+
} from "./sprint.entity";
|
|
19
|
+
export type {
|
|
20
|
+
CreateTaskInput,
|
|
21
|
+
MoveTaskInput,
|
|
22
|
+
Task,
|
|
23
|
+
TaskDependency,
|
|
24
|
+
TaskPriority,
|
|
25
|
+
TaskStatus,
|
|
26
|
+
TaskTouchedFile,
|
|
27
|
+
TaskTouchedFileInput,
|
|
28
|
+
TaskTouchedFileType,
|
|
29
|
+
UpdateTaskInput
|
|
30
|
+
} from "./task.entity";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Plan aggregate root types for the domain layer.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type PlanStatus = "draft" | "active" | "completed" | "archived";
|
|
8
|
+
|
|
9
|
+
export interface Plan {
|
|
10
|
+
id: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string | null;
|
|
14
|
+
markdownContent: string | null;
|
|
15
|
+
status: PlanStatus;
|
|
16
|
+
isActive: boolean;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CreatePlanInput {
|
|
22
|
+
slug: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description?: string | null;
|
|
25
|
+
markdownContent?: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UpdatePlanInput {
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string | null;
|
|
31
|
+
markdownContent?: string | null;
|
|
32
|
+
status?: PlanStatus;
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Sprint entity types scoped to a plan.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type SprintStatus = "planned" | "active" | "completed" | "archived";
|
|
8
|
+
|
|
9
|
+
export interface Sprint {
|
|
10
|
+
id: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
planId: string;
|
|
13
|
+
name: string;
|
|
14
|
+
goal: string;
|
|
15
|
+
/** Optional sprint notes / retro / scope — GitHub-flavored markdown. */
|
|
16
|
+
markdownContent: string | null;
|
|
17
|
+
status: SprintStatus;
|
|
18
|
+
order: number;
|
|
19
|
+
startDate: string | null;
|
|
20
|
+
endDate: string | null;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CreateSprintInput {
|
|
26
|
+
slug: string;
|
|
27
|
+
planId: string;
|
|
28
|
+
name: string;
|
|
29
|
+
goal: string;
|
|
30
|
+
markdownContent?: string | null;
|
|
31
|
+
order?: number;
|
|
32
|
+
startDate?: string | null;
|
|
33
|
+
endDate?: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UpdateSprintInput {
|
|
37
|
+
name?: string;
|
|
38
|
+
goal?: string;
|
|
39
|
+
markdownContent?: string | null;
|
|
40
|
+
status?: SprintStatus;
|
|
41
|
+
order?: number;
|
|
42
|
+
startDate?: string | null;
|
|
43
|
+
endDate?: string | null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Task and dependency types for the domain layer.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type TaskStatus = "todo" | "in_progress" | "blocked" | "done";
|
|
8
|
+
|
|
9
|
+
export type TaskPriority = "low" | "medium" | "high" | "critical";
|
|
10
|
+
|
|
11
|
+
/** Persisted in `task_touched_files.file_type`. */
|
|
12
|
+
export type TaskTouchedFileType =
|
|
13
|
+
| "test"
|
|
14
|
+
| "implementation"
|
|
15
|
+
| "doc"
|
|
16
|
+
| "config"
|
|
17
|
+
| "other";
|
|
18
|
+
|
|
19
|
+
export interface TaskTouchedFile {
|
|
20
|
+
id: string;
|
|
21
|
+
taskId: string;
|
|
22
|
+
path: string;
|
|
23
|
+
fileType: TaskTouchedFileType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Task {
|
|
27
|
+
id: string;
|
|
28
|
+
sprintId: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description: string | null;
|
|
31
|
+
status: TaskStatus;
|
|
32
|
+
priority: TaskPriority;
|
|
33
|
+
order: number;
|
|
34
|
+
assignee: string | null;
|
|
35
|
+
tags: string[] | null;
|
|
36
|
+
/** Files touched for this task (tests, implementation, docs, …). */
|
|
37
|
+
touchedFiles: TaskTouchedFile[];
|
|
38
|
+
createdAt: string;
|
|
39
|
+
updatedAt: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TaskDependency {
|
|
43
|
+
taskId: string;
|
|
44
|
+
dependsOnTaskId: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Payload row for create/update (no id / taskId). */
|
|
48
|
+
export interface TaskTouchedFileInput {
|
|
49
|
+
path: string;
|
|
50
|
+
fileType: TaskTouchedFileType;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface CreateTaskInput {
|
|
54
|
+
sprintId: string;
|
|
55
|
+
title: string;
|
|
56
|
+
description?: string | null;
|
|
57
|
+
priority: TaskPriority;
|
|
58
|
+
order?: number;
|
|
59
|
+
assignee?: string | null;
|
|
60
|
+
tags?: string[] | null;
|
|
61
|
+
dependsOnTaskIds?: string[];
|
|
62
|
+
touchedFiles?: TaskTouchedFileInput[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface UpdateTaskInput {
|
|
66
|
+
title?: string;
|
|
67
|
+
description?: string | null;
|
|
68
|
+
status?: TaskStatus;
|
|
69
|
+
priority?: TaskPriority;
|
|
70
|
+
order?: number;
|
|
71
|
+
assignee?: string | null;
|
|
72
|
+
tags?: string[] | null;
|
|
73
|
+
/** When set, replaces all touched-file rows for this task. */
|
|
74
|
+
touchedFiles?: TaskTouchedFileInput[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface MoveTaskInput {
|
|
78
|
+
taskId: string;
|
|
79
|
+
targetSprintId: string;
|
|
80
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Barrel export for domain repository ports.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { PlanRepository } from "./plan.repository";
|
|
8
|
+
export type { SprintRepository } from "./sprint.repository";
|
|
9
|
+
export type { TaskFilter, TaskRepository } from "./task.repository";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Plan persistence port implemented by infrastructure adapters.
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
CreatePlanInput,
|
|
8
|
+
Plan,
|
|
9
|
+
UpdatePlanInput
|
|
10
|
+
} from "../entities/plan.entity";
|
|
11
|
+
|
|
12
|
+
export interface PlanRepository {
|
|
13
|
+
create(input: CreatePlanInput): Promise<Plan>;
|
|
14
|
+
findById(id: string): Promise<Plan | null>;
|
|
15
|
+
findBySlug(slug: string): Promise<Plan | null>;
|
|
16
|
+
findActive(): Promise<Plan | null>;
|
|
17
|
+
list(): Promise<Plan[]>;
|
|
18
|
+
update(id: string, input: UpdatePlanInput): Promise<Plan | null>;
|
|
19
|
+
setActive(id: string): Promise<void>;
|
|
20
|
+
delete(id: string): Promise<boolean>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Sprint persistence port implemented by infrastructure adapters.
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
CreateSprintInput,
|
|
8
|
+
Sprint,
|
|
9
|
+
UpdateSprintInput
|
|
10
|
+
} from "../entities/sprint.entity";
|
|
11
|
+
|
|
12
|
+
export interface SprintRepository {
|
|
13
|
+
create(input: CreateSprintInput): Promise<Sprint>;
|
|
14
|
+
findById(id: string): Promise<Sprint | null>;
|
|
15
|
+
findBySlug(planId: string, slug: string): Promise<Sprint | null>;
|
|
16
|
+
listByPlanId(planId: string): Promise<Sprint[]>;
|
|
17
|
+
update(id: string, input: UpdateSprintInput): Promise<Sprint | null>;
|
|
18
|
+
delete(id: string): Promise<boolean>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Task persistence port implemented by infrastructure adapters.
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
CreateTaskInput,
|
|
8
|
+
Task,
|
|
9
|
+
TaskDependency,
|
|
10
|
+
TaskPriority,
|
|
11
|
+
TaskStatus,
|
|
12
|
+
UpdateTaskInput
|
|
13
|
+
} from "../entities/task.entity";
|
|
14
|
+
|
|
15
|
+
export interface TaskFilter {
|
|
16
|
+
status?: TaskStatus;
|
|
17
|
+
priority?: TaskPriority;
|
|
18
|
+
assignee?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TaskRepository {
|
|
22
|
+
create(input: CreateTaskInput): Promise<Task>;
|
|
23
|
+
findById(id: string): Promise<Task | null>;
|
|
24
|
+
listBySprintId(sprintId: string, filter?: TaskFilter): Promise<Task[]>;
|
|
25
|
+
listByPlanId(planId: string, filter?: TaskFilter): Promise<Task[]>;
|
|
26
|
+
update(id: string, input: UpdateTaskInput): Promise<Task | null>;
|
|
27
|
+
move(taskId: string, targetSprintId: string): Promise<Task | null>;
|
|
28
|
+
delete(id: string): Promise<boolean>;
|
|
29
|
+
getDependencies(taskId: string): Promise<TaskDependency[]>;
|
|
30
|
+
/** Task IDs that list {@link dependsOnTaskId} as a prerequisite. */
|
|
31
|
+
listDependentTaskIds(dependsOnTaskId: string): Promise<string[]>;
|
|
32
|
+
setDependencies(taskId: string, dependsOnTaskIds: string[]): Promise<void>;
|
|
33
|
+
/** Count tasks in the database grouped by status (all plans). */
|
|
34
|
+
countByStatusGlobally(): Promise<Record<TaskStatus, number>>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Barrel export for domain services.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { PlanDomainService } from "./plan-domain.service";
|
|
8
|
+
export { SprintDomainService } from "./sprint-domain.service";
|
|
9
|
+
export { TaskDomainService } from "./task-domain.service";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Plan lifecycle transition rules for the domain layer.
|
|
5
|
+
*/
|
|
6
|
+
import { ValidationError } from "../../errors/backend-errors.js";
|
|
7
|
+
import type { PlanStatus } from "../entities/plan.entity";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates allowed plan status transitions (draft through archived).
|
|
11
|
+
*/
|
|
12
|
+
export class PlanDomainService {
|
|
13
|
+
private readonly transitions: Record<PlanStatus, readonly PlanStatus[]> = {
|
|
14
|
+
draft: ["active"],
|
|
15
|
+
active: ["completed", "archived"],
|
|
16
|
+
completed: ["archived"],
|
|
17
|
+
archived: []
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param from Current plan status.
|
|
22
|
+
* @param to Requested plan status.
|
|
23
|
+
* @returns True when the transition is allowed (including no-op same status).
|
|
24
|
+
*/
|
|
25
|
+
public canTransitionStatus(from: PlanStatus, to: PlanStatus): boolean {
|
|
26
|
+
if (from === to) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return this.transitions[from].includes(to);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param from Current plan status.
|
|
34
|
+
* @param to Requested plan status.
|
|
35
|
+
* @throws ValidationError when the transition is not allowed.
|
|
36
|
+
*/
|
|
37
|
+
public validateTransition(from: PlanStatus, to: PlanStatus): void {
|
|
38
|
+
if (!this.canTransitionStatus(from, to)) {
|
|
39
|
+
throw new ValidationError(
|
|
40
|
+
`Illegal plan status transition: ${from} -> ${to}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Sprint lifecycle transition rules for the domain layer.
|
|
5
|
+
*/
|
|
6
|
+
import { ValidationError } from "../../errors/backend-errors.js";
|
|
7
|
+
import type { SprintStatus } from "../entities/sprint.entity";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates allowed sprint status transitions (planned through archived).
|
|
11
|
+
*/
|
|
12
|
+
export class SprintDomainService {
|
|
13
|
+
private readonly transitions: Record<SprintStatus, readonly SprintStatus[]> = {
|
|
14
|
+
planned: ["active"],
|
|
15
|
+
active: ["completed", "archived"],
|
|
16
|
+
completed: ["archived"],
|
|
17
|
+
archived: []
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param from Current sprint status.
|
|
22
|
+
* @param to Requested sprint status.
|
|
23
|
+
* @returns True when the transition is allowed (including no-op same status).
|
|
24
|
+
*/
|
|
25
|
+
public canTransitionStatus(from: SprintStatus, to: SprintStatus): boolean {
|
|
26
|
+
if (from === to) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return this.transitions[from].includes(to);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param from Current sprint status.
|
|
34
|
+
* @param to Requested sprint status.
|
|
35
|
+
* @throws ValidationError when the transition is not allowed.
|
|
36
|
+
*/
|
|
37
|
+
public validateTransition(from: SprintStatus, to: SprintStatus): void {
|
|
38
|
+
if (!this.canTransitionStatus(from, to)) {
|
|
39
|
+
throw new ValidationError(
|
|
40
|
+
`Illegal sprint status transition: ${from} -> ${to}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Task workflow, ordering, and dependency graph validation.
|
|
5
|
+
*/
|
|
6
|
+
import { ValidationError } from "../../errors/backend-errors.js";
|
|
7
|
+
import type { Task, TaskDependency, TaskStatus } from "../entities/task.entity";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Enforces task status moves, unique ordering, and acyclic dependencies.
|
|
11
|
+
*/
|
|
12
|
+
export class TaskDomainService {
|
|
13
|
+
private readonly transitions: Record<TaskStatus, readonly TaskStatus[]> = {
|
|
14
|
+
todo: ["in_progress", "blocked", "done"],
|
|
15
|
+
in_progress: ["blocked", "done", "todo"],
|
|
16
|
+
blocked: ["in_progress", "todo"],
|
|
17
|
+
/** Allow reopening from Done on the Kanban (done → any non-terminal workflow state). */
|
|
18
|
+
done: ["todo", "in_progress", "blocked"]
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param from Current task status.
|
|
23
|
+
* @param to Requested task status.
|
|
24
|
+
* @returns True when the transition is allowed (including no-op same status).
|
|
25
|
+
*/
|
|
26
|
+
public canTransitionStatus(from: TaskStatus, to: TaskStatus): boolean {
|
|
27
|
+
if (from === to) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return this.transitions[from].includes(to);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param from Current task status.
|
|
35
|
+
* @param to Requested task status.
|
|
36
|
+
* @throws ValidationError when the transition is not allowed.
|
|
37
|
+
*/
|
|
38
|
+
public validateTransition(from: TaskStatus, to: TaskStatus): void {
|
|
39
|
+
if (!this.canTransitionStatus(from, to)) {
|
|
40
|
+
throw new ValidationError(
|
|
41
|
+
`Illegal task status transition: ${from} -> ${to}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param tasks Tasks whose {@link Task.order} values must be unique within this set.
|
|
48
|
+
* @throws ValidationError when two tasks share the same order value.
|
|
49
|
+
*/
|
|
50
|
+
public validateOrderUniqueness(tasks: Task[]): void {
|
|
51
|
+
const seen = new Set<number>();
|
|
52
|
+
for (const t of tasks) {
|
|
53
|
+
if (seen.has(t.order)) {
|
|
54
|
+
throw new ValidationError(`Duplicate task order: ${t.order}`);
|
|
55
|
+
}
|
|
56
|
+
seen.add(t.order);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensures dependencies only reference known tasks and form a directed acyclic graph.
|
|
62
|
+
*
|
|
63
|
+
* @param tasks Tasks participating in the graph.
|
|
64
|
+
* @param dependencies Directed edges: taskId depends on dependsOnTaskId.
|
|
65
|
+
* @throws ValidationError when an endpoint is missing or a cycle exists.
|
|
66
|
+
*/
|
|
67
|
+
public validateDependencyGraph(
|
|
68
|
+
tasks: Task[],
|
|
69
|
+
dependencies: TaskDependency[]
|
|
70
|
+
): void {
|
|
71
|
+
const ids = new Set(tasks.map((t) => t.id));
|
|
72
|
+
for (const d of dependencies) {
|
|
73
|
+
if (!ids.has(d.taskId) || !ids.has(d.dependsOnTaskId)) {
|
|
74
|
+
throw new ValidationError(
|
|
75
|
+
`Dependency references unknown task (${d.taskId} -> ${d.dependsOnTaskId})`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const adj = new Map<string, string[]>();
|
|
81
|
+
for (const id of ids) {
|
|
82
|
+
adj.set(id, []);
|
|
83
|
+
}
|
|
84
|
+
for (const d of dependencies) {
|
|
85
|
+
adj.get(d.taskId)!.push(d.dependsOnTaskId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const color = new Map<string, number>();
|
|
89
|
+
for (const id of ids) {
|
|
90
|
+
color.set(id, 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const dfs = (u: string, stack: string[]): void => {
|
|
94
|
+
color.set(u, 1);
|
|
95
|
+
stack.push(u);
|
|
96
|
+
for (const v of adj.get(u) ?? []) {
|
|
97
|
+
const c = color.get(v) ?? 0;
|
|
98
|
+
if (c === 1) {
|
|
99
|
+
const i = stack.indexOf(v);
|
|
100
|
+
const segment = stack.slice(i);
|
|
101
|
+
throw new ValidationError(
|
|
102
|
+
`Dependency cycle: ${segment.join(" -> ")} -> ${v}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (c === 0) {
|
|
106
|
+
dfs(v, stack);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
stack.pop();
|
|
110
|
+
color.set(u, 2);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
for (const id of ids) {
|
|
114
|
+
if (color.get(id) === 0) {
|
|
115
|
+
dfs(id, []);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param dependsOnIds Task ids that must exist as prerequisites.
|
|
122
|
+
* @param existingTaskIds Task ids that are allowed as dependency targets.
|
|
123
|
+
* @throws ValidationError when any prerequisite id is not in the allowed set.
|
|
124
|
+
*/
|
|
125
|
+
public validateDependencyReferences(
|
|
126
|
+
dependsOnIds: string[],
|
|
127
|
+
existingTaskIds: string[]
|
|
128
|
+
): void {
|
|
129
|
+
const allowed = new Set(existingTaskIds);
|
|
130
|
+
for (const id of dependsOnIds) {
|
|
131
|
+
if (!allowed.has(id)) {
|
|
132
|
+
throw new ValidationError(`Unknown dependency target task: ${id}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* package: @sprintdock/backend
|
|
3
|
+
* author: vikash sharma
|
|
4
|
+
* description: Typed HTTP-oriented error hierarchy for domain and infrastructure failures.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base error carrying an HTTP-style status code for protocol adapters.
|
|
9
|
+
*/
|
|
10
|
+
export class BackendError extends Error {
|
|
11
|
+
public readonly statusCode: number;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param message Human-readable error message.
|
|
15
|
+
* @param statusCode HTTP status code for mapping in REST/MCP layers.
|
|
16
|
+
*/
|
|
17
|
+
public constructor(message: string, statusCode: number) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "BackendError";
|
|
20
|
+
this.statusCode = statusCode;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Raised when a requested entity does not exist.
|
|
26
|
+
*/
|
|
27
|
+
export class NotFoundError extends BackendError {
|
|
28
|
+
/**
|
|
29
|
+
* @param entityName Domain entity label (for example, Plan or Task).
|
|
30
|
+
* @param id Identifier that was not found.
|
|
31
|
+
*/
|
|
32
|
+
public constructor(entityName: string, id: string) {
|
|
33
|
+
super(`${entityName} not found: ${id}`, 404);
|
|
34
|
+
this.name = "NotFoundError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Raised when input or state violates business or schema rules.
|
|
40
|
+
*/
|
|
41
|
+
export class ValidationError extends BackendError {
|
|
42
|
+
/**
|
|
43
|
+
* @param message Human-readable validation message.
|
|
44
|
+
*/
|
|
45
|
+
public constructor(message: string) {
|
|
46
|
+
super(message, 400);
|
|
47
|
+
this.name = "ValidationError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Raised when an operation conflicts with current persisted state.
|
|
53
|
+
*/
|
|
54
|
+
export class ConflictError extends BackendError {
|
|
55
|
+
/**
|
|
56
|
+
* @param message Human-readable conflict description.
|
|
57
|
+
*/
|
|
58
|
+
public constructor(message: string) {
|
|
59
|
+
super(message, 409);
|
|
60
|
+
this.name = "ConflictError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Raised when persistence or storage operations fail unexpectedly.
|
|
66
|
+
*/
|
|
67
|
+
export class StorageError extends BackendError {
|
|
68
|
+
/**
|
|
69
|
+
* @param message Human-readable storage failure message.
|
|
70
|
+
*/
|
|
71
|
+
public constructor(message: string) {
|
|
72
|
+
super(message, 500);
|
|
73
|
+
this.name = "StorageError";
|
|
74
|
+
}
|
|
75
|
+
}
|