@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +57 -21
  3. package/dist/domain/custom-fields.d.ts +9 -0
  4. package/dist/domain/custom-fields.d.ts.map +1 -0
  5. package/dist/domain/custom-fields.js +72 -0
  6. package/dist/domain/custom-fields.js.map +1 -0
  7. package/dist/domain/ddd.test.d.ts +2 -0
  8. package/dist/domain/ddd.test.d.ts.map +1 -0
  9. package/dist/domain/ddd.test.js +246 -0
  10. package/dist/domain/ddd.test.js.map +1 -0
  11. package/dist/domain/entities.d.ts +82 -0
  12. package/dist/domain/entities.d.ts.map +1 -0
  13. package/dist/domain/entities.js +321 -0
  14. package/dist/domain/entities.js.map +1 -0
  15. package/dist/domain/events.d.ts +153 -0
  16. package/dist/domain/events.d.ts.map +1 -0
  17. package/dist/domain/events.js +35 -0
  18. package/dist/domain/events.js.map +1 -0
  19. package/dist/domain/graph.d.ts +19 -0
  20. package/dist/domain/graph.d.ts.map +1 -0
  21. package/dist/domain/graph.js +58 -0
  22. package/dist/domain/graph.js.map +1 -0
  23. package/dist/domain/index.d.ts +5 -0
  24. package/dist/domain/index.d.ts.map +1 -0
  25. package/dist/domain/index.js +5 -0
  26. package/dist/domain/index.js.map +1 -0
  27. package/dist/engine/index.d.ts +32 -2
  28. package/dist/engine/index.d.ts.map +1 -1
  29. package/dist/engine/index.js +416 -6
  30. package/dist/engine/index.js.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/storage/file-storage.d.ts +24 -0
  36. package/dist/storage/file-storage.d.ts.map +1 -0
  37. package/dist/storage/file-storage.js +73 -0
  38. package/dist/storage/file-storage.js.map +1 -0
  39. package/dist/storage/firebase-storage.d.ts +48 -0
  40. package/dist/storage/firebase-storage.d.ts.map +1 -0
  41. package/dist/storage/firebase-storage.js +142 -0
  42. package/dist/storage/firebase-storage.js.map +1 -0
  43. package/dist/storage/index.d.ts +4 -0
  44. package/dist/storage/index.d.ts.map +1 -0
  45. package/dist/storage/index.js +4 -0
  46. package/dist/storage/index.js.map +1 -0
  47. package/dist/storage/s3.d.ts +36 -0
  48. package/dist/storage/s3.d.ts.map +1 -0
  49. package/dist/storage/s3.js +135 -0
  50. package/dist/storage/s3.js.map +1 -0
  51. package/dist/storage/storage.test.d.ts +2 -0
  52. package/dist/storage/storage.test.d.ts.map +1 -0
  53. package/dist/storage/storage.test.js +117 -0
  54. package/dist/storage/storage.test.js.map +1 -0
  55. package/dist/store/firebase.d.ts +12 -1
  56. package/dist/store/firebase.d.ts.map +1 -1
  57. package/dist/store/firebase.js +63 -1
  58. package/dist/store/firebase.js.map +1 -1
  59. package/dist/store/index.d.ts +49 -2
  60. package/dist/store/index.d.ts.map +1 -1
  61. package/dist/store/index.js +49 -0
  62. package/dist/store/index.js.map +1 -1
  63. package/dist/store/sqlite.d.ts +12 -1
  64. package/dist/store/sqlite.d.ts.map +1 -1
  65. package/dist/store/sqlite.js +128 -6
  66. package/dist/store/sqlite.js.map +1 -1
  67. package/dist/store/sqlite.test.js +47 -1
  68. package/dist/store/sqlite.test.js.map +1 -1
  69. package/dist/types/index.d.ts +57 -1
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/package.json +1 -1
  72. package/src/domain/custom-fields.ts +131 -0
  73. package/src/domain/ddd.test.ts +307 -0
  74. package/src/domain/entities.ts +390 -0
  75. package/src/domain/events.ts +196 -0
  76. package/src/domain/graph.ts +70 -0
  77. package/src/domain/index.ts +4 -0
  78. package/src/engine/index.ts +518 -8
  79. package/src/index.ts +2 -0
  80. package/src/storage/file-storage.ts +96 -0
  81. package/src/storage/firebase-storage.ts +177 -0
  82. package/src/storage/index.ts +3 -0
  83. package/src/storage/s3.ts +165 -0
  84. package/src/storage/storage.test.ts +146 -0
  85. package/src/store/firebase.ts +64 -1
  86. package/src/store/index.ts +99 -11
  87. package/src/store/sqlite.test.ts +59 -1
  88. package/src/store/sqlite.ts +172 -6
  89. package/src/types/index.ts +70 -0
  90. package/tsconfig.tsbuildinfo +1 -1
@@ -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,
@@ -16,68 +17,103 @@ import type {
16
17
  } from '../types/index.js';
17
18
  import { generateProjectKey } from '../utils/key.js';
18
19
 
19
- export interface StorageAdapter {
20
- // Projects
20
+ export interface ProjectRepository {
21
21
  getProjects(): Promise<Project[]>;
22
22
  getProject(id: string): Promise<Project | null>;
23
23
  createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project>;
24
24
  updateProject(id: string, updates: Partial<Project>): Promise<Project | null>;
25
25
  deleteProject(id: string): Promise<boolean>;
26
+ }
26
27
 
27
- // Workflows
28
+ export interface WorkflowRepository {
28
29
  getWorkflows(): Promise<Workflow[]>;
29
30
  getWorkflow(id: string): Promise<Workflow | null>;
30
31
  createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow>;
31
32
  updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null>;
32
33
  deleteWorkflow(id: string): Promise<boolean>;
34
+ }
33
35
 
34
- // Tasks
36
+ export interface TaskRepository {
35
37
  getTasks(projectId?: string): Promise<Task[]>;
36
38
  getTask(id: string): Promise<Task | null>;
37
39
  createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task>;
38
40
  updateTask(id: string, updates: Partial<Task>): Promise<Task | null>;
39
41
  deleteTask(id: string): Promise<boolean>;
42
+ }
40
43
 
41
- // Teams
44
+ export interface TeamRepository {
42
45
  getTeams(): Promise<Team[]>;
43
46
  getTeam(id: string): Promise<Team | null>;
44
47
  createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team>;
45
48
  updateTeam(id: string, updates: Partial<Team>): Promise<Team | null>;
46
49
  deleteTeam(id: string): Promise<boolean>;
50
+ }
47
51
 
48
- // Task Containers
52
+ export interface ContainerRepository {
49
53
  getContainers(projectId: string): Promise<TaskContainer[]>;
50
54
  getContainer(id: string): Promise<TaskContainer | null>;
51
55
  createContainer(container: Omit<TaskContainer, 'id' | 'createdAt' | 'updatedAt'>): Promise<TaskContainer>;
52
56
  updateContainer(id: string, updates: Partial<TaskContainer>): Promise<TaskContainer | null>;
53
57
  deleteContainer(id: string): Promise<boolean>;
58
+ }
54
59
 
55
- // Iterations
60
+ export interface IterationRepository {
56
61
  getIterations(projectId: string): Promise<Iteration[]>;
57
62
  getIteration(id: string): Promise<Iteration | null>;
58
63
  createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration>;
59
64
  updateIteration(id: string, updates: Partial<Iteration>): Promise<Iteration | null>;
60
65
  deleteIteration(id: string): Promise<boolean>;
66
+ }
61
67
 
62
- // Comments & Activity
68
+ export interface CommentRepository {
63
69
  getComments(taskId: string): Promise<Comment[]>;
70
+ getComment(id: string): Promise<Comment | null>;
64
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>;
81
+ }
82
+
83
+ export interface ActivityRepository {
65
84
  getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]>;
66
85
  logActivity(activity: Omit<Activity, 'id' | 'createdAt'>): Promise<Activity>;
86
+ }
67
87
 
68
- // Time Tracking
88
+ export interface TimeEntryRepository {
69
89
  getTimeEntries(taskId: string): Promise<TimeEntry[]>;
70
90
  logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt'>): Promise<TimeEntry>;
91
+ }
71
92
 
72
- // Dependencies
93
+ export interface DependencyRepository {
73
94
  getDependencies(taskId: string): Promise<TaskDependency[]>;
74
95
  addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency>;
96
+ }
75
97
 
76
- // Webhooks
98
+ export interface WebhookRepository {
77
99
  getWebhooks(): Promise<Webhook[]>;
78
100
  addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook>;
79
101
  }
80
102
 
103
+ export interface StorageAdapter
104
+ extends ProjectRepository,
105
+ WorkflowRepository,
106
+ TaskRepository,
107
+ TeamRepository,
108
+ ContainerRepository,
109
+ IterationRepository,
110
+ CommentRepository,
111
+ AttachmentRepository,
112
+ ActivityRepository,
113
+ TimeEntryRepository,
114
+ DependencyRepository,
115
+ WebhookRepository {}
116
+
81
117
  export class InMemoryStore implements StorageAdapter {
82
118
  private projects = new Map<string, Project>();
83
119
  private workflows = new Map<string, Workflow>();
@@ -86,6 +122,7 @@ export class InMemoryStore implements StorageAdapter {
86
122
  private containers = new Map<string, TaskContainer>();
87
123
  private iterations = new Map<string, Iteration>();
88
124
  private comments = new Map<string, Comment>();
125
+ private attachments = new Map<string, Attachment>();
89
126
  private activities: Activity[] = [];
90
127
  private timeEntries = new Map<string, TimeEntry>();
91
128
  private dependencies = new Map<string, TaskDependency>();
@@ -304,6 +341,10 @@ export class InMemoryStore implements StorageAdapter {
304
341
  return Array.from(this.comments.values()).filter((c) => c.taskId === taskId);
305
342
  }
306
343
 
344
+ async getComment(id: string): Promise<Comment | null> {
345
+ return this.comments.get(id) || null;
346
+ }
347
+
307
348
  async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
308
349
  const id = `cmt_${Math.random().toString(36).substring(2, 9)}`;
309
350
  const now = new Date().toISOString();
@@ -312,6 +353,53 @@ export class InMemoryStore implements StorageAdapter {
312
353
  return newComment;
313
354
  }
314
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
+
315
403
  // Activities
316
404
  async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
317
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[] = [];
@@ -155,6 +155,13 @@ export interface Task {
155
155
  updatedAt: string;
156
156
  }
157
157
 
158
+ export type CreateTaskInput = Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'status' | 'priority'> & {
159
+ status?: TaskStatus;
160
+ priority?: Priority;
161
+ };
162
+
163
+ export type CreateProjectInput = Omit<Project, 'id' | 'createdAt' | 'updatedAt'>;
164
+
158
165
  export interface Iteration {
159
166
  id: string;
160
167
  projectId: string;
@@ -167,15 +174,73 @@ export interface Iteration {
167
174
  createdAt: string;
168
175
  }
169
176
 
177
+ export type AuthorType = 'user' | 'agent' | 'system';
178
+
170
179
  export interface Comment {
171
180
  id: string;
172
181
  taskId: string;
173
182
  authorId: string;
183
+ authorType?: AuthorType;
184
+ parentId?: string; // Threaded reply support
174
185
  content: string;
186
+ metadata?: Record<string, unknown>;
187
+ createdAt: string;
188
+ updatedAt: string;
189
+ }
190
+
191
+ export interface Attachment {
192
+ id: string;
193
+ taskId?: string;
194
+ projectId?: string;
195
+ commentId?: string;
196
+ uploaderId: string;
197
+ uploaderType?: AuthorType;
198
+ filename: string;
199
+ mimeType: string;
200
+ sizeBytes: number;
201
+ url: string;
202
+ storageKey?: string;
203
+ metadata?: Record<string, unknown>;
175
204
  createdAt: string;
176
205
  updatedAt: string;
177
206
  }
178
207
 
208
+ export type CreateAttachmentInput = Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>;
209
+
210
+ export interface UploadFileInput {
211
+ filename: string;
212
+ data: Uint8Array | ArrayBuffer | Buffer | Blob | string;
213
+ mimeType?: string;
214
+ pathPrefix?: string;
215
+ }
216
+
217
+ export interface UploadFileResult {
218
+ storageKey: string;
219
+ url: string;
220
+ sizeBytes: number;
221
+ mimeType: string;
222
+ }
223
+
224
+ export interface PresignedUrlOptions {
225
+ storageKey: string;
226
+ expiresInSeconds?: number;
227
+ contentType?: string;
228
+ }
229
+
230
+ export interface PresignedUploadResult {
231
+ uploadUrl: string;
232
+ storageKey: string;
233
+ method?: 'PUT' | 'POST';
234
+ headers?: Record<string, string>;
235
+ }
236
+
237
+ export interface FileStorageAdapter {
238
+ upload(input: UploadFileInput): Promise<UploadFileResult>;
239
+ delete(storageKey: string): Promise<boolean>;
240
+ getDownloadUrl?(storageKey: string): Promise<string>;
241
+ getPresignedUploadUrl?(options: PresignedUrlOptions): Promise<PresignedUploadResult>;
242
+ }
243
+
179
244
  export interface TimeEntry {
180
245
  id: string;
181
246
  taskId: string;
@@ -214,6 +279,10 @@ export type WebhookEvent =
214
279
  | 'task.deleted'
215
280
  | 'task.status_changed'
216
281
  | 'comment.created'
282
+ | 'comment.updated'
283
+ | 'comment.deleted'
284
+ | 'attachment.created'
285
+ | 'attachment.deleted'
217
286
  | 'iteration.started'
218
287
  | 'iteration.completed'
219
288
  | 'team.created'
@@ -243,6 +312,7 @@ export interface CriticalPathPlugin {
243
312
 
244
313
  export interface CriticalPathConfig {
245
314
  store?: 'memory' | 'sqlite' | unknown;
315
+ fileStorage?: FileStorageAdapter;
246
316
  plugins?: CriticalPathPlugin[];
247
317
  webhooks?: Omit<Webhook, 'id' | 'createdAt'>[];
248
318
  initialData?: {
@@ -1 +1 @@
1
- {"root":["./src/index.test.ts","./src/index.ts","./src/use-case-validation.test.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
1
+ {"root":["./src/index.test.ts","./src/index.ts","./src/use-case-validation.test.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}