@critical-path/core 0.5.2 → 0.7.0
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 +40 -0
- package/README.md +57 -21
- package/dist/domain/custom-fields.d.ts +9 -0
- package/dist/domain/custom-fields.d.ts.map +1 -0
- package/dist/domain/custom-fields.js +72 -0
- package/dist/domain/custom-fields.js.map +1 -0
- package/dist/domain/ddd.test.d.ts +2 -0
- package/dist/domain/ddd.test.d.ts.map +1 -0
- package/dist/domain/ddd.test.js +246 -0
- package/dist/domain/ddd.test.js.map +1 -0
- package/dist/domain/entities.d.ts +82 -0
- package/dist/domain/entities.d.ts.map +1 -0
- package/dist/domain/entities.js +321 -0
- package/dist/domain/entities.js.map +1 -0
- package/dist/domain/events.d.ts +153 -0
- package/dist/domain/events.d.ts.map +1 -0
- package/dist/domain/events.js +35 -0
- package/dist/domain/events.js.map +1 -0
- package/dist/domain/graph.d.ts +19 -0
- package/dist/domain/graph.d.ts.map +1 -0
- package/dist/domain/graph.js +58 -0
- package/dist/domain/graph.js.map +1 -0
- package/dist/domain/index.d.ts +5 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +5 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/engine/index.d.ts +32 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +416 -6
- package/dist/engine/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/storage/file-storage.d.ts +24 -0
- package/dist/storage/file-storage.d.ts.map +1 -0
- package/dist/storage/file-storage.js +73 -0
- package/dist/storage/file-storage.js.map +1 -0
- package/dist/storage/firebase-storage.d.ts +48 -0
- package/dist/storage/firebase-storage.d.ts.map +1 -0
- package/dist/storage/firebase-storage.js +142 -0
- package/dist/storage/firebase-storage.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/s3.d.ts +36 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +135 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/storage.test.d.ts +2 -0
- package/dist/storage/storage.test.d.ts.map +1 -0
- package/dist/storage/storage.test.js +117 -0
- package/dist/storage/storage.test.js.map +1 -0
- package/dist/store/firebase.d.ts +12 -1
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +63 -1
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/index.d.ts +49 -2
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +49 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +12 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +128 -6
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sqlite.test.js +47 -1
- package/dist/store/sqlite.test.js.map +1 -1
- package/dist/types/index.d.ts +57 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/domain/custom-fields.ts +131 -0
- package/src/domain/ddd.test.ts +307 -0
- package/src/domain/entities.ts +390 -0
- package/src/domain/events.ts +196 -0
- package/src/domain/graph.ts +70 -0
- package/src/domain/index.ts +4 -0
- package/src/engine/index.ts +518 -8
- package/src/index.ts +2 -0
- package/src/storage/file-storage.ts +96 -0
- package/src/storage/firebase-storage.ts +177 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/s3.ts +165 -0
- package/src/storage/storage.test.ts +146 -0
- package/src/store/firebase.ts +64 -1
- package/src/store/index.ts +99 -11
- package/src/store/sqlite.test.ts +59 -1
- package/src/store/sqlite.ts +172 -6
- package/src/types/index.ts +70 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/engine/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { CriticalPathConfig, Project, Task, TaskDependencyGraph, Team, TaskContainer, Iteration, Workflow } from '../types/index.js';
|
|
1
|
+
import type { CriticalPathConfig, Project, Task, TaskDependency, TaskDependencyGraph, Team, TaskContainer, Iteration, Comment, Attachment, FileStorageAdapter, UploadFileInput, PresignedUrlOptions, PresignedUploadResult, TimeEntry, Workflow, CreateTaskInput } from '../types/index.js';
|
|
2
2
|
import { StorageAdapter } from '../store/index.js';
|
|
3
3
|
import { PluginRegistry } from '../plugins/index.js';
|
|
4
4
|
import { type TaskLifecycleState } from '../utils/status.js';
|
|
5
|
+
import { DomainEventBus } from '../domain/events.js';
|
|
5
6
|
export declare class CriticalPathEngine {
|
|
6
7
|
readonly store: StorageAdapter;
|
|
8
|
+
readonly fileStorage?: FileStorageAdapter;
|
|
7
9
|
readonly plugins: PluginRegistry;
|
|
10
|
+
readonly events: DomainEventBus;
|
|
8
11
|
constructor(config?: CriticalPathConfig);
|
|
9
12
|
private seedInitialData;
|
|
10
13
|
getWorkflows(): Promise<Workflow[]>;
|
|
@@ -17,13 +20,40 @@ export declare class CriticalPathEngine {
|
|
|
17
20
|
getProjects(): Promise<Project[]>;
|
|
18
21
|
getProject(id: string): Promise<Project | null>;
|
|
19
22
|
createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project>;
|
|
23
|
+
updateProject(id: string, updates: Partial<Project>): Promise<Project | null>;
|
|
20
24
|
getTasks(projectId?: string): Promise<Task[]>;
|
|
21
25
|
getTask(id: string): Promise<Task | null>;
|
|
22
|
-
createTask(taskInput:
|
|
26
|
+
createTask(taskInput: CreateTaskInput): Promise<Task>;
|
|
23
27
|
updateTask(id: string, updates: Partial<Task>): Promise<Task | null>;
|
|
24
28
|
deleteTask(id: string): Promise<boolean>;
|
|
29
|
+
addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency>;
|
|
25
30
|
getTaskDependencyGraph(taskId: string): Promise<TaskDependencyGraph>;
|
|
26
31
|
getTaskLifecycleState(taskId: string): Promise<TaskLifecycleState | null>;
|
|
32
|
+
logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt' | 'userId'> & {
|
|
33
|
+
userId?: string;
|
|
34
|
+
}): Promise<TimeEntry>;
|
|
35
|
+
getComments(taskId: string): Promise<Comment[]>;
|
|
36
|
+
getComment(id: string): Promise<Comment | null>;
|
|
37
|
+
addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment>;
|
|
38
|
+
updateComment(id: string, updates: Partial<Comment>): Promise<Comment | null>;
|
|
39
|
+
deleteComment(id: string): Promise<boolean>;
|
|
40
|
+
getAttachments(filter?: {
|
|
41
|
+
taskId?: string;
|
|
42
|
+
projectId?: string;
|
|
43
|
+
commentId?: string;
|
|
44
|
+
}): Promise<Attachment[]>;
|
|
45
|
+
getAttachment(id: string): Promise<Attachment | null>;
|
|
46
|
+
createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment>;
|
|
47
|
+
deleteAttachment(id: string): Promise<boolean>;
|
|
48
|
+
uploadAttachmentFile(input: UploadFileInput & {
|
|
49
|
+
taskId?: string;
|
|
50
|
+
projectId?: string;
|
|
51
|
+
commentId?: string;
|
|
52
|
+
uploaderId: string;
|
|
53
|
+
uploaderType?: 'user' | 'agent' | 'system';
|
|
54
|
+
metadata?: Record<string, unknown>;
|
|
55
|
+
}): Promise<Attachment>;
|
|
56
|
+
getPresignedAttachmentUploadUrl(options: PresignedUrlOptions): Promise<PresignedUploadResult>;
|
|
27
57
|
getTeams(): Promise<Team[]>;
|
|
28
58
|
getTeam(id: string): Promise<Team | null>;
|
|
29
59
|
createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/engine/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,OAAO,EACP,IAAI,EACJ,mBAAmB,EACnB,IAAI,EACJ,aAAa,EACb,SAAS,EAET,QAAQ,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/engine/index.ts"],"names":[],"mappings":"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"}
|
package/dist/engine/index.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { InMemoryStore } from '../store/index.js';
|
|
2
2
|
import { PluginRegistry } from '../plugins/index.js';
|
|
3
|
-
import { deriveTaskLifecycleState } from '../utils/status.js';
|
|
3
|
+
import { deriveTaskLifecycleState, resolveStatusDefinition } from '../utils/status.js';
|
|
4
4
|
import { validateTransition, getAllowedNextStatuses, WorkflowValidationError, DEFAULT_SOFTWARE_WORKFLOW } from '../utils/workflow.js';
|
|
5
|
+
import { DomainEventBus } from '../domain/events.js';
|
|
6
|
+
import { validateCustomFieldValues } from '../domain/custom-fields.js';
|
|
7
|
+
import { detectDependencyCycle, CircularDependencyError } from '../domain/graph.js';
|
|
5
8
|
export class CriticalPathEngine {
|
|
6
9
|
store;
|
|
10
|
+
fileStorage;
|
|
7
11
|
plugins;
|
|
12
|
+
events;
|
|
8
13
|
constructor(config = {}) {
|
|
9
14
|
this.store = typeof config.store === 'object' && config.store !== null
|
|
10
15
|
? config.store
|
|
11
16
|
: new InMemoryStore();
|
|
17
|
+
this.fileStorage = config.fileStorage;
|
|
12
18
|
this.plugins = new PluginRegistry();
|
|
19
|
+
this.events = new DomainEventBus();
|
|
13
20
|
if (config.plugins) {
|
|
14
21
|
for (const plugin of config.plugins) {
|
|
15
22
|
this.plugins.register(plugin);
|
|
@@ -60,6 +67,16 @@ export class CriticalPathEngine {
|
|
|
60
67
|
}
|
|
61
68
|
async createWorkflow(workflow) {
|
|
62
69
|
const created = await this.store.createWorkflow(workflow);
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
const event = {
|
|
72
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
73
|
+
name: 'workflow.created',
|
|
74
|
+
aggregateId: created.id,
|
|
75
|
+
aggregateType: 'Workflow',
|
|
76
|
+
occurredAt: now,
|
|
77
|
+
payload: { workflow: created }
|
|
78
|
+
};
|
|
79
|
+
await this.events.publish(event);
|
|
63
80
|
await this.store.logActivity({
|
|
64
81
|
actorId: 'system',
|
|
65
82
|
action: 'workflow.created',
|
|
@@ -74,6 +91,16 @@ export class CriticalPathEngine {
|
|
|
74
91
|
return null;
|
|
75
92
|
const updated = await this.store.updateWorkflow(id, updates);
|
|
76
93
|
if (updated) {
|
|
94
|
+
const now = new Date().toISOString();
|
|
95
|
+
const event = {
|
|
96
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
97
|
+
name: 'workflow.updated',
|
|
98
|
+
aggregateId: updated.id,
|
|
99
|
+
aggregateType: 'Workflow',
|
|
100
|
+
occurredAt: now,
|
|
101
|
+
payload: { workflow: updated, previous: existing }
|
|
102
|
+
};
|
|
103
|
+
await this.events.publish(event);
|
|
77
104
|
await this.store.logActivity({
|
|
78
105
|
actorId: 'system',
|
|
79
106
|
action: 'workflow.updated',
|
|
@@ -89,6 +116,16 @@ export class CriticalPathEngine {
|
|
|
89
116
|
return false;
|
|
90
117
|
const deleted = await this.store.deleteWorkflow(id);
|
|
91
118
|
if (deleted) {
|
|
119
|
+
const now = new Date().toISOString();
|
|
120
|
+
const event = {
|
|
121
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
122
|
+
name: 'workflow.deleted',
|
|
123
|
+
aggregateId: id,
|
|
124
|
+
aggregateType: 'Workflow',
|
|
125
|
+
occurredAt: now,
|
|
126
|
+
payload: { workflowId: id, name: existing.name }
|
|
127
|
+
};
|
|
128
|
+
await this.events.publish(event);
|
|
92
129
|
await this.store.logActivity({
|
|
93
130
|
actorId: 'system',
|
|
94
131
|
action: 'workflow.deleted',
|
|
@@ -131,6 +168,16 @@ export class CriticalPathEngine {
|
|
|
131
168
|
}
|
|
132
169
|
async createProject(project) {
|
|
133
170
|
const created = await this.store.createProject(project);
|
|
171
|
+
const now = new Date().toISOString();
|
|
172
|
+
const event = {
|
|
173
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
174
|
+
name: 'project.created',
|
|
175
|
+
aggregateId: created.id,
|
|
176
|
+
aggregateType: 'Project',
|
|
177
|
+
occurredAt: now,
|
|
178
|
+
payload: { project: created }
|
|
179
|
+
};
|
|
180
|
+
await this.events.publish(event);
|
|
134
181
|
await this.store.logActivity({
|
|
135
182
|
projectId: created.id,
|
|
136
183
|
actorId: project.ownerId || 'system',
|
|
@@ -140,6 +187,32 @@ export class CriticalPathEngine {
|
|
|
140
187
|
this.dispatchWebhook('project.created', { project: created });
|
|
141
188
|
return created;
|
|
142
189
|
}
|
|
190
|
+
async updateProject(id, updates) {
|
|
191
|
+
const existing = await this.store.getProject(id);
|
|
192
|
+
if (!existing)
|
|
193
|
+
return null;
|
|
194
|
+
const updated = await this.store.updateProject(id, updates);
|
|
195
|
+
if (updated) {
|
|
196
|
+
const now = new Date().toISOString();
|
|
197
|
+
const event = {
|
|
198
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
199
|
+
name: 'project.updated',
|
|
200
|
+
aggregateId: updated.id,
|
|
201
|
+
aggregateType: 'Project',
|
|
202
|
+
occurredAt: now,
|
|
203
|
+
payload: { project: updated, previous: existing }
|
|
204
|
+
};
|
|
205
|
+
await this.events.publish(event);
|
|
206
|
+
await this.store.logActivity({
|
|
207
|
+
projectId: updated.id,
|
|
208
|
+
actorId: 'system',
|
|
209
|
+
action: 'project.updated',
|
|
210
|
+
details: { name: updated.name }
|
|
211
|
+
});
|
|
212
|
+
this.dispatchWebhook('project.updated', { project: updated });
|
|
213
|
+
}
|
|
214
|
+
return updated;
|
|
215
|
+
}
|
|
143
216
|
// --- Tasks ---
|
|
144
217
|
async getTasks(projectId) {
|
|
145
218
|
return this.store.getTasks(projectId);
|
|
@@ -150,13 +223,24 @@ export class CriticalPathEngine {
|
|
|
150
223
|
async createTask(taskInput) {
|
|
151
224
|
const processedInput = await this.plugins.runBeforeTaskCreate(taskInput);
|
|
152
225
|
const projectId = processedInput.projectId || taskInput.projectId;
|
|
226
|
+
const project = await this.store.getProject(projectId);
|
|
153
227
|
const workflow = await this.resolveProjectWorkflow(projectId);
|
|
228
|
+
// Validate custom field domain invariants
|
|
229
|
+
if (project?.customFieldDefinitions && (processedInput.customFields || taskInput.customFields)) {
|
|
230
|
+
validateCustomFieldValues(project.customFieldDefinitions, processedInput.customFields || taskInput.customFields);
|
|
231
|
+
}
|
|
154
232
|
const defaultStatus = workflow?.defaultStatusKey || 'todo';
|
|
233
|
+
const initialStatus = processedInput.status || taskInput.status || defaultStatus;
|
|
234
|
+
const now = new Date().toISOString();
|
|
235
|
+
// Derive initial lifecycle timestamps
|
|
236
|
+
const statusDef = resolveStatusDefinition(initialStatus, project?.statusDefinitions || workflow?.statuses);
|
|
237
|
+
const actualStartDate = processedInput.actualStartDate ?? taskInput.actualStartDate ?? (statusDef.executionState === 'active' ? now : undefined);
|
|
238
|
+
const actualEndDate = processedInput.actualEndDate ?? taskInput.actualEndDate ?? (statusDef.completionState === 'done' ? now : undefined);
|
|
155
239
|
const created = await this.store.createTask({
|
|
156
240
|
projectId,
|
|
157
241
|
title: processedInput.title || taskInput.title,
|
|
158
242
|
description: processedInput.description ?? taskInput.description,
|
|
159
|
-
status:
|
|
243
|
+
status: initialStatus,
|
|
160
244
|
priority: processedInput.priority || taskInput.priority || 'medium',
|
|
161
245
|
taskType: processedInput.taskType || taskInput.taskType || 'task',
|
|
162
246
|
assigneeId: processedInput.assigneeId ?? taskInput.assigneeId,
|
|
@@ -166,8 +250,8 @@ export class CriticalPathEngine {
|
|
|
166
250
|
teamId: processedInput.teamId ?? taskInput.teamId,
|
|
167
251
|
containerId: processedInput.containerId ?? taskInput.containerId,
|
|
168
252
|
plannedStartDate: processedInput.plannedStartDate ?? taskInput.plannedStartDate,
|
|
169
|
-
actualStartDate
|
|
170
|
-
actualEndDate
|
|
253
|
+
actualStartDate,
|
|
254
|
+
actualEndDate,
|
|
171
255
|
dueDate: processedInput.dueDate ?? taskInput.dueDate,
|
|
172
256
|
estimatedHours: processedInput.estimatedHours ?? taskInput.estimatedHours,
|
|
173
257
|
loggedHours: processedInput.loggedHours ?? taskInput.loggedHours ?? 0,
|
|
@@ -176,12 +260,22 @@ export class CriticalPathEngine {
|
|
|
176
260
|
estimatedDurationMinutes: processedInput.estimatedDurationMinutes ?? taskInput.estimatedDurationMinutes,
|
|
177
261
|
actualDurationMinutes: processedInput.actualDurationMinutes ?? taskInput.actualDurationMinutes,
|
|
178
262
|
billableDurationMinutes: processedInput.billableDurationMinutes ?? taskInput.billableDurationMinutes,
|
|
179
|
-
progress: processedInput.progress ?? taskInput.progress ?? 0,
|
|
263
|
+
progress: processedInput.progress ?? taskInput.progress ?? (statusDef.completionState === 'done' ? 100 : 0),
|
|
180
264
|
tags: processedInput.tags ?? taskInput.tags ?? [],
|
|
181
265
|
customFields: processedInput.customFields ?? taskInput.customFields ?? {},
|
|
182
266
|
parentId: processedInput.parentId ?? taskInput.parentId
|
|
183
267
|
});
|
|
184
268
|
await this.plugins.runAfterTaskCreate(created);
|
|
269
|
+
// Publish typed Domain Event
|
|
270
|
+
const event = {
|
|
271
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
272
|
+
name: 'task.created',
|
|
273
|
+
aggregateId: created.id,
|
|
274
|
+
aggregateType: 'Task',
|
|
275
|
+
occurredAt: now,
|
|
276
|
+
payload: { task: created }
|
|
277
|
+
};
|
|
278
|
+
await this.events.publish(event);
|
|
185
279
|
await this.store.logActivity({
|
|
186
280
|
projectId: created.projectId,
|
|
187
281
|
taskId: created.id,
|
|
@@ -196,19 +290,73 @@ export class CriticalPathEngine {
|
|
|
196
290
|
const existing = await this.store.getTask(id);
|
|
197
291
|
if (!existing)
|
|
198
292
|
return null;
|
|
293
|
+
const project = await this.store.getProject(existing.projectId);
|
|
294
|
+
const workflow = await this.resolveProjectWorkflow(existing.projectId);
|
|
295
|
+
// Validate workflow transition invariant
|
|
199
296
|
if (updates.status && updates.status !== existing.status) {
|
|
200
|
-
const workflow = await this.resolveProjectWorkflow(existing.projectId);
|
|
201
297
|
const isValid = validateTransition(workflow || undefined, existing.status, updates.status);
|
|
202
298
|
if (!isValid) {
|
|
203
299
|
throw new WorkflowValidationError(existing.status, updates.status, workflow?.id);
|
|
204
300
|
}
|
|
205
301
|
}
|
|
302
|
+
// Validate custom field invariants
|
|
303
|
+
if (project?.customFieldDefinitions && updates.customFields) {
|
|
304
|
+
validateCustomFieldValues(project.customFieldDefinitions, {
|
|
305
|
+
...existing.customFields,
|
|
306
|
+
...updates.customFields
|
|
307
|
+
});
|
|
308
|
+
}
|
|
206
309
|
const processedUpdates = await this.plugins.runBeforeTaskUpdate(id, updates);
|
|
310
|
+
// Auto-update execution/completion timestamps if status changes
|
|
311
|
+
if (processedUpdates.status && processedUpdates.status !== existing.status) {
|
|
312
|
+
const statusDef = resolveStatusDefinition(processedUpdates.status, project?.statusDefinitions || workflow?.statuses);
|
|
313
|
+
const now = new Date().toISOString();
|
|
314
|
+
if (statusDef.executionState === 'active' && !existing.actualStartDate && !processedUpdates.actualStartDate) {
|
|
315
|
+
processedUpdates.actualStartDate = now;
|
|
316
|
+
}
|
|
317
|
+
if (statusDef.completionState === 'done' && !processedUpdates.actualEndDate) {
|
|
318
|
+
processedUpdates.actualEndDate = now;
|
|
319
|
+
if (processedUpdates.progress === undefined && (existing.progress || 0) < 100) {
|
|
320
|
+
processedUpdates.progress = 100;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
207
324
|
const updated = await this.store.updateTask(id, processedUpdates);
|
|
208
325
|
if (!updated)
|
|
209
326
|
return null;
|
|
210
327
|
await this.plugins.runAfterTaskUpdate(updated, existing);
|
|
328
|
+
const now = new Date().toISOString();
|
|
211
329
|
const isStatusChange = existing.status !== updated.status;
|
|
330
|
+
// Publish typed Domain Events
|
|
331
|
+
if (isStatusChange) {
|
|
332
|
+
const statusEvent = {
|
|
333
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
334
|
+
name: 'task.status_changed',
|
|
335
|
+
aggregateId: updated.id,
|
|
336
|
+
aggregateType: 'Task',
|
|
337
|
+
occurredAt: now,
|
|
338
|
+
payload: {
|
|
339
|
+
task: updated,
|
|
340
|
+
previousStatus: existing.status,
|
|
341
|
+
newStatus: updated.status
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
await this.events.publish(statusEvent);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const updateEvent = {
|
|
348
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
349
|
+
name: 'task.updated',
|
|
350
|
+
aggregateId: updated.id,
|
|
351
|
+
aggregateType: 'Task',
|
|
352
|
+
occurredAt: now,
|
|
353
|
+
payload: {
|
|
354
|
+
task: updated,
|
|
355
|
+
previous: existing
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
await this.events.publish(updateEvent);
|
|
359
|
+
}
|
|
212
360
|
await this.store.logActivity({
|
|
213
361
|
projectId: updated.projectId,
|
|
214
362
|
taskId: updated.id,
|
|
@@ -230,6 +378,20 @@ export class CriticalPathEngine {
|
|
|
230
378
|
const deleted = await this.store.deleteTask(id);
|
|
231
379
|
if (deleted) {
|
|
232
380
|
await this.plugins.runAfterTaskDelete(id);
|
|
381
|
+
const now = new Date().toISOString();
|
|
382
|
+
const event = {
|
|
383
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
384
|
+
name: 'task.deleted',
|
|
385
|
+
aggregateId: id,
|
|
386
|
+
aggregateType: 'Task',
|
|
387
|
+
occurredAt: now,
|
|
388
|
+
payload: {
|
|
389
|
+
taskId: id,
|
|
390
|
+
projectId: existing.projectId,
|
|
391
|
+
title: existing.title
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
await this.events.publish(event);
|
|
233
395
|
await this.store.logActivity({
|
|
234
396
|
projectId: existing.projectId,
|
|
235
397
|
taskId: id,
|
|
@@ -242,6 +404,30 @@ export class CriticalPathEngine {
|
|
|
242
404
|
return deleted;
|
|
243
405
|
}
|
|
244
406
|
// --- Task Dependencies & Graph ---
|
|
407
|
+
async addDependency(dep) {
|
|
408
|
+
const existingDependencies = await this.store.getDependencies(dep.taskId);
|
|
409
|
+
const allProjectDeps = [
|
|
410
|
+
...existingDependencies,
|
|
411
|
+
...(await this.store.getDependencies(dep.dependsOnTaskId))
|
|
412
|
+
];
|
|
413
|
+
// Enforce Directed Acyclic Graph (DAG) Invariant
|
|
414
|
+
const cycleCheck = detectDependencyCycle(allProjectDeps, dep);
|
|
415
|
+
if (cycleCheck.hasCycle) {
|
|
416
|
+
throw new CircularDependencyError(dep.taskId, dep.dependsOnTaskId, cycleCheck.cyclePath);
|
|
417
|
+
}
|
|
418
|
+
const created = await this.store.addDependency(dep);
|
|
419
|
+
const now = new Date().toISOString();
|
|
420
|
+
const event = {
|
|
421
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
422
|
+
name: 'dependency.added',
|
|
423
|
+
aggregateId: created.id,
|
|
424
|
+
aggregateType: 'Dependency',
|
|
425
|
+
occurredAt: now,
|
|
426
|
+
payload: { dependency: created }
|
|
427
|
+
};
|
|
428
|
+
await this.events.publish(event);
|
|
429
|
+
return created;
|
|
430
|
+
}
|
|
245
431
|
async getTaskDependencyGraph(taskId) {
|
|
246
432
|
const dependencies = await this.store.getDependencies(taskId);
|
|
247
433
|
const upstreamTaskIds = new Set();
|
|
@@ -272,6 +458,181 @@ export class CriticalPathEngine {
|
|
|
272
458
|
const statusDefs = project?.statusDefinitions || workflow?.statuses;
|
|
273
459
|
return deriveTaskLifecycleState(task, statusDefs);
|
|
274
460
|
}
|
|
461
|
+
// --- Time Tracking ---
|
|
462
|
+
async logTime(entry) {
|
|
463
|
+
if (entry.hours <= 0) {
|
|
464
|
+
throw new Error('Logged hours must be a positive number.');
|
|
465
|
+
}
|
|
466
|
+
const task = await this.store.getTask(entry.taskId);
|
|
467
|
+
if (task) {
|
|
468
|
+
const newLoggedHours = (task.loggedHours || 0) + entry.hours;
|
|
469
|
+
const newActualHours = (task.actualHours || 0) + entry.hours;
|
|
470
|
+
const newBillableHours = entry.isBillable !== false ? (task.billableHours || 0) + entry.hours : task.billableHours;
|
|
471
|
+
await this.store.updateTask(task.id, {
|
|
472
|
+
loggedHours: newLoggedHours,
|
|
473
|
+
actualHours: newActualHours,
|
|
474
|
+
billableHours: newBillableHours
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
const fullEntry = {
|
|
478
|
+
...entry,
|
|
479
|
+
userId: entry.userId || task?.assigneeId || 'system'
|
|
480
|
+
};
|
|
481
|
+
const created = await this.store.logTime(fullEntry);
|
|
482
|
+
const now = new Date().toISOString();
|
|
483
|
+
const event = {
|
|
484
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
485
|
+
name: 'time.logged',
|
|
486
|
+
aggregateId: entry.taskId,
|
|
487
|
+
aggregateType: 'Task',
|
|
488
|
+
occurredAt: now,
|
|
489
|
+
payload: { taskId: entry.taskId, timeEntry: created }
|
|
490
|
+
};
|
|
491
|
+
await this.events.publish(event);
|
|
492
|
+
return created;
|
|
493
|
+
}
|
|
494
|
+
// --- Comments ---
|
|
495
|
+
async getComments(taskId) {
|
|
496
|
+
return this.store.getComments(taskId);
|
|
497
|
+
}
|
|
498
|
+
async getComment(id) {
|
|
499
|
+
return this.store.getComment(id);
|
|
500
|
+
}
|
|
501
|
+
async addComment(comment) {
|
|
502
|
+
const created = await this.store.addComment(comment);
|
|
503
|
+
const now = new Date().toISOString();
|
|
504
|
+
const event = {
|
|
505
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
506
|
+
name: 'comment.created',
|
|
507
|
+
aggregateId: created.id,
|
|
508
|
+
aggregateType: 'Comment',
|
|
509
|
+
occurredAt: now,
|
|
510
|
+
payload: { taskId: comment.taskId, comment: created }
|
|
511
|
+
};
|
|
512
|
+
await this.events.publish(event);
|
|
513
|
+
this.dispatchWebhook('comment.created', { comment: created });
|
|
514
|
+
return created;
|
|
515
|
+
}
|
|
516
|
+
async updateComment(id, updates) {
|
|
517
|
+
const existing = await this.store.getComment(id);
|
|
518
|
+
if (!existing)
|
|
519
|
+
return null;
|
|
520
|
+
const updated = await this.store.updateComment(id, updates);
|
|
521
|
+
if (!updated)
|
|
522
|
+
return null;
|
|
523
|
+
const now = new Date().toISOString();
|
|
524
|
+
const event = {
|
|
525
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
526
|
+
name: 'comment.updated',
|
|
527
|
+
aggregateId: updated.id,
|
|
528
|
+
aggregateType: 'Comment',
|
|
529
|
+
occurredAt: now,
|
|
530
|
+
payload: { comment: updated, previous: existing }
|
|
531
|
+
};
|
|
532
|
+
await this.events.publish(event);
|
|
533
|
+
this.dispatchWebhook('comment.updated', { comment: updated });
|
|
534
|
+
return updated;
|
|
535
|
+
}
|
|
536
|
+
async deleteComment(id) {
|
|
537
|
+
const existing = await this.store.getComment(id);
|
|
538
|
+
if (!existing)
|
|
539
|
+
return false;
|
|
540
|
+
const deleted = await this.store.deleteComment(id);
|
|
541
|
+
if (deleted) {
|
|
542
|
+
const now = new Date().toISOString();
|
|
543
|
+
const event = {
|
|
544
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
545
|
+
name: 'comment.deleted',
|
|
546
|
+
aggregateId: id,
|
|
547
|
+
aggregateType: 'Comment',
|
|
548
|
+
occurredAt: now,
|
|
549
|
+
payload: { taskId: existing.taskId, commentId: id }
|
|
550
|
+
};
|
|
551
|
+
await this.events.publish(event);
|
|
552
|
+
this.dispatchWebhook('comment.deleted', { commentId: id, taskId: existing.taskId });
|
|
553
|
+
}
|
|
554
|
+
return deleted;
|
|
555
|
+
}
|
|
556
|
+
// --- Attachments ---
|
|
557
|
+
async getAttachments(filter) {
|
|
558
|
+
return this.store.getAttachments(filter);
|
|
559
|
+
}
|
|
560
|
+
async getAttachment(id) {
|
|
561
|
+
return this.store.getAttachment(id);
|
|
562
|
+
}
|
|
563
|
+
async createAttachment(attachment) {
|
|
564
|
+
const created = await this.store.createAttachment(attachment);
|
|
565
|
+
const now = new Date().toISOString();
|
|
566
|
+
const event = {
|
|
567
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
568
|
+
name: 'attachment.created',
|
|
569
|
+
aggregateId: created.id,
|
|
570
|
+
aggregateType: 'Attachment',
|
|
571
|
+
occurredAt: now,
|
|
572
|
+
payload: { attachment: created }
|
|
573
|
+
};
|
|
574
|
+
await this.events.publish(event);
|
|
575
|
+
this.dispatchWebhook('attachment.created', { attachment: created });
|
|
576
|
+
return created;
|
|
577
|
+
}
|
|
578
|
+
async deleteAttachment(id) {
|
|
579
|
+
const existing = await this.store.getAttachment(id);
|
|
580
|
+
if (!existing)
|
|
581
|
+
return false;
|
|
582
|
+
if (existing.storageKey && this.fileStorage) {
|
|
583
|
+
try {
|
|
584
|
+
await this.fileStorage.delete(existing.storageKey);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// Silently continue if underlying file is already gone
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const deleted = await this.store.deleteAttachment(id);
|
|
591
|
+
if (deleted) {
|
|
592
|
+
const now = new Date().toISOString();
|
|
593
|
+
const event = {
|
|
594
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
595
|
+
name: 'attachment.deleted',
|
|
596
|
+
aggregateId: id,
|
|
597
|
+
aggregateType: 'Attachment',
|
|
598
|
+
occurredAt: now,
|
|
599
|
+
payload: { attachmentId: id, storageKey: existing.storageKey, url: existing.url }
|
|
600
|
+
};
|
|
601
|
+
await this.events.publish(event);
|
|
602
|
+
this.dispatchWebhook('attachment.deleted', { attachmentId: id, taskId: existing.taskId, projectId: existing.projectId });
|
|
603
|
+
}
|
|
604
|
+
return deleted;
|
|
605
|
+
}
|
|
606
|
+
async uploadAttachmentFile(input) {
|
|
607
|
+
if (!this.fileStorage) {
|
|
608
|
+
throw new Error('No FileStorageAdapter configured in CriticalPathEngine. Pass "fileStorage" in config to enable direct file uploads.');
|
|
609
|
+
}
|
|
610
|
+
const uploadResult = await this.fileStorage.upload({
|
|
611
|
+
filename: input.filename,
|
|
612
|
+
data: input.data,
|
|
613
|
+
mimeType: input.mimeType,
|
|
614
|
+
pathPrefix: input.pathPrefix || (input.projectId ? `projects/${input.projectId}` : input.taskId ? `tasks/${input.taskId}` : undefined)
|
|
615
|
+
});
|
|
616
|
+
return this.createAttachment({
|
|
617
|
+
taskId: input.taskId,
|
|
618
|
+
projectId: input.projectId,
|
|
619
|
+
commentId: input.commentId,
|
|
620
|
+
uploaderId: input.uploaderId,
|
|
621
|
+
uploaderType: input.uploaderType || 'user',
|
|
622
|
+
filename: input.filename,
|
|
623
|
+
mimeType: uploadResult.mimeType,
|
|
624
|
+
sizeBytes: uploadResult.sizeBytes,
|
|
625
|
+
url: uploadResult.url,
|
|
626
|
+
storageKey: uploadResult.storageKey,
|
|
627
|
+
metadata: input.metadata
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
async getPresignedAttachmentUploadUrl(options) {
|
|
631
|
+
if (!this.fileStorage || !this.fileStorage.getPresignedUploadUrl) {
|
|
632
|
+
throw new Error('Presigned uploads are not supported by the configured FileStorageAdapter.');
|
|
633
|
+
}
|
|
634
|
+
return this.fileStorage.getPresignedUploadUrl(options);
|
|
635
|
+
}
|
|
275
636
|
// --- Teams ---
|
|
276
637
|
async getTeams() {
|
|
277
638
|
return this.store.getTeams();
|
|
@@ -281,6 +642,16 @@ export class CriticalPathEngine {
|
|
|
281
642
|
}
|
|
282
643
|
async createTeam(team) {
|
|
283
644
|
const created = await this.store.createTeam(team);
|
|
645
|
+
const now = new Date().toISOString();
|
|
646
|
+
const event = {
|
|
647
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
648
|
+
name: 'team.created',
|
|
649
|
+
aggregateId: created.id,
|
|
650
|
+
aggregateType: 'Team',
|
|
651
|
+
occurredAt: now,
|
|
652
|
+
payload: { team: created }
|
|
653
|
+
};
|
|
654
|
+
await this.events.publish(event);
|
|
284
655
|
this.dispatchWebhook('team.created', { team: created });
|
|
285
656
|
return created;
|
|
286
657
|
}
|
|
@@ -299,6 +670,16 @@ export class CriticalPathEngine {
|
|
|
299
670
|
}
|
|
300
671
|
async createContainer(container) {
|
|
301
672
|
const created = await this.store.createContainer(container);
|
|
673
|
+
const now = new Date().toISOString();
|
|
674
|
+
const event = {
|
|
675
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
676
|
+
name: 'container.created',
|
|
677
|
+
aggregateId: created.id,
|
|
678
|
+
aggregateType: 'Container',
|
|
679
|
+
occurredAt: now,
|
|
680
|
+
payload: { container: created }
|
|
681
|
+
};
|
|
682
|
+
await this.events.publish(event);
|
|
302
683
|
this.dispatchWebhook('container.created', { container: created });
|
|
303
684
|
return created;
|
|
304
685
|
}
|
|
@@ -318,6 +699,16 @@ export class CriticalPathEngine {
|
|
|
318
699
|
async createIteration(iteration) {
|
|
319
700
|
const created = await this.store.createIteration(iteration);
|
|
320
701
|
if (created.status === 'active') {
|
|
702
|
+
const now = new Date().toISOString();
|
|
703
|
+
const event = {
|
|
704
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
705
|
+
name: 'iteration.started',
|
|
706
|
+
aggregateId: created.id,
|
|
707
|
+
aggregateType: 'Iteration',
|
|
708
|
+
occurredAt: now,
|
|
709
|
+
payload: { iteration: created }
|
|
710
|
+
};
|
|
711
|
+
await this.events.publish(event);
|
|
321
712
|
this.dispatchWebhook('iteration.started', { iteration: created });
|
|
322
713
|
}
|
|
323
714
|
return created;
|
|
@@ -328,10 +719,29 @@ export class CriticalPathEngine {
|
|
|
328
719
|
return null;
|
|
329
720
|
const updated = await this.store.updateIteration(id, updates);
|
|
330
721
|
if (updated) {
|
|
722
|
+
const now = new Date().toISOString();
|
|
331
723
|
if (existing.status !== 'active' && updated.status === 'active') {
|
|
724
|
+
const event = {
|
|
725
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
726
|
+
name: 'iteration.started',
|
|
727
|
+
aggregateId: updated.id,
|
|
728
|
+
aggregateType: 'Iteration',
|
|
729
|
+
occurredAt: now,
|
|
730
|
+
payload: { iteration: updated }
|
|
731
|
+
};
|
|
732
|
+
await this.events.publish(event);
|
|
332
733
|
this.dispatchWebhook('iteration.started', { iteration: updated });
|
|
333
734
|
}
|
|
334
735
|
else if (existing.status !== 'completed' && updated.status === 'completed') {
|
|
736
|
+
const event = {
|
|
737
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
738
|
+
name: 'iteration.completed',
|
|
739
|
+
aggregateId: updated.id,
|
|
740
|
+
aggregateType: 'Iteration',
|
|
741
|
+
occurredAt: now,
|
|
742
|
+
payload: { iteration: updated }
|
|
743
|
+
};
|
|
744
|
+
await this.events.publish(event);
|
|
335
745
|
this.dispatchWebhook('iteration.completed', { iteration: updated });
|
|
336
746
|
}
|
|
337
747
|
}
|