@critical-path/core 0.11.0 → 0.13.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 +21 -0
- package/README.md +39 -0
- package/dist/deliverables.test.d.ts +2 -0
- package/dist/deliverables.test.d.ts.map +1 -0
- package/dist/deliverables.test.js +232 -0
- package/dist/deliverables.test.js.map +1 -0
- package/dist/domain/entities.d.ts +25 -1
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +153 -0
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/events.d.ts +31 -3
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/engine/index.d.ts +9 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +173 -2
- package/dist/engine/index.js.map +1 -1
- package/dist/index.test.js +28 -0
- package/dist/index.test.js.map +1 -1
- package/dist/store/firebase.d.ts +6 -1
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +41 -0
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/index.d.ts +15 -2
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +38 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +7 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +103 -6
- package/dist/store/sqlite.js.map +1 -1
- package/dist/types/index.d.ts +41 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/workflow.d.ts +2 -0
- package/dist/utils/workflow.d.ts.map +1 -1
- package/dist/utils/workflow.js +64 -0
- package/dist/utils/workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/deliverables.test.ts +284 -0
- package/src/domain/entities.ts +185 -2
- package/src/domain/events.ts +28 -3
- package/src/engine/index.ts +197 -2
- package/src/index.test.ts +33 -0
- package/src/store/firebase.ts +47 -1
- package/src/store/index.ts +54 -1
- package/src/store/sqlite.ts +146 -5
- package/src/types/index.ts +60 -1
- package/src/utils/workflow.ts +77 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/domain/events.ts
CHANGED
|
@@ -9,14 +9,15 @@ import type {
|
|
|
9
9
|
TimeEntry,
|
|
10
10
|
Comment,
|
|
11
11
|
CommentReaction,
|
|
12
|
-
Attachment
|
|
12
|
+
Attachment,
|
|
13
|
+
Deliverable
|
|
13
14
|
} from '../types/index.js';
|
|
14
15
|
|
|
15
16
|
export interface DomainEvent<TPayload = Record<string, unknown>> {
|
|
16
17
|
readonly id: string;
|
|
17
18
|
readonly name: string;
|
|
18
19
|
readonly aggregateId: string;
|
|
19
|
-
readonly aggregateType: 'Task' | 'Project' | 'Workflow' | 'Iteration' | 'Team' | 'Container' | 'Dependency' | 'Comment' | 'Attachment';
|
|
20
|
+
readonly aggregateType: 'Task' | 'Project' | 'Workflow' | 'Iteration' | 'Team' | 'Container' | 'Dependency' | 'Comment' | 'Attachment' | 'Deliverable';
|
|
20
21
|
readonly occurredAt: string;
|
|
21
22
|
readonly payload: TPayload;
|
|
22
23
|
}
|
|
@@ -136,6 +137,26 @@ export interface ContainerCreatedEvent extends DomainEvent<{ container: TaskCont
|
|
|
136
137
|
readonly aggregateType: 'Container';
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
export interface DeliverableCreatedEvent extends DomainEvent<{ deliverable: Deliverable }> {
|
|
141
|
+
readonly name: 'deliverable.created';
|
|
142
|
+
readonly aggregateType: 'Deliverable';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface DeliverableUpdatedEvent extends DomainEvent<{ deliverable: Deliverable; previous: Deliverable }> {
|
|
146
|
+
readonly name: 'deliverable.updated';
|
|
147
|
+
readonly aggregateType: 'Deliverable';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface DeliverableStatusChangedEvent extends DomainEvent<{ deliverable: Deliverable; previousStatus: string; newStatus: string }> {
|
|
151
|
+
readonly name: 'deliverable.status_changed';
|
|
152
|
+
readonly aggregateType: 'Deliverable';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface DeliverableDeletedEvent extends DomainEvent<{ deliverableId: string; projectId: string }> {
|
|
156
|
+
readonly name: 'deliverable.deleted';
|
|
157
|
+
readonly aggregateType: 'Deliverable';
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
export type CriticalPathDomainEvent =
|
|
140
161
|
| TaskCreatedEvent
|
|
141
162
|
| TaskUpdatedEvent
|
|
@@ -158,7 +179,11 @@ export type CriticalPathDomainEvent =
|
|
|
158
179
|
| IterationStartedEvent
|
|
159
180
|
| IterationCompletedEvent
|
|
160
181
|
| TeamCreatedEvent
|
|
161
|
-
| ContainerCreatedEvent
|
|
182
|
+
| ContainerCreatedEvent
|
|
183
|
+
| DeliverableCreatedEvent
|
|
184
|
+
| DeliverableUpdatedEvent
|
|
185
|
+
| DeliverableStatusChangedEvent
|
|
186
|
+
| DeliverableDeletedEvent;
|
|
162
187
|
|
|
163
188
|
export type DomainEventHandler<T extends DomainEvent = DomainEvent> = (event: T) => Promise<void> | void;
|
|
164
189
|
|
package/src/engine/index.ts
CHANGED
|
@@ -17,7 +17,10 @@ import type {
|
|
|
17
17
|
WebhookEvent,
|
|
18
18
|
Workflow,
|
|
19
19
|
CreateTaskInput,
|
|
20
|
-
CreateProjectInput
|
|
20
|
+
CreateProjectInput,
|
|
21
|
+
Deliverable,
|
|
22
|
+
DeliverableSummary,
|
|
23
|
+
CreateDeliverableInput
|
|
21
24
|
} from '../types/index.js';
|
|
22
25
|
import { StorageAdapter, InMemoryStore } from '../store/index.js';
|
|
23
26
|
import { PluginRegistry } from '../plugins/index.js';
|
|
@@ -25,6 +28,7 @@ import { deriveTaskLifecycleState, type TaskLifecycleState, resolveStatusDefinit
|
|
|
25
28
|
import {
|
|
26
29
|
validateTransition,
|
|
27
30
|
getAllowedNextStatuses,
|
|
31
|
+
getAllowedPreviousStatuses,
|
|
28
32
|
WorkflowValidationError,
|
|
29
33
|
DEFAULT_SOFTWARE_WORKFLOW
|
|
30
34
|
} from '../utils/workflow.js';
|
|
@@ -44,6 +48,10 @@ import {
|
|
|
44
48
|
IterationCompletedEvent,
|
|
45
49
|
TeamCreatedEvent,
|
|
46
50
|
ContainerCreatedEvent,
|
|
51
|
+
DeliverableCreatedEvent,
|
|
52
|
+
DeliverableUpdatedEvent,
|
|
53
|
+
DeliverableStatusChangedEvent,
|
|
54
|
+
DeliverableDeletedEvent,
|
|
47
55
|
TaskDependencyAddedEvent,
|
|
48
56
|
TimeLoggedEvent,
|
|
49
57
|
CommentAddedEvent,
|
|
@@ -63,6 +71,7 @@ export class CriticalPathEngine {
|
|
|
63
71
|
public readonly fileStorage?: FileStorageAdapter;
|
|
64
72
|
public readonly plugins: PluginRegistry;
|
|
65
73
|
public readonly events: DomainEventBus;
|
|
74
|
+
public readonly ready: Promise<void> = Promise.resolve();
|
|
66
75
|
|
|
67
76
|
constructor(config: CriticalPathConfig = {}) {
|
|
68
77
|
this.store = typeof config.store === 'object' && config.store !== null
|
|
@@ -80,7 +89,7 @@ export class CriticalPathEngine {
|
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
if (config.initialData) {
|
|
83
|
-
this.seedInitialData(config.initialData);
|
|
92
|
+
this.ready = this.seedInitialData(config.initialData);
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -105,6 +114,11 @@ export class CriticalPathEngine {
|
|
|
105
114
|
await this.store.createContainer(c);
|
|
106
115
|
}
|
|
107
116
|
}
|
|
117
|
+
if (data.deliverables) {
|
|
118
|
+
for (const d of data.deliverables) {
|
|
119
|
+
await this.store.createDeliverable(d);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
108
122
|
if (data.iterations) {
|
|
109
123
|
for (const it of data.iterations) {
|
|
110
124
|
await this.store.createIteration(it);
|
|
@@ -226,6 +240,13 @@ export class CriticalPathEngine {
|
|
|
226
240
|
return getAllowedNextStatuses(workflow || undefined, task.status);
|
|
227
241
|
}
|
|
228
242
|
|
|
243
|
+
async getAllowedPreviousTaskTransitions(taskId: string): Promise<string[]> {
|
|
244
|
+
const task = await this.store.getTask(taskId);
|
|
245
|
+
if (!task) return [];
|
|
246
|
+
const workflow = await this.resolveProjectWorkflow(task.projectId);
|
|
247
|
+
return getAllowedPreviousStatuses(workflow || undefined, task.status);
|
|
248
|
+
}
|
|
249
|
+
|
|
229
250
|
// --- Projects ---
|
|
230
251
|
async getProjects(): Promise<Project[]> {
|
|
231
252
|
return this.store.getProjects();
|
|
@@ -327,11 +348,13 @@ export class CriticalPathEngine {
|
|
|
327
348
|
priority: processedInput.priority || taskInput.priority || 'medium',
|
|
328
349
|
taskType: processedInput.taskType || taskInput.taskType || 'task',
|
|
329
350
|
assigneeId: processedInput.assigneeId ?? taskInput.assigneeId,
|
|
351
|
+
assignees: processedInput.assignees ?? taskInput.assignees,
|
|
330
352
|
reporterId: processedInput.reporterId ?? taskInput.reporterId,
|
|
331
353
|
reviewerId: processedInput.reviewerId ?? taskInput.reviewerId,
|
|
332
354
|
iterationId: processedInput.iterationId ?? taskInput.iterationId,
|
|
333
355
|
teamId: processedInput.teamId ?? taskInput.teamId,
|
|
334
356
|
containerId: processedInput.containerId ?? taskInput.containerId,
|
|
357
|
+
deliverableId: processedInput.deliverableId ?? taskInput.deliverableId,
|
|
335
358
|
plannedStartDate: processedInput.plannedStartDate ?? taskInput.plannedStartDate,
|
|
336
359
|
actualStartDate,
|
|
337
360
|
actualEndDate,
|
|
@@ -937,6 +960,178 @@ export class CriticalPathEngine {
|
|
|
937
960
|
return this.store.deleteContainer(id);
|
|
938
961
|
}
|
|
939
962
|
|
|
963
|
+
// --- Deliverables ---
|
|
964
|
+
async getDeliverables(projectId: string): Promise<Deliverable[]> {
|
|
965
|
+
return this.store.getDeliverables(projectId);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async getDeliverable(id: string): Promise<Deliverable | null> {
|
|
969
|
+
return this.store.getDeliverable(id);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async createDeliverable(input: CreateDeliverableInput): Promise<Deliverable> {
|
|
973
|
+
const project = await this.store.getProject(input.projectId);
|
|
974
|
+
if (project?.customFieldDefinitions && input.customFields) {
|
|
975
|
+
validateCustomFieldValues(project.customFieldDefinitions, input.customFields);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const created = await this.store.createDeliverable({
|
|
979
|
+
...input,
|
|
980
|
+
status: input.status || 'planned',
|
|
981
|
+
outputUrls: input.outputUrls ?? [],
|
|
982
|
+
customFields: input.customFields ?? {}
|
|
983
|
+
});
|
|
984
|
+
const now = new Date().toISOString();
|
|
985
|
+
|
|
986
|
+
const event: DeliverableCreatedEvent = {
|
|
987
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
988
|
+
name: 'deliverable.created',
|
|
989
|
+
aggregateId: created.id,
|
|
990
|
+
aggregateType: 'Deliverable',
|
|
991
|
+
occurredAt: now,
|
|
992
|
+
payload: { deliverable: created }
|
|
993
|
+
};
|
|
994
|
+
await this.events.publish(event);
|
|
995
|
+
|
|
996
|
+
await this.store.logActivity({
|
|
997
|
+
projectId: created.projectId,
|
|
998
|
+
actorId: 'system',
|
|
999
|
+
action: 'deliverable.created',
|
|
1000
|
+
details: { title: created.title, format: created.format }
|
|
1001
|
+
});
|
|
1002
|
+
this.dispatchWebhook('deliverable.created', { deliverable: created });
|
|
1003
|
+
return created;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async updateDeliverable(id: string, updates: Partial<Deliverable>): Promise<Deliverable | null> {
|
|
1007
|
+
const existing = await this.store.getDeliverable(id);
|
|
1008
|
+
if (!existing) return null;
|
|
1009
|
+
|
|
1010
|
+
if (updates.status && updates.status === 'delivered' && !existing.deliveredAt && !updates.deliveredAt) {
|
|
1011
|
+
updates.deliveredAt = new Date().toISOString();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const updated = await this.store.updateDeliverable(id, updates);
|
|
1015
|
+
if (updated) {
|
|
1016
|
+
const now = new Date().toISOString();
|
|
1017
|
+
|
|
1018
|
+
if (updates.status && updates.status !== existing.status) {
|
|
1019
|
+
const statusEvent: DeliverableStatusChangedEvent = {
|
|
1020
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
1021
|
+
name: 'deliverable.status_changed',
|
|
1022
|
+
aggregateId: updated.id,
|
|
1023
|
+
aggregateType: 'Deliverable',
|
|
1024
|
+
occurredAt: now,
|
|
1025
|
+
payload: {
|
|
1026
|
+
deliverable: updated,
|
|
1027
|
+
previousStatus: existing.status,
|
|
1028
|
+
newStatus: updated.status
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
await this.events.publish(statusEvent);
|
|
1032
|
+
this.dispatchWebhook('deliverable.status_changed', {
|
|
1033
|
+
deliverable: updated,
|
|
1034
|
+
previousStatus: existing.status,
|
|
1035
|
+
newStatus: updated.status
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const updateEvent: DeliverableUpdatedEvent = {
|
|
1040
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
1041
|
+
name: 'deliverable.updated',
|
|
1042
|
+
aggregateId: updated.id,
|
|
1043
|
+
aggregateType: 'Deliverable',
|
|
1044
|
+
occurredAt: now,
|
|
1045
|
+
payload: { deliverable: updated, previous: existing }
|
|
1046
|
+
};
|
|
1047
|
+
await this.events.publish(updateEvent);
|
|
1048
|
+
|
|
1049
|
+
await this.store.logActivity({
|
|
1050
|
+
projectId: updated.projectId,
|
|
1051
|
+
actorId: 'system',
|
|
1052
|
+
action: 'deliverable.updated',
|
|
1053
|
+
details: { title: updated.title, status: updated.status }
|
|
1054
|
+
});
|
|
1055
|
+
this.dispatchWebhook('deliverable.updated', { deliverable: updated });
|
|
1056
|
+
}
|
|
1057
|
+
return updated;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async deleteDeliverable(id: string): Promise<boolean> {
|
|
1061
|
+
const existing = await this.store.getDeliverable(id);
|
|
1062
|
+
if (!existing) return false;
|
|
1063
|
+
|
|
1064
|
+
const deleted = await this.store.deleteDeliverable(id);
|
|
1065
|
+
if (deleted) {
|
|
1066
|
+
const now = new Date().toISOString();
|
|
1067
|
+
const event: DeliverableDeletedEvent = {
|
|
1068
|
+
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
1069
|
+
name: 'deliverable.deleted',
|
|
1070
|
+
aggregateId: id,
|
|
1071
|
+
aggregateType: 'Deliverable',
|
|
1072
|
+
occurredAt: now,
|
|
1073
|
+
payload: { deliverableId: id, projectId: existing.projectId }
|
|
1074
|
+
};
|
|
1075
|
+
await this.events.publish(event);
|
|
1076
|
+
|
|
1077
|
+
await this.store.logActivity({
|
|
1078
|
+
projectId: existing.projectId,
|
|
1079
|
+
actorId: 'system',
|
|
1080
|
+
action: 'deliverable.deleted',
|
|
1081
|
+
details: { title: existing.title }
|
|
1082
|
+
});
|
|
1083
|
+
this.dispatchWebhook('deliverable.deleted', { deliverableId: id, projectId: existing.projectId });
|
|
1084
|
+
}
|
|
1085
|
+
return deleted;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async getDeliverableSummary(deliverableId: string): Promise<DeliverableSummary | null> {
|
|
1089
|
+
const deliverable = await this.store.getDeliverable(deliverableId);
|
|
1090
|
+
if (!deliverable) return null;
|
|
1091
|
+
|
|
1092
|
+
const project = await this.store.getProject(deliverable.projectId);
|
|
1093
|
+
const workflow = await this.resolveProjectWorkflow(deliverable.projectId);
|
|
1094
|
+
const allTasks = await this.store.getTasks(deliverable.projectId);
|
|
1095
|
+
const tasks = allTasks.filter((t) => t.deliverableId === deliverableId);
|
|
1096
|
+
|
|
1097
|
+
const totalTasks = tasks.length;
|
|
1098
|
+
let completedTasks = 0;
|
|
1099
|
+
let activeTasks = 0;
|
|
1100
|
+
let estimatedHours = 0;
|
|
1101
|
+
let loggedHours = 0;
|
|
1102
|
+
let totalProgress = 0;
|
|
1103
|
+
|
|
1104
|
+
for (const task of tasks) {
|
|
1105
|
+
const statusDef = resolveStatusDefinition(task.status, project?.statusDefinitions || workflow?.statuses);
|
|
1106
|
+
if (statusDef.completionState === 'done') {
|
|
1107
|
+
completedTasks++;
|
|
1108
|
+
} else if (statusDef.executionState === 'active') {
|
|
1109
|
+
activeTasks++;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (task.estimatedHours) estimatedHours += task.estimatedHours;
|
|
1113
|
+
if (task.loggedHours) loggedHours += task.loggedHours;
|
|
1114
|
+
|
|
1115
|
+
if (typeof task.progress === 'number') {
|
|
1116
|
+
totalProgress += task.progress;
|
|
1117
|
+
} else {
|
|
1118
|
+
totalProgress += statusDef.completionState === 'done' ? 100 : 0;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const progressPercentage = totalTasks > 0 ? Math.round(totalProgress / totalTasks) : 0;
|
|
1123
|
+
|
|
1124
|
+
return {
|
|
1125
|
+
deliverable,
|
|
1126
|
+
totalTasks,
|
|
1127
|
+
completedTasks,
|
|
1128
|
+
activeTasks,
|
|
1129
|
+
progressPercentage,
|
|
1130
|
+
estimatedHours,
|
|
1131
|
+
loggedHours
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
940
1135
|
// --- Iterations ---
|
|
941
1136
|
async getIterations(projectId: string): Promise<Iteration[]> {
|
|
942
1137
|
return this.store.getIterations(projectId);
|
package/src/index.test.ts
CHANGED
|
@@ -339,6 +339,39 @@ describe('CriticalPathEngine Core Tests', () => {
|
|
|
339
339
|
});
|
|
340
340
|
expect(updated?.mentions).toEqual(['coordinator']);
|
|
341
341
|
});
|
|
342
|
+
|
|
343
|
+
it('supports multi-assignees and backward status transitions', async () => {
|
|
344
|
+
const engine = new CriticalPathEngine();
|
|
345
|
+
const proj = await engine.createProject({ key: 'TRN', name: 'Transitions Project' });
|
|
346
|
+
|
|
347
|
+
// Task with multi-assignees
|
|
348
|
+
const task = await engine.createTask({
|
|
349
|
+
projectId: proj.id,
|
|
350
|
+
title: 'Multi-assignee Task',
|
|
351
|
+
status: 'in_progress',
|
|
352
|
+
assignees: [
|
|
353
|
+
{ id: 'agent_1', name: 'Agent Alpha', role: 'Planner', type: 'agent' },
|
|
354
|
+
{ id: 'user_1', name: 'Alice Developer', role: 'Developer', type: 'user', avatarUrl: 'https://example.com/alice.png' }
|
|
355
|
+
]
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(task.assignees).toHaveLength(2);
|
|
359
|
+
expect(task.assignees?.[0].type).toBe('agent');
|
|
360
|
+
expect(task.assignees?.[1].avatarUrl).toBe('https://example.com/alice.png');
|
|
361
|
+
|
|
362
|
+
// Backward transitions
|
|
363
|
+
const previousStatuses = await engine.getAllowedPreviousTaskTransitions(task.id);
|
|
364
|
+
expect(previousStatuses).toContain('todo');
|
|
365
|
+
|
|
366
|
+
// First status (backlog) should have no previous status
|
|
367
|
+
const backlogTask = await engine.createTask({
|
|
368
|
+
projectId: proj.id,
|
|
369
|
+
title: 'Backlog Task',
|
|
370
|
+
status: 'backlog'
|
|
371
|
+
});
|
|
372
|
+
const backlogPrev = await engine.getAllowedPreviousTaskTransitions(backlogTask.id);
|
|
373
|
+
expect(backlogPrev).toEqual([]);
|
|
374
|
+
});
|
|
342
375
|
});
|
|
343
376
|
|
|
344
377
|
|
package/src/store/firebase.ts
CHANGED
|
@@ -11,7 +11,9 @@ import type {
|
|
|
11
11
|
Activity,
|
|
12
12
|
Webhook,
|
|
13
13
|
TaskDependency,
|
|
14
|
-
Workflow
|
|
14
|
+
Workflow,
|
|
15
|
+
Deliverable,
|
|
16
|
+
CreateDeliverableInput
|
|
15
17
|
} from '../types/index.js';
|
|
16
18
|
import { generateProjectKey } from '../utils/key.js';
|
|
17
19
|
|
|
@@ -283,6 +285,50 @@ export class FirebaseStore implements StorageAdapter {
|
|
|
283
285
|
return true;
|
|
284
286
|
}
|
|
285
287
|
|
|
288
|
+
// --- Deliverables ---
|
|
289
|
+
async getDeliverables(projectId: string): Promise<Deliverable[]> {
|
|
290
|
+
const snap = await this.db.collection('deliverables').where('projectId', '==', projectId).get();
|
|
291
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async getDeliverable(id: string): Promise<Deliverable | null> {
|
|
295
|
+
const snap = await this.db.collection('deliverables').doc(id).get();
|
|
296
|
+
if (!snap.exists) return null;
|
|
297
|
+
return { ...snap.data(), id: snap.id };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable> {
|
|
301
|
+
const docRef = this.db.collection('deliverables').doc();
|
|
302
|
+
const now = new Date().toISOString();
|
|
303
|
+
const newDeliverable: Deliverable = {
|
|
304
|
+
...deliverable,
|
|
305
|
+
id: docRef.id,
|
|
306
|
+
status: deliverable.status || 'planned',
|
|
307
|
+
outputUrls: deliverable.outputUrls ? [...deliverable.outputUrls] : [],
|
|
308
|
+
customFields: deliverable.customFields ? { ...deliverable.customFields } : {},
|
|
309
|
+
createdAt: now,
|
|
310
|
+
updatedAt: now
|
|
311
|
+
};
|
|
312
|
+
await docRef.set(sanitizeFirestoreData(newDeliverable));
|
|
313
|
+
return newDeliverable;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async updateDeliverable(id: string, updates: Partial<Deliverable>): Promise<Deliverable | null> {
|
|
317
|
+
const existing = await this.getDeliverable(id);
|
|
318
|
+
if (!existing) return null;
|
|
319
|
+
|
|
320
|
+
const updated: Deliverable = { ...existing, ...updates, updatedAt: new Date().toISOString() };
|
|
321
|
+
await this.db.collection('deliverables').doc(id).set(sanitizeFirestoreData(updated), { merge: true });
|
|
322
|
+
return updated;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async deleteDeliverable(id: string): Promise<boolean> {
|
|
326
|
+
const existing = await this.getDeliverable(id);
|
|
327
|
+
if (!existing) return false;
|
|
328
|
+
await this.db.collection('deliverables').doc(id).delete();
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
286
332
|
// --- Tasks ---
|
|
287
333
|
async getTasks(projectId?: string): Promise<Task[]> {
|
|
288
334
|
let snap;
|
package/src/store/index.ts
CHANGED
|
@@ -13,7 +13,9 @@ import type {
|
|
|
13
13
|
Activity,
|
|
14
14
|
Webhook,
|
|
15
15
|
TaskDependency,
|
|
16
|
-
Workflow
|
|
16
|
+
Workflow,
|
|
17
|
+
Deliverable,
|
|
18
|
+
CreateDeliverableInput
|
|
17
19
|
} from '../types/index.js';
|
|
18
20
|
import { generateProjectKey } from '../utils/key.js';
|
|
19
21
|
|
|
@@ -102,12 +104,21 @@ export interface WebhookRepository {
|
|
|
102
104
|
addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook>;
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
export interface DeliverableRepository {
|
|
108
|
+
getDeliverables(projectId: string): Promise<Deliverable[]>;
|
|
109
|
+
getDeliverable(id: string): Promise<Deliverable | null>;
|
|
110
|
+
createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable>;
|
|
111
|
+
updateDeliverable(id: string, updates: Partial<Deliverable>): Promise<Deliverable | null>;
|
|
112
|
+
deleteDeliverable(id: string): Promise<boolean>;
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
export interface StorageAdapter
|
|
106
116
|
extends ProjectRepository,
|
|
107
117
|
WorkflowRepository,
|
|
108
118
|
TaskRepository,
|
|
109
119
|
TeamRepository,
|
|
110
120
|
ContainerRepository,
|
|
121
|
+
DeliverableRepository,
|
|
111
122
|
IterationRepository,
|
|
112
123
|
CommentRepository,
|
|
113
124
|
AttachmentRepository,
|
|
@@ -122,6 +133,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
122
133
|
private tasks = new Map<string, Task>();
|
|
123
134
|
private teams = new Map<string, Team>();
|
|
124
135
|
private containers = new Map<string, TaskContainer>();
|
|
136
|
+
private deliverables = new Map<string, Deliverable>();
|
|
125
137
|
private iterations = new Map<string, Iteration>();
|
|
126
138
|
private comments = new Map<string, Comment>();
|
|
127
139
|
private attachments = new Map<string, Attachment>();
|
|
@@ -306,6 +318,47 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
306
318
|
return this.containers.delete(id);
|
|
307
319
|
}
|
|
308
320
|
|
|
321
|
+
// Deliverables
|
|
322
|
+
async getDeliverables(projectId: string): Promise<Deliverable[]> {
|
|
323
|
+
return Array.from(this.deliverables.values()).filter((d) => d.projectId === projectId);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async getDeliverable(id: string): Promise<Deliverable | null> {
|
|
327
|
+
return this.deliverables.get(id) || null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable> {
|
|
331
|
+
const id = (deliverable as any).id || `deliv_${Math.random().toString(36).substring(2, 9)}`;
|
|
332
|
+
const now = new Date().toISOString();
|
|
333
|
+
const newDeliverable: Deliverable = {
|
|
334
|
+
...deliverable,
|
|
335
|
+
id,
|
|
336
|
+
status: deliverable.status || 'planned',
|
|
337
|
+
outputUrls: deliverable.outputUrls ? [...deliverable.outputUrls] : [],
|
|
338
|
+
customFields: deliverable.customFields ? { ...deliverable.customFields } : {},
|
|
339
|
+
createdAt: now,
|
|
340
|
+
updatedAt: now
|
|
341
|
+
};
|
|
342
|
+
this.deliverables.set(id, newDeliverable);
|
|
343
|
+
return newDeliverable;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async updateDeliverable(id: string, updates: Partial<Deliverable>): Promise<Deliverable | null> {
|
|
347
|
+
const existing = this.deliverables.get(id);
|
|
348
|
+
if (!existing) return null;
|
|
349
|
+
const updated: Deliverable = {
|
|
350
|
+
...existing,
|
|
351
|
+
...updates,
|
|
352
|
+
updatedAt: new Date().toISOString()
|
|
353
|
+
};
|
|
354
|
+
this.deliverables.set(id, updated);
|
|
355
|
+
return updated;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async deleteDeliverable(id: string): Promise<boolean> {
|
|
359
|
+
return this.deliverables.delete(id);
|
|
360
|
+
}
|
|
361
|
+
|
|
309
362
|
// Iterations
|
|
310
363
|
async getIterations(projectId: string): Promise<Iteration[]> {
|
|
311
364
|
return Array.from(this.iterations.values()).filter((s) => s.projectId === projectId);
|