@critical-path/core 0.6.0 → 0.8.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +6 -0
  3. package/dist/domain/entities.d.ts +4 -0
  4. package/dist/domain/entities.d.ts.map +1 -1
  5. package/dist/domain/entities.js +14 -0
  6. package/dist/domain/entities.js.map +1 -1
  7. package/dist/domain/events.d.ts +32 -4
  8. package/dist/domain/events.d.ts.map +1 -1
  9. package/dist/domain/events.js.map +1 -1
  10. package/dist/engine/index.d.ts +23 -1
  11. package/dist/engine/index.d.ts.map +1 -1
  12. package/dist/engine/index.js +132 -2
  13. package/dist/engine/index.js.map +1 -1
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.test.js +26 -0
  19. package/dist/index.test.js.map +1 -1
  20. package/dist/storage/file-storage.d.ts +24 -0
  21. package/dist/storage/file-storage.d.ts.map +1 -0
  22. package/dist/storage/file-storage.js +73 -0
  23. package/dist/storage/file-storage.js.map +1 -0
  24. package/dist/storage/firebase-storage.d.ts +48 -0
  25. package/dist/storage/firebase-storage.d.ts.map +1 -0
  26. package/dist/storage/firebase-storage.js +142 -0
  27. package/dist/storage/firebase-storage.js.map +1 -0
  28. package/dist/storage/index.d.ts +4 -0
  29. package/dist/storage/index.d.ts.map +1 -0
  30. package/dist/storage/index.js +4 -0
  31. package/dist/storage/index.js.map +1 -0
  32. package/dist/storage/s3.d.ts +36 -0
  33. package/dist/storage/s3.d.ts.map +1 -0
  34. package/dist/storage/s3.js +135 -0
  35. package/dist/storage/s3.js.map +1 -0
  36. package/dist/storage/storage.test.d.ts +2 -0
  37. package/dist/storage/storage.test.d.ts.map +1 -0
  38. package/dist/storage/storage.test.js +117 -0
  39. package/dist/storage/storage.test.js.map +1 -0
  40. package/dist/store/firebase.d.ts +13 -1
  41. package/dist/store/firebase.d.ts.map +1 -1
  42. package/dist/store/firebase.js +95 -18
  43. package/dist/store/firebase.js.map +1 -1
  44. package/dist/store/firebase.test.js +12 -0
  45. package/dist/store/firebase.test.js.map +1 -1
  46. package/dist/store/index.d.ts +27 -2
  47. package/dist/store/index.d.ts.map +1 -1
  48. package/dist/store/index.js +49 -0
  49. package/dist/store/index.js.map +1 -1
  50. package/dist/store/sqlite.d.ts +12 -1
  51. package/dist/store/sqlite.d.ts.map +1 -1
  52. package/dist/store/sqlite.js +128 -6
  53. package/dist/store/sqlite.js.map +1 -1
  54. package/dist/store/sqlite.test.js +47 -1
  55. package/dist/store/sqlite.test.js.map +1 -1
  56. package/dist/types/index.d.ts +52 -1
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/domain/entities.ts +19 -0
  60. package/src/domain/events.ts +28 -3
  61. package/src/engine/index.ts +168 -3
  62. package/src/index.test.ts +32 -0
  63. package/src/index.ts +2 -0
  64. package/src/storage/file-storage.ts +96 -0
  65. package/src/storage/firebase-storage.ts +177 -0
  66. package/src/storage/index.ts +3 -0
  67. package/src/storage/s3.ts +165 -0
  68. package/src/storage/storage.test.ts +146 -0
  69. package/src/store/firebase.test.ts +15 -0
  70. package/src/store/firebase.ts +97 -18
  71. package/src/store/index.ts +64 -0
  72. package/src/store/sqlite.test.ts +59 -1
  73. package/src/store/sqlite.ts +172 -6
  74. package/src/types/index.ts +63 -0
  75. package/tsconfig.tsbuildinfo +1 -1
@@ -6,6 +6,7 @@ import type {
6
6
  Team,
7
7
  TaskContainer,
8
8
  Comment,
9
+ Attachment,
9
10
  TimeEntry,
10
11
  Activity,
11
12
  Webhook,
@@ -101,6 +102,22 @@ export interface FirebaseStoreConfig {
101
102
  db: FirestoreDBInterface;
102
103
  }
103
104
 
105
+ export function sanitizeFirestoreData<T>(obj: T): T {
106
+ if (obj === null || typeof obj !== 'object') {
107
+ return obj;
108
+ }
109
+ if (Array.isArray(obj)) {
110
+ return obj.map(sanitizeFirestoreData) as unknown as T;
111
+ }
112
+ const clean: Record<string, any> = {};
113
+ for (const [key, value] of Object.entries(obj as Record<string, any>)) {
114
+ if (value !== undefined) {
115
+ clean[key] = sanitizeFirestoreData(value);
116
+ }
117
+ }
118
+ return clean as T;
119
+ }
120
+
104
121
  export class FirebaseStore implements StorageAdapter {
105
122
  private db: FirestoreDBInterface;
106
123
 
@@ -130,7 +147,7 @@ export class FirebaseStore implements StorageAdapter {
130
147
  const now = new Date().toISOString();
131
148
  const key = project.key || generateProjectKey(project.name);
132
149
  const newProj: Project = { ...project, key, id: docRef.id, createdAt: now, updatedAt: now };
133
- await docRef.set(newProj);
150
+ await docRef.set(sanitizeFirestoreData(newProj));
134
151
  return newProj;
135
152
  }
136
153
 
@@ -143,7 +160,7 @@ export class FirebaseStore implements StorageAdapter {
143
160
  ...updates,
144
161
  updatedAt: new Date().toISOString()
145
162
  };
146
- await this.db.collection('projects').doc(id).set(updated, { merge: true });
163
+ await this.db.collection('projects').doc(id).set(sanitizeFirestoreData(updated), { merge: true });
147
164
  return updated;
148
165
  }
149
166
 
@@ -170,7 +187,7 @@ export class FirebaseStore implements StorageAdapter {
170
187
  const docRef = this.db.collection('workflows').doc();
171
188
  const now = new Date().toISOString();
172
189
  const newWf: Workflow = { ...workflow, id: docRef.id, createdAt: now, updatedAt: now };
173
- await docRef.set(newWf);
190
+ await docRef.set(sanitizeFirestoreData(newWf));
174
191
  return newWf;
175
192
  }
176
193
 
@@ -183,7 +200,7 @@ export class FirebaseStore implements StorageAdapter {
183
200
  ...updates,
184
201
  updatedAt: new Date().toISOString()
185
202
  };
186
- await this.db.collection('workflows').doc(id).set(updated, { merge: true });
203
+ await this.db.collection('workflows').doc(id).set(sanitizeFirestoreData(updated), { merge: true });
187
204
  return updated;
188
205
  }
189
206
 
@@ -210,7 +227,7 @@ export class FirebaseStore implements StorageAdapter {
210
227
  const docRef = this.db.collection('teams').doc();
211
228
  const now = new Date().toISOString();
212
229
  const newTeam: Team = { ...team, id: docRef.id, createdAt: now, updatedAt: now };
213
- await docRef.set(newTeam);
230
+ await docRef.set(sanitizeFirestoreData(newTeam));
214
231
  return newTeam;
215
232
  }
216
233
 
@@ -219,7 +236,7 @@ export class FirebaseStore implements StorageAdapter {
219
236
  if (!existing) return null;
220
237
 
221
238
  const updated: Team = { ...existing, ...updates, updatedAt: new Date().toISOString() };
222
- await this.db.collection('teams').doc(id).set(updated, { merge: true });
239
+ await this.db.collection('teams').doc(id).set(sanitizeFirestoreData(updated), { merge: true });
223
240
  return updated;
224
241
  }
225
242
 
@@ -246,7 +263,7 @@ export class FirebaseStore implements StorageAdapter {
246
263
  const docRef = this.db.collection('containers').doc();
247
264
  const now = new Date().toISOString();
248
265
  const newContainer: TaskContainer = { ...container, id: docRef.id, createdAt: now, updatedAt: now };
249
- await docRef.set(newContainer);
266
+ await docRef.set(sanitizeFirestoreData(newContainer));
250
267
  return newContainer;
251
268
  }
252
269
 
@@ -255,7 +272,7 @@ export class FirebaseStore implements StorageAdapter {
255
272
  if (!existing) return null;
256
273
 
257
274
  const updated: TaskContainer = { ...existing, ...updates, updatedAt: new Date().toISOString() };
258
- await this.db.collection('containers').doc(id).set(updated, { merge: true });
275
+ await this.db.collection('containers').doc(id).set(sanitizeFirestoreData(updated), { merge: true });
259
276
  return updated;
260
277
  }
261
278
 
@@ -287,7 +304,7 @@ export class FirebaseStore implements StorageAdapter {
287
304
  const docRef = this.db.collection('tasks').doc();
288
305
  const now = new Date().toISOString();
289
306
  const newTask: Task = { ...task, id: docRef.id, createdAt: now, updatedAt: now };
290
- await docRef.set(newTask);
307
+ await docRef.set(sanitizeFirestoreData(newTask));
291
308
  return newTask;
292
309
  }
293
310
 
@@ -300,7 +317,7 @@ export class FirebaseStore implements StorageAdapter {
300
317
  ...updates,
301
318
  updatedAt: new Date().toISOString()
302
319
  };
303
- await this.db.collection('tasks').doc(id).set(updated, { merge: true });
320
+ await this.db.collection('tasks').doc(id).set(sanitizeFirestoreData(updated), { merge: true });
304
321
  return updated;
305
322
  }
306
323
 
@@ -327,7 +344,7 @@ export class FirebaseStore implements StorageAdapter {
327
344
  const docRef = this.db.collection('iterations').doc();
328
345
  const now = new Date().toISOString();
329
346
  const newIteration: Iteration = { ...iteration, id: docRef.id, createdAt: now };
330
- await docRef.set(newIteration);
347
+ await docRef.set(sanitizeFirestoreData(newIteration));
331
348
  return newIteration;
332
349
  }
333
350
 
@@ -336,7 +353,7 @@ export class FirebaseStore implements StorageAdapter {
336
353
  if (!snap.exists) return null;
337
354
 
338
355
  const updated: Iteration = { ...snap.data(), id, ...updates };
339
- await this.db.collection('iterations').doc(id).set(updated, { merge: true });
356
+ await this.db.collection('iterations').doc(id).set(sanitizeFirestoreData(updated), { merge: true });
340
357
  return updated;
341
358
  }
342
359
 
@@ -347,20 +364,82 @@ export class FirebaseStore implements StorageAdapter {
347
364
  return true;
348
365
  }
349
366
 
350
- // --- Comments & Activity ---
367
+ // --- Comments ---
351
368
  async getComments(taskId: string): Promise<Comment[]> {
352
369
  const snap = await this.db.collection('comments').where('taskId', '==', taskId).get();
353
370
  return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
354
371
  }
355
372
 
373
+ async getComment(id: string): Promise<Comment | null> {
374
+ const snap = await this.db.collection('comments').doc(id).get();
375
+ if (!snap.exists) return null;
376
+ return { ...snap.data(), id: snap.id };
377
+ }
378
+
356
379
  async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
357
380
  const docRef = this.db.collection('comments').doc();
358
381
  const now = new Date().toISOString();
359
382
  const newComment: Comment = { ...comment, id: docRef.id, createdAt: now, updatedAt: now };
360
- await docRef.set(newComment);
383
+ await docRef.set(sanitizeFirestoreData(newComment));
361
384
  return newComment;
362
385
  }
363
386
 
387
+ async updateComment(id: string, updates: Partial<Comment>): Promise<Comment | null> {
388
+ const existing = await this.getComment(id);
389
+ if (!existing) return null;
390
+
391
+ const updated: Comment = {
392
+ ...existing,
393
+ ...updates,
394
+ updatedAt: new Date().toISOString()
395
+ };
396
+ await this.db.collection('comments').doc(id).set(sanitizeFirestoreData(updated), { merge: true });
397
+ return updated;
398
+ }
399
+
400
+ async deleteComment(id: string): Promise<boolean> {
401
+ const existing = await this.getComment(id);
402
+ if (!existing) return false;
403
+ await this.db.collection('comments').doc(id).delete();
404
+ return true;
405
+ }
406
+
407
+ // --- Attachments ---
408
+ async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]> {
409
+ let snap;
410
+ if (filter?.taskId) {
411
+ snap = await this.db.collection('attachments').where('taskId', '==', filter.taskId).get();
412
+ } else if (filter?.projectId) {
413
+ snap = await this.db.collection('attachments').where('projectId', '==', filter.projectId).get();
414
+ } else if (filter?.commentId) {
415
+ snap = await this.db.collection('attachments').where('commentId', '==', filter.commentId).get();
416
+ } else {
417
+ snap = await this.db.collection('attachments').get();
418
+ }
419
+ return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
420
+ }
421
+
422
+ async getAttachment(id: string): Promise<Attachment | null> {
423
+ const snap = await this.db.collection('attachments').doc(id).get();
424
+ if (!snap.exists) return null;
425
+ return { ...snap.data(), id: snap.id };
426
+ }
427
+
428
+ async createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment> {
429
+ const docRef = this.db.collection('attachments').doc();
430
+ const now = new Date().toISOString();
431
+ const newAtt: Attachment = { ...attachment, id: docRef.id, createdAt: now, updatedAt: now };
432
+ await docRef.set(sanitizeFirestoreData(newAtt));
433
+ return newAtt;
434
+ }
435
+
436
+ async deleteAttachment(id: string): Promise<boolean> {
437
+ const existing = await this.getAttachment(id);
438
+ if (!existing) return false;
439
+ await this.db.collection('attachments').doc(id).delete();
440
+ return true;
441
+ }
442
+
364
443
  async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
365
444
  let snap;
366
445
  if (filter?.taskId) {
@@ -377,7 +456,7 @@ export class FirebaseStore implements StorageAdapter {
377
456
  const docRef = this.db.collection('activities').doc();
378
457
  const now = new Date().toISOString();
379
458
  const newAct: Activity = { ...activity, id: docRef.id, createdAt: now };
380
- await docRef.set(newAct);
459
+ await docRef.set(sanitizeFirestoreData(newAct));
381
460
  return newAct;
382
461
  }
383
462
 
@@ -391,7 +470,7 @@ export class FirebaseStore implements StorageAdapter {
391
470
  const docRef = this.db.collection('time_entries').doc();
392
471
  const now = new Date().toISOString();
393
472
  const newEntry: TimeEntry = { ...entry, id: docRef.id, loggedAt: now };
394
- await docRef.set(newEntry);
473
+ await docRef.set(sanitizeFirestoreData(newEntry));
395
474
  return newEntry;
396
475
  }
397
476
 
@@ -404,7 +483,7 @@ export class FirebaseStore implements StorageAdapter {
404
483
  async addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency> {
405
484
  const docRef = this.db.collection('dependencies').doc();
406
485
  const newDep: TaskDependency = { ...dep, id: docRef.id };
407
- await docRef.set(newDep);
486
+ await docRef.set(sanitizeFirestoreData(newDep));
408
487
  return newDep;
409
488
  }
410
489
 
@@ -418,7 +497,7 @@ export class FirebaseStore implements StorageAdapter {
418
497
  const docRef = this.db.collection('webhooks').doc();
419
498
  const now = new Date().toISOString();
420
499
  const newWh: Webhook = { ...webhook, id: docRef.id, createdAt: now };
421
- await docRef.set(newWh);
500
+ await docRef.set(sanitizeFirestoreData(newWh));
422
501
  return newWh;
423
502
  }
424
503
  }
@@ -8,6 +8,7 @@ import type {
8
8
  Team,
9
9
  TaskContainer,
10
10
  Comment,
11
+ Attachment,
11
12
  TimeEntry,
12
13
  Activity,
13
14
  Webhook,
@@ -66,7 +67,17 @@ export interface IterationRepository {
66
67
 
67
68
  export interface CommentRepository {
68
69
  getComments(taskId: string): Promise<Comment[]>;
70
+ getComment(id: string): Promise<Comment | null>;
69
71
  addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment>;
72
+ updateComment(id: string, updates: Partial<Comment>): Promise<Comment | null>;
73
+ deleteComment(id: string): Promise<boolean>;
74
+ }
75
+
76
+ export interface AttachmentRepository {
77
+ getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]>;
78
+ getAttachment(id: string): Promise<Attachment | null>;
79
+ createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment>;
80
+ deleteAttachment(id: string): Promise<boolean>;
70
81
  }
71
82
 
72
83
  export interface ActivityRepository {
@@ -97,6 +108,7 @@ export interface StorageAdapter
97
108
  ContainerRepository,
98
109
  IterationRepository,
99
110
  CommentRepository,
111
+ AttachmentRepository,
100
112
  ActivityRepository,
101
113
  TimeEntryRepository,
102
114
  DependencyRepository,
@@ -110,6 +122,7 @@ export class InMemoryStore implements StorageAdapter {
110
122
  private containers = new Map<string, TaskContainer>();
111
123
  private iterations = new Map<string, Iteration>();
112
124
  private comments = new Map<string, Comment>();
125
+ private attachments = new Map<string, Attachment>();
113
126
  private activities: Activity[] = [];
114
127
  private timeEntries = new Map<string, TimeEntry>();
115
128
  private dependencies = new Map<string, TaskDependency>();
@@ -328,6 +341,10 @@ export class InMemoryStore implements StorageAdapter {
328
341
  return Array.from(this.comments.values()).filter((c) => c.taskId === taskId);
329
342
  }
330
343
 
344
+ async getComment(id: string): Promise<Comment | null> {
345
+ return this.comments.get(id) || null;
346
+ }
347
+
331
348
  async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
332
349
  const id = `cmt_${Math.random().toString(36).substring(2, 9)}`;
333
350
  const now = new Date().toISOString();
@@ -336,6 +353,53 @@ export class InMemoryStore implements StorageAdapter {
336
353
  return newComment;
337
354
  }
338
355
 
356
+ async updateComment(id: string, updates: Partial<Comment>): Promise<Comment | null> {
357
+ const existing = this.comments.get(id);
358
+ if (!existing) return null;
359
+ const updated: Comment = {
360
+ ...existing,
361
+ ...updates,
362
+ updatedAt: new Date().toISOString()
363
+ };
364
+ this.comments.set(id, updated);
365
+ return updated;
366
+ }
367
+
368
+ async deleteComment(id: string): Promise<boolean> {
369
+ return this.comments.delete(id);
370
+ }
371
+
372
+ // Attachments
373
+ async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]> {
374
+ return Array.from(this.attachments.values()).filter((a) => {
375
+ if (filter?.taskId && a.taskId !== filter.taskId) return false;
376
+ if (filter?.projectId && a.projectId !== filter.projectId) return false;
377
+ if (filter?.commentId && a.commentId !== filter.commentId) return false;
378
+ return true;
379
+ });
380
+ }
381
+
382
+ async getAttachment(id: string): Promise<Attachment | null> {
383
+ return this.attachments.get(id) || null;
384
+ }
385
+
386
+ async createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment> {
387
+ const id = `att_${Math.random().toString(36).substring(2, 9)}`;
388
+ const now = new Date().toISOString();
389
+ const newAttachment: Attachment = {
390
+ ...attachment,
391
+ id,
392
+ createdAt: now,
393
+ updatedAt: now
394
+ };
395
+ this.attachments.set(id, newAttachment);
396
+ return newAttachment;
397
+ }
398
+
399
+ async deleteAttachment(id: string): Promise<boolean> {
400
+ return this.attachments.delete(id);
401
+ }
402
+
339
403
  // Activities
340
404
  async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
341
405
  return this.activities.filter((a) => {
@@ -57,12 +57,31 @@ describe('SQLiteStore', () => {
57
57
  const comment = await store.addComment({
58
58
  taskId: 'task_1',
59
59
  authorId: 'user_1',
60
+ authorType: 'user',
60
61
  content: 'Great work!'
61
62
  });
62
63
  expect(comment.id).toMatch(/^cmt_/);
64
+ expect(comment.authorType).toBe('user');
65
+
66
+ // Threaded reply
67
+ const reply = await store.addComment({
68
+ taskId: 'task_1',
69
+ authorId: 'agent_1',
70
+ authorType: 'agent',
71
+ parentId: comment.id,
72
+ content: 'I agree!'
73
+ });
74
+ expect(reply.parentId).toBe(comment.id);
75
+ expect(reply.authorType).toBe('agent');
63
76
 
64
77
  const comments = await store.getComments('task_1');
65
- expect(comments).toHaveLength(1);
78
+ expect(comments).toHaveLength(2);
79
+
80
+ const updatedComment = await store.updateComment(comment.id, { content: 'Updated content' });
81
+ expect(updatedComment?.content).toBe('Updated content');
82
+
83
+ const fetchedComment = await store.getComment(comment.id);
84
+ expect(fetchedComment?.content).toBe('Updated content');
66
85
 
67
86
  const timeEntry = await store.logTime({
68
87
  taskId: 'task_1',
@@ -74,6 +93,45 @@ describe('SQLiteStore', () => {
74
93
 
75
94
  const entries = await store.getTimeEntries('task_1');
76
95
  expect(entries).toHaveLength(1);
96
+
97
+ const deletedComment = await store.deleteComment(reply.id);
98
+ expect(deletedComment).toBe(true);
99
+ expect(await store.getComments('task_1')).toHaveLength(1);
100
+ });
101
+
102
+ it('should create, filter, retrieve, and delete attachments', async () => {
103
+ const attachment = await store.createAttachment({
104
+ filename: 'design-mockup.fig',
105
+ url: 'https://storage.example.com/design-mockup.fig',
106
+ storageKey: 'attachments/task_1/design-mockup.fig',
107
+ mimeType: 'application/octet-stream',
108
+ sizeBytes: 2048,
109
+ taskId: 'task_1',
110
+ projectId: 'proj_1',
111
+ uploaderId: 'user_1',
112
+ uploaderType: 'user'
113
+ });
114
+
115
+ expect(attachment.id).toMatch(/^att_/);
116
+ expect(attachment.filename).toBe('design-mockup.fig');
117
+
118
+ const fetched = await store.getAttachment(attachment.id);
119
+ expect(fetched?.url).toBe('https://storage.example.com/design-mockup.fig');
120
+
121
+ const taskAttachments = await store.getAttachments({ taskId: 'task_1' });
122
+ expect(taskAttachments).toHaveLength(1);
123
+
124
+ const projectAttachments = await store.getAttachments({ projectId: 'proj_1' });
125
+ expect(projectAttachments).toHaveLength(1);
126
+
127
+ const nonExistent = await store.getAttachments({ taskId: 'other_task' });
128
+ expect(nonExistent).toHaveLength(0);
129
+
130
+ const deleted = await store.deleteAttachment(attachment.id);
131
+ expect(deleted).toBe(true);
132
+
133
+ const remaining = await store.getAttachments({ taskId: 'task_1' });
134
+ expect(remaining).toHaveLength(0);
77
135
  });
78
136
 
79
137
  it('should create, retrieve, update, and delete workflows', async () => {
@@ -7,6 +7,7 @@ import type {
7
7
  Team,
8
8
  TaskContainer,
9
9
  Comment,
10
+ Attachment,
10
11
  TimeEntry,
11
12
  Activity,
12
13
  Webhook,
@@ -150,7 +151,27 @@ export class SQLiteStore implements StorageAdapter {
150
151
  id TEXT PRIMARY KEY,
151
152
  taskId TEXT NOT NULL,
152
153
  authorId TEXT NOT NULL,
154
+ authorType TEXT,
155
+ parentId TEXT,
153
156
  content TEXT NOT NULL,
157
+ metadata TEXT,
158
+ createdAt TEXT NOT NULL,
159
+ updatedAt TEXT NOT NULL
160
+ );
161
+
162
+ CREATE TABLE IF NOT EXISTS attachments (
163
+ id TEXT PRIMARY KEY,
164
+ taskId TEXT,
165
+ projectId TEXT,
166
+ commentId TEXT,
167
+ uploaderId TEXT NOT NULL,
168
+ uploaderType TEXT,
169
+ filename TEXT NOT NULL,
170
+ mimeType TEXT NOT NULL,
171
+ sizeBytes INTEGER NOT NULL,
172
+ url TEXT NOT NULL,
173
+ storageKey TEXT,
174
+ metadata TEXT,
154
175
  createdAt TEXT NOT NULL,
155
176
  updatedAt TEXT NOT NULL
156
177
  );
@@ -660,10 +681,24 @@ export class SQLiteStore implements StorageAdapter {
660
681
  return (result.changes ?? 0) > 0;
661
682
  }
662
683
 
663
- // --- Comments & Activity ---
684
+ // --- Comments ---
664
685
  async getComments(taskId: string): Promise<Comment[]> {
665
- const stmt = this.db.prepare('SELECT * FROM comments WHERE taskId = ?');
666
- return stmt.all(taskId) as any[];
686
+ const stmt = this.db.prepare('SELECT * FROM comments WHERE taskId = ? ORDER BY createdAt ASC');
687
+ const rows = stmt.all(taskId) as any[];
688
+ return rows.map((r) => ({
689
+ ...r,
690
+ metadata: r.metadata ? JSON.parse(r.metadata) : undefined
691
+ }));
692
+ }
693
+
694
+ async getComment(id: string): Promise<Comment | null> {
695
+ const stmt = this.db.prepare('SELECT * FROM comments WHERE id = ?');
696
+ const row = stmt.get(id) as any;
697
+ if (!row) return null;
698
+ return {
699
+ ...row,
700
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined
701
+ };
667
702
  }
668
703
 
669
704
  async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
@@ -672,13 +707,144 @@ export class SQLiteStore implements StorageAdapter {
672
707
  const newComment: Comment = { ...comment, id, createdAt: now, updatedAt: now };
673
708
 
674
709
  const stmt = this.db.prepare(`
675
- INSERT INTO comments (id, taskId, authorId, content, createdAt, updatedAt)
676
- VALUES (?, ?, ?, ?, ?, ?)
710
+ INSERT INTO comments (id, taskId, authorId, authorType, parentId, content, metadata, createdAt, updatedAt)
711
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
677
712
  `);
678
- stmt.run(newComment.id, newComment.taskId, newComment.authorId, newComment.content, newComment.createdAt, newComment.updatedAt);
713
+ stmt.run(
714
+ newComment.id,
715
+ newComment.taskId,
716
+ newComment.authorId,
717
+ newComment.authorType || 'user',
718
+ newComment.parentId || null,
719
+ newComment.content,
720
+ newComment.metadata ? JSON.stringify(newComment.metadata) : null,
721
+ newComment.createdAt,
722
+ newComment.updatedAt
723
+ );
679
724
  return newComment;
680
725
  }
681
726
 
727
+ async updateComment(id: string, updates: Partial<Comment>): Promise<Comment | null> {
728
+ const existing = await this.getComment(id);
729
+ if (!existing) return null;
730
+
731
+ const updated: Comment = {
732
+ ...existing,
733
+ ...updates,
734
+ updatedAt: new Date().toISOString()
735
+ };
736
+
737
+ const stmt = this.db.prepare(`
738
+ UPDATE comments SET
739
+ authorId = ?,
740
+ authorType = ?,
741
+ parentId = ?,
742
+ content = ?,
743
+ metadata = ?,
744
+ updatedAt = ?
745
+ WHERE id = ?
746
+ `);
747
+ stmt.run(
748
+ updated.authorId,
749
+ updated.authorType || 'user',
750
+ updated.parentId || null,
751
+ updated.content,
752
+ updated.metadata ? JSON.stringify(updated.metadata) : null,
753
+ updated.updatedAt,
754
+ id
755
+ );
756
+ return updated;
757
+ }
758
+
759
+ async deleteComment(id: string): Promise<boolean> {
760
+ const stmt = this.db.prepare('DELETE FROM comments WHERE id = ?');
761
+ const result = stmt.run(id);
762
+ return (result.changes ?? 0) > 0;
763
+ }
764
+
765
+ // --- Attachments ---
766
+ async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]> {
767
+ let sql = 'SELECT * FROM attachments';
768
+ const params: any[] = [];
769
+ const conditions: string[] = [];
770
+
771
+ if (filter?.taskId) {
772
+ conditions.push('taskId = ?');
773
+ params.push(filter.taskId);
774
+ }
775
+ if (filter?.projectId) {
776
+ conditions.push('projectId = ?');
777
+ params.push(filter.projectId);
778
+ }
779
+ if (filter?.commentId) {
780
+ conditions.push('commentId = ?');
781
+ params.push(filter.commentId);
782
+ }
783
+
784
+ if (conditions.length > 0) {
785
+ sql += ' WHERE ' + conditions.join(' AND ');
786
+ }
787
+ sql += ' ORDER BY createdAt DESC';
788
+
789
+ const stmt = this.db.prepare(sql);
790
+ const rows = stmt.all(...params) as any[];
791
+ return rows.map((r) => ({
792
+ ...r,
793
+ sizeBytes: Number(r.sizeBytes),
794
+ metadata: r.metadata ? JSON.parse(r.metadata) : undefined
795
+ }));
796
+ }
797
+
798
+ async getAttachment(id: string): Promise<Attachment | null> {
799
+ const stmt = this.db.prepare('SELECT * FROM attachments WHERE id = ?');
800
+ const row = stmt.get(id) as any;
801
+ if (!row) return null;
802
+ return {
803
+ ...row,
804
+ sizeBytes: Number(row.sizeBytes),
805
+ metadata: row.metadata ? JSON.parse(row.metadata) : undefined
806
+ };
807
+ }
808
+
809
+ async createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment> {
810
+ const id = `att_${Math.random().toString(36).substring(2, 9)}`;
811
+ const now = new Date().toISOString();
812
+ const newAtt: Attachment = {
813
+ ...attachment,
814
+ id,
815
+ createdAt: now,
816
+ updatedAt: now
817
+ };
818
+
819
+ const stmt = this.db.prepare(`
820
+ INSERT INTO attachments (id, taskId, projectId, commentId, uploaderId, uploaderType, filename, mimeType, sizeBytes, url, storageKey, metadata, createdAt, updatedAt)
821
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
822
+ `);
823
+ stmt.run(
824
+ newAtt.id,
825
+ newAtt.taskId || null,
826
+ newAtt.projectId || null,
827
+ newAtt.commentId || null,
828
+ newAtt.uploaderId,
829
+ newAtt.uploaderType || 'user',
830
+ newAtt.filename,
831
+ newAtt.mimeType,
832
+ newAtt.sizeBytes,
833
+ newAtt.url,
834
+ newAtt.storageKey || null,
835
+ newAtt.metadata ? JSON.stringify(newAtt.metadata) : null,
836
+ newAtt.createdAt,
837
+ newAtt.updatedAt
838
+ );
839
+ return newAtt;
840
+ }
841
+
842
+ async deleteAttachment(id: string): Promise<boolean> {
843
+ const stmt = this.db.prepare('DELETE FROM attachments WHERE id = ?');
844
+ const result = stmt.run(id);
845
+ return (result.changes ?? 0) > 0;
846
+ }
847
+
682
848
  async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
683
849
  let sql = 'SELECT * FROM activities';
684
850
  const params: any[] = [];