@critical-path/core 0.1.2 → 0.2.1

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 (50) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +43 -26
  3. package/dist/engine/index.d.ts +19 -1
  4. package/dist/engine/index.d.ts.map +1 -1
  5. package/dist/engine/index.js +129 -6
  6. package/dist/engine/index.js.map +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.test.js +96 -1
  12. package/dist/index.test.js.map +1 -1
  13. package/dist/store/firebase.d.ts +113 -0
  14. package/dist/store/firebase.d.ts.map +1 -0
  15. package/dist/store/firebase.js +319 -0
  16. package/dist/store/firebase.js.map +1 -0
  17. package/dist/store/firebase.test.d.ts +2 -0
  18. package/dist/store/firebase.test.d.ts.map +1 -0
  19. package/dist/store/firebase.test.js +45 -0
  20. package/dist/store/firebase.test.js.map +1 -0
  21. package/dist/store/index.d.ts +36 -8
  22. package/dist/store/index.d.ts.map +1 -1
  23. package/dist/store/index.js +78 -12
  24. package/dist/store/index.js.map +1 -1
  25. package/dist/store/sqlite.d.ts +61 -0
  26. package/dist/store/sqlite.d.ts.map +1 -0
  27. package/dist/store/sqlite.js +492 -0
  28. package/dist/store/sqlite.js.map +1 -0
  29. package/dist/store/sqlite.test.d.ts +2 -0
  30. package/dist/store/sqlite.test.d.ts.map +1 -0
  31. package/dist/store/sqlite.test.js +63 -0
  32. package/dist/store/sqlite.test.js.map +1 -0
  33. package/dist/types/index.d.ts +68 -8
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/dist/utils/status.d.ts +14 -0
  36. package/dist/utils/status.d.ts.map +1 -0
  37. package/dist/utils/status.js +58 -0
  38. package/dist/utils/status.js.map +1 -0
  39. package/package.json +9 -1
  40. package/src/engine/index.ts +161 -8
  41. package/src/index.test.ts +113 -1
  42. package/src/index.ts +1 -0
  43. package/src/store/firebase.test.ts +59 -0
  44. package/src/store/firebase.ts +381 -0
  45. package/src/store/index.ts +113 -19
  46. package/src/store/sqlite.test.ts +78 -0
  47. package/src/store/sqlite.ts +729 -0
  48. package/src/types/index.ts +74 -9
  49. package/src/utils/status.ts +85 -0
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,729 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import type { StorageAdapter } from './index.js';
3
+ import type {
4
+ Project,
5
+ Task,
6
+ Iteration,
7
+ Team,
8
+ TaskContainer,
9
+ Comment,
10
+ TimeEntry,
11
+ Activity,
12
+ Webhook,
13
+ TaskDependency
14
+ } from '../types/index.js';
15
+
16
+ export interface SQLiteStoreConfig {
17
+ /**
18
+ * Database file path (e.g., 'critical-path.db' or ':memory:').
19
+ * Defaults to ':memory:'.
20
+ */
21
+ filename?: string;
22
+ /**
23
+ * Existing DatabaseSync instance or custom SQLite instance.
24
+ */
25
+ db?: DatabaseSync;
26
+ }
27
+
28
+ export class SQLiteStore implements StorageAdapter {
29
+ private db: DatabaseSync;
30
+
31
+ constructor(config: SQLiteStoreConfig = {}) {
32
+ if (config.db) {
33
+ this.db = config.db;
34
+ } else {
35
+ this.db = new DatabaseSync(config.filename || ':memory:');
36
+ }
37
+ this.initTables();
38
+ }
39
+
40
+ private initTables(): void {
41
+ this.db.exec(`
42
+ CREATE TABLE IF NOT EXISTS projects (
43
+ id TEXT PRIMARY KEY,
44
+ key TEXT NOT NULL,
45
+ name TEXT NOT NULL,
46
+ description TEXT,
47
+ ownerId TEXT,
48
+ members TEXT,
49
+ teamIds TEXT,
50
+ statusDefinitions TEXT,
51
+ priorityDefinitions TEXT,
52
+ customFieldDefinitions TEXT,
53
+ createdAt TEXT NOT NULL,
54
+ updatedAt TEXT NOT NULL
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS teams (
58
+ id TEXT PRIMARY KEY,
59
+ name TEXT NOT NULL,
60
+ description TEXT,
61
+ leaderId TEXT,
62
+ memberIds TEXT NOT NULL,
63
+ createdAt TEXT NOT NULL,
64
+ updatedAt TEXT NOT NULL
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS containers (
68
+ id TEXT PRIMARY KEY,
69
+ projectId TEXT NOT NULL,
70
+ name TEXT NOT NULL,
71
+ description TEXT,
72
+ parentId TEXT,
73
+ type TEXT,
74
+ color TEXT,
75
+ createdAt TEXT NOT NULL,
76
+ updatedAt TEXT NOT NULL
77
+ );
78
+
79
+ CREATE TABLE IF NOT EXISTS tasks (
80
+ id TEXT PRIMARY KEY,
81
+ projectId TEXT NOT NULL,
82
+ title TEXT NOT NULL,
83
+ description TEXT,
84
+ status TEXT NOT NULL,
85
+ priority TEXT NOT NULL,
86
+ assigneeId TEXT,
87
+ reporterId TEXT,
88
+ reviewerId TEXT,
89
+ iterationId TEXT,
90
+ teamId TEXT,
91
+ containerId TEXT,
92
+ plannedStartDate TEXT,
93
+ actualStartDate TEXT,
94
+ actualEndDate TEXT,
95
+ dueDate TEXT,
96
+ estimatedHours REAL,
97
+ loggedHours REAL,
98
+ actualHours REAL,
99
+ billableHours REAL,
100
+ estimatedDurationMinutes REAL,
101
+ actualDurationMinutes REAL,
102
+ billableDurationMinutes REAL,
103
+ progress REAL,
104
+ tags TEXT,
105
+ customFields TEXT,
106
+ parentId TEXT,
107
+ createdAt TEXT NOT NULL,
108
+ updatedAt TEXT NOT NULL
109
+ );
110
+
111
+ CREATE TABLE IF NOT EXISTS iterations (
112
+ id TEXT PRIMARY KEY,
113
+ projectId TEXT NOT NULL,
114
+ name TEXT NOT NULL,
115
+ goal TEXT,
116
+ type TEXT,
117
+ startDate TEXT,
118
+ endDate TEXT,
119
+ status TEXT NOT NULL,
120
+ createdAt TEXT NOT NULL
121
+ );
122
+
123
+ CREATE TABLE IF NOT EXISTS comments (
124
+ id TEXT PRIMARY KEY,
125
+ taskId TEXT NOT NULL,
126
+ authorId TEXT NOT NULL,
127
+ content TEXT NOT NULL,
128
+ createdAt TEXT NOT NULL,
129
+ updatedAt TEXT NOT NULL
130
+ );
131
+
132
+ CREATE TABLE IF NOT EXISTS activities (
133
+ id TEXT PRIMARY KEY,
134
+ projectId TEXT,
135
+ taskId TEXT,
136
+ actorId TEXT NOT NULL,
137
+ action TEXT NOT NULL,
138
+ details TEXT,
139
+ createdAt TEXT NOT NULL
140
+ );
141
+
142
+ CREATE TABLE IF NOT EXISTS time_entries (
143
+ id TEXT PRIMARY KEY,
144
+ taskId TEXT NOT NULL,
145
+ userId TEXT NOT NULL,
146
+ hours REAL NOT NULL,
147
+ isBillable INTEGER,
148
+ description TEXT,
149
+ loggedAt TEXT NOT NULL
150
+ );
151
+
152
+ CREATE TABLE IF NOT EXISTS dependencies (
153
+ id TEXT PRIMARY KEY,
154
+ taskId TEXT NOT NULL,
155
+ dependsOnTaskId TEXT NOT NULL,
156
+ type TEXT NOT NULL
157
+ );
158
+
159
+ CREATE TABLE IF NOT EXISTS webhooks (
160
+ id TEXT PRIMARY KEY,
161
+ url TEXT NOT NULL,
162
+ events TEXT NOT NULL,
163
+ secret TEXT,
164
+ active INTEGER NOT NULL,
165
+ createdAt TEXT NOT NULL
166
+ );
167
+ `);
168
+ }
169
+
170
+ // --- Projects ---
171
+ async getProjects(): Promise<Project[]> {
172
+ const stmt = this.db.prepare('SELECT * FROM projects');
173
+ const rows = stmt.all() as any[];
174
+ return rows.map((r) => this.mapProject(r));
175
+ }
176
+
177
+ async getProject(id: string): Promise<Project | null> {
178
+ const stmt = this.db.prepare('SELECT * FROM projects WHERE id = ?');
179
+ const row = stmt.get(id) as any;
180
+ return row ? this.mapProject(row) : null;
181
+ }
182
+
183
+ async createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
184
+ const id = `proj_${Math.random().toString(36).substring(2, 9)}`;
185
+ const now = new Date().toISOString();
186
+ const newProj: Project = { ...project, id, createdAt: now, updatedAt: now };
187
+
188
+ const stmt = this.db.prepare(`
189
+ INSERT INTO projects (id, key, name, description, ownerId, members, teamIds, statusDefinitions, priorityDefinitions, customFieldDefinitions, createdAt, updatedAt)
190
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
191
+ `);
192
+ stmt.run(
193
+ newProj.id,
194
+ newProj.key,
195
+ newProj.name,
196
+ newProj.description || null,
197
+ newProj.ownerId || null,
198
+ JSON.stringify(newProj.members || []),
199
+ JSON.stringify(newProj.teamIds || []),
200
+ JSON.stringify(newProj.statusDefinitions || []),
201
+ JSON.stringify(newProj.priorityDefinitions || []),
202
+ JSON.stringify(newProj.customFieldDefinitions || []),
203
+ newProj.createdAt,
204
+ newProj.updatedAt
205
+ );
206
+ return newProj;
207
+ }
208
+
209
+ async updateProject(id: string, updates: Partial<Project>): Promise<Project | null> {
210
+ const existing = await this.getProject(id);
211
+ if (!existing) return null;
212
+
213
+ const updated: Project = {
214
+ ...existing,
215
+ ...updates,
216
+ updatedAt: new Date().toISOString()
217
+ };
218
+
219
+ const stmt = this.db.prepare(`
220
+ UPDATE projects
221
+ SET key = ?, name = ?, description = ?, ownerId = ?, members = ?, teamIds = ?, statusDefinitions = ?, priorityDefinitions = ?, customFieldDefinitions = ?, updatedAt = ?
222
+ WHERE id = ?
223
+ `);
224
+ stmt.run(
225
+ updated.key,
226
+ updated.name,
227
+ updated.description || null,
228
+ updated.ownerId || null,
229
+ JSON.stringify(updated.members || []),
230
+ JSON.stringify(updated.teamIds || []),
231
+ JSON.stringify(updated.statusDefinitions || []),
232
+ JSON.stringify(updated.priorityDefinitions || []),
233
+ JSON.stringify(updated.customFieldDefinitions || []),
234
+ updated.updatedAt,
235
+ id
236
+ );
237
+ return updated;
238
+ }
239
+
240
+ async deleteProject(id: string): Promise<boolean> {
241
+ const stmt = this.db.prepare('DELETE FROM projects WHERE id = ?');
242
+ const result = stmt.run(id);
243
+ return (result.changes ?? 0) > 0;
244
+ }
245
+
246
+ // --- Teams ---
247
+ async getTeams(): Promise<Team[]> {
248
+ const stmt = this.db.prepare('SELECT * FROM teams');
249
+ const rows = stmt.all() as any[];
250
+ return rows.map((r) => this.mapTeam(r));
251
+ }
252
+
253
+ async getTeam(id: string): Promise<Team | null> {
254
+ const stmt = this.db.prepare('SELECT * FROM teams WHERE id = ?');
255
+ const row = stmt.get(id) as any;
256
+ return row ? this.mapTeam(row) : null;
257
+ }
258
+
259
+ async createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team> {
260
+ const id = `team_${Math.random().toString(36).substring(2, 9)}`;
261
+ const now = new Date().toISOString();
262
+ const newTeam: Team = { ...team, id, createdAt: now, updatedAt: now };
263
+
264
+ const stmt = this.db.prepare(`
265
+ INSERT INTO teams (id, name, description, leaderId, memberIds, createdAt, updatedAt)
266
+ VALUES (?, ?, ?, ?, ?, ?, ?)
267
+ `);
268
+ stmt.run(
269
+ newTeam.id,
270
+ newTeam.name,
271
+ newTeam.description || null,
272
+ newTeam.leaderId || null,
273
+ JSON.stringify(newTeam.memberIds || []),
274
+ newTeam.createdAt,
275
+ newTeam.updatedAt
276
+ );
277
+ return newTeam;
278
+ }
279
+
280
+ async updateTeam(id: string, updates: Partial<Team>): Promise<Team | null> {
281
+ const existing = await this.getTeam(id);
282
+ if (!existing) return null;
283
+
284
+ const updated: Team = { ...existing, ...updates, updatedAt: new Date().toISOString() };
285
+ const stmt = this.db.prepare(`
286
+ UPDATE teams SET name = ?, description = ?, leaderId = ?, memberIds = ?, updatedAt = ?
287
+ WHERE id = ?
288
+ `);
289
+ stmt.run(
290
+ updated.name,
291
+ updated.description || null,
292
+ updated.leaderId || null,
293
+ JSON.stringify(updated.memberIds || []),
294
+ updated.updatedAt,
295
+ id
296
+ );
297
+ return updated;
298
+ }
299
+
300
+ async deleteTeam(id: string): Promise<boolean> {
301
+ const stmt = this.db.prepare('DELETE FROM teams WHERE id = ?');
302
+ const result = stmt.run(id);
303
+ return (result.changes ?? 0) > 0;
304
+ }
305
+
306
+ // --- Task Containers ---
307
+ async getContainers(projectId: string): Promise<TaskContainer[]> {
308
+ const stmt = this.db.prepare('SELECT * FROM containers WHERE projectId = ?');
309
+ const rows = stmt.all(projectId) as any[];
310
+ return rows;
311
+ }
312
+
313
+ async getContainer(id: string): Promise<TaskContainer | null> {
314
+ const stmt = this.db.prepare('SELECT * FROM containers WHERE id = ?');
315
+ const row = stmt.get(id) as any;
316
+ return row || null;
317
+ }
318
+
319
+ async createContainer(container: Omit<TaskContainer, 'id' | 'createdAt' | 'updatedAt'>): Promise<TaskContainer> {
320
+ const id = `cnt_${Math.random().toString(36).substring(2, 9)}`;
321
+ const now = new Date().toISOString();
322
+ const newContainer: TaskContainer = { ...container, id, createdAt: now, updatedAt: now };
323
+
324
+ const stmt = this.db.prepare(`
325
+ INSERT INTO containers (id, projectId, name, description, parentId, type, color, createdAt, updatedAt)
326
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
327
+ `);
328
+ stmt.run(
329
+ newContainer.id,
330
+ newContainer.projectId,
331
+ newContainer.name,
332
+ newContainer.description || null,
333
+ newContainer.parentId || null,
334
+ newContainer.type || null,
335
+ newContainer.color || null,
336
+ newContainer.createdAt,
337
+ newContainer.updatedAt
338
+ );
339
+ return newContainer;
340
+ }
341
+
342
+ async updateContainer(id: string, updates: Partial<TaskContainer>): Promise<TaskContainer | null> {
343
+ const existing = await this.getContainer(id);
344
+ if (!existing) return null;
345
+
346
+ const updated: TaskContainer = { ...existing, ...updates, updatedAt: new Date().toISOString() };
347
+ const stmt = this.db.prepare(`
348
+ UPDATE containers SET name = ?, description = ?, parentId = ?, type = ?, color = ?, updatedAt = ?
349
+ WHERE id = ?
350
+ `);
351
+ stmt.run(
352
+ updated.name,
353
+ updated.description || null,
354
+ updated.parentId || null,
355
+ updated.type || null,
356
+ updated.color || null,
357
+ updated.updatedAt,
358
+ id
359
+ );
360
+ return updated;
361
+ }
362
+
363
+ async deleteContainer(id: string): Promise<boolean> {
364
+ const stmt = this.db.prepare('DELETE FROM containers WHERE id = ?');
365
+ const result = stmt.run(id);
366
+ return (result.changes ?? 0) > 0;
367
+ }
368
+
369
+ // --- Tasks ---
370
+ async getTasks(projectId?: string): Promise<Task[]> {
371
+ let rows: any[];
372
+ if (projectId) {
373
+ const stmt = this.db.prepare('SELECT * FROM tasks WHERE projectId = ?');
374
+ rows = stmt.all(projectId) as any[];
375
+ } else {
376
+ const stmt = this.db.prepare('SELECT * FROM tasks');
377
+ rows = stmt.all() as any[];
378
+ }
379
+ return rows.map((r) => this.mapTask(r));
380
+ }
381
+
382
+ async getTask(id: string): Promise<Task | null> {
383
+ const stmt = this.db.prepare('SELECT * FROM tasks WHERE id = ?');
384
+ const row = stmt.get(id) as any;
385
+ return row ? this.mapTask(row) : null;
386
+ }
387
+
388
+ async createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
389
+ const id = `task_${Math.random().toString(36).substring(2, 9)}`;
390
+ const now = new Date().toISOString();
391
+ const newTask: Task = { ...task, id, createdAt: now, updatedAt: now };
392
+
393
+ const stmt = this.db.prepare(`
394
+ INSERT INTO tasks (
395
+ id, projectId, title, description, status, priority, assigneeId, reporterId,
396
+ reviewerId, iterationId, teamId, containerId, plannedStartDate, actualStartDate, actualEndDate,
397
+ dueDate, estimatedHours, loggedHours, actualHours, billableHours,
398
+ estimatedDurationMinutes, actualDurationMinutes, billableDurationMinutes, progress,
399
+ tags, customFields, parentId, createdAt, updatedAt
400
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
401
+ `);
402
+ stmt.run(
403
+ newTask.id,
404
+ newTask.projectId,
405
+ newTask.title,
406
+ newTask.description || null,
407
+ newTask.status,
408
+ newTask.priority,
409
+ newTask.assigneeId || null,
410
+ newTask.reporterId || null,
411
+ newTask.reviewerId || null,
412
+ newTask.iterationId || null,
413
+ newTask.teamId || null,
414
+ newTask.containerId || null,
415
+ newTask.plannedStartDate || null,
416
+ newTask.actualStartDate || null,
417
+ newTask.actualEndDate || null,
418
+ newTask.dueDate || null,
419
+ newTask.estimatedHours ?? null,
420
+ newTask.loggedHours ?? null,
421
+ newTask.actualHours ?? null,
422
+ newTask.billableHours ?? null,
423
+ newTask.estimatedDurationMinutes ?? null,
424
+ newTask.actualDurationMinutes ?? null,
425
+ newTask.billableDurationMinutes ?? null,
426
+ newTask.progress ?? null,
427
+ JSON.stringify(newTask.tags || []),
428
+ JSON.stringify(newTask.customFields || {}),
429
+ newTask.parentId || null,
430
+ newTask.createdAt,
431
+ newTask.updatedAt
432
+ );
433
+ return newTask;
434
+ }
435
+
436
+ async updateTask(id: string, updates: Partial<Task>): Promise<Task | null> {
437
+ const existing = await this.getTask(id);
438
+ if (!existing) return null;
439
+
440
+ const updated: Task = {
441
+ ...existing,
442
+ ...updates,
443
+ updatedAt: new Date().toISOString()
444
+ };
445
+
446
+ const stmt = this.db.prepare(`
447
+ UPDATE tasks SET
448
+ projectId = ?, title = ?, description = ?, status = ?, priority = ?,
449
+ assigneeId = ?, reporterId = ?, reviewerId = ?, iterationId = ?, teamId = ?, containerId = ?,
450
+ plannedStartDate = ?, actualStartDate = ?, actualEndDate = ?, dueDate = ?,
451
+ estimatedHours = ?, loggedHours = ?, actualHours = ?, billableHours = ?,
452
+ estimatedDurationMinutes = ?, actualDurationMinutes = ?, billableDurationMinutes = ?, progress = ?,
453
+ tags = ?, customFields = ?, parentId = ?, updatedAt = ?
454
+ WHERE id = ?
455
+ `);
456
+ stmt.run(
457
+ updated.projectId,
458
+ updated.title,
459
+ updated.description || null,
460
+ updated.status,
461
+ updated.priority,
462
+ updated.assigneeId || null,
463
+ updated.reporterId || null,
464
+ updated.reviewerId || null,
465
+ updated.iterationId || null,
466
+ updated.teamId || null,
467
+ updated.containerId || null,
468
+ updated.plannedStartDate || null,
469
+ updated.actualStartDate || null,
470
+ updated.actualEndDate || null,
471
+ updated.dueDate || null,
472
+ updated.estimatedHours ?? null,
473
+ updated.loggedHours ?? null,
474
+ updated.actualHours ?? null,
475
+ updated.billableHours ?? null,
476
+ updated.estimatedDurationMinutes ?? null,
477
+ updated.actualDurationMinutes ?? null,
478
+ updated.billableDurationMinutes ?? null,
479
+ updated.progress ?? null,
480
+ JSON.stringify(updated.tags || []),
481
+ JSON.stringify(updated.customFields || {}),
482
+ updated.parentId || null,
483
+ updated.updatedAt,
484
+ id
485
+ );
486
+ return updated;
487
+ }
488
+
489
+ async deleteTask(id: string): Promise<boolean> {
490
+ const stmt = this.db.prepare('DELETE FROM tasks WHERE id = ?');
491
+ const result = stmt.run(id);
492
+ return (result.changes ?? 0) > 0;
493
+ }
494
+
495
+ // --- Iterations ---
496
+ async getIterations(projectId: string): Promise<Iteration[]> {
497
+ const stmt = this.db.prepare('SELECT * FROM iterations WHERE projectId = ?');
498
+ const rows = stmt.all(projectId) as any[];
499
+ return rows;
500
+ }
501
+
502
+ async getIteration(id: string): Promise<Iteration | null> {
503
+ const stmt = this.db.prepare('SELECT * FROM iterations WHERE id = ?');
504
+ const row = stmt.get(id) as any;
505
+ return row || null;
506
+ }
507
+
508
+ async createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration> {
509
+ const id = `iter_${Math.random().toString(36).substring(2, 9)}`;
510
+ const now = new Date().toISOString();
511
+ const newIteration: Iteration = { ...iteration, id, createdAt: now };
512
+
513
+ const stmt = this.db.prepare(`
514
+ INSERT INTO iterations (id, projectId, name, goal, type, startDate, endDate, status, createdAt)
515
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
516
+ `);
517
+ stmt.run(
518
+ newIteration.id,
519
+ newIteration.projectId,
520
+ newIteration.name,
521
+ newIteration.goal || null,
522
+ newIteration.type || null,
523
+ newIteration.startDate || null,
524
+ newIteration.endDate || null,
525
+ newIteration.status,
526
+ newIteration.createdAt
527
+ );
528
+ return newIteration;
529
+ }
530
+
531
+ async updateIteration(id: string, updates: Partial<Iteration>): Promise<Iteration | null> {
532
+ const existing = await this.getIteration(id);
533
+ if (!existing) return null;
534
+
535
+ const updated: Iteration = { ...existing, ...updates };
536
+
537
+ const updateStmt = this.db.prepare(`
538
+ UPDATE iterations SET name = ?, goal = ?, type = ?, startDate = ?, endDate = ?, status = ?
539
+ WHERE id = ?
540
+ `);
541
+ updateStmt.run(
542
+ updated.name,
543
+ updated.goal || null,
544
+ updated.type || null,
545
+ updated.startDate || null,
546
+ updated.endDate || null,
547
+ updated.status,
548
+ id
549
+ );
550
+ return updated;
551
+ }
552
+
553
+ async deleteIteration(id: string): Promise<boolean> {
554
+ const stmt = this.db.prepare('DELETE FROM iterations WHERE id = ?');
555
+ const result = stmt.run(id);
556
+ return (result.changes ?? 0) > 0;
557
+ }
558
+
559
+ // --- Comments & Activity ---
560
+ async getComments(taskId: string): Promise<Comment[]> {
561
+ const stmt = this.db.prepare('SELECT * FROM comments WHERE taskId = ?');
562
+ return stmt.all(taskId) as any[];
563
+ }
564
+
565
+ async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
566
+ const id = `cmt_${Math.random().toString(36).substring(2, 9)}`;
567
+ const now = new Date().toISOString();
568
+ const newComment: Comment = { ...comment, id, createdAt: now, updatedAt: now };
569
+
570
+ const stmt = this.db.prepare(`
571
+ INSERT INTO comments (id, taskId, authorId, content, createdAt, updatedAt)
572
+ VALUES (?, ?, ?, ?, ?, ?)
573
+ `);
574
+ stmt.run(newComment.id, newComment.taskId, newComment.authorId, newComment.content, newComment.createdAt, newComment.updatedAt);
575
+ return newComment;
576
+ }
577
+
578
+ async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
579
+ let sql = 'SELECT * FROM activities';
580
+ const params: any[] = [];
581
+ const conditions: string[] = [];
582
+
583
+ if (filter?.projectId) {
584
+ conditions.push('projectId = ?');
585
+ params.push(filter.projectId);
586
+ }
587
+ if (filter?.taskId) {
588
+ conditions.push('taskId = ?');
589
+ params.push(filter.taskId);
590
+ }
591
+
592
+ if (conditions.length > 0) {
593
+ sql += ' WHERE ' + conditions.join(' AND ');
594
+ }
595
+ sql += ' ORDER BY createdAt DESC';
596
+
597
+ const stmt = this.db.prepare(sql);
598
+ const rows = stmt.all(...params) as any[];
599
+ return rows.map((r) => ({
600
+ ...r,
601
+ details: r.details ? JSON.parse(r.details) : undefined
602
+ }));
603
+ }
604
+
605
+ async logActivity(activity: Omit<Activity, 'id' | 'createdAt'>): Promise<Activity> {
606
+ const id = `act_${Math.random().toString(36).substring(2, 9)}`;
607
+ const now = new Date().toISOString();
608
+ const newAct: Activity = { ...activity, id, createdAt: now };
609
+
610
+ const stmt = this.db.prepare(`
611
+ INSERT INTO activities (id, projectId, taskId, actorId, action, details, createdAt)
612
+ VALUES (?, ?, ?, ?, ?, ?, ?)
613
+ `);
614
+ stmt.run(
615
+ newAct.id,
616
+ newAct.projectId || null,
617
+ newAct.taskId || null,
618
+ newAct.actorId,
619
+ newAct.action,
620
+ newAct.details ? JSON.stringify(newAct.details) : null,
621
+ newAct.createdAt
622
+ );
623
+ return newAct;
624
+ }
625
+
626
+ // --- Time Tracking ---
627
+ async getTimeEntries(taskId: string): Promise<TimeEntry[]> {
628
+ const stmt = this.db.prepare('SELECT * FROM time_entries WHERE taskId = ?');
629
+ const rows = stmt.all(taskId) as any[];
630
+ return rows.map((r) => ({ ...r, isBillable: r.isBillable !== null ? Boolean(r.isBillable) : undefined }));
631
+ }
632
+
633
+ async logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt'>): Promise<TimeEntry> {
634
+ const id = `time_${Math.random().toString(36).substring(2, 9)}`;
635
+ const now = new Date().toISOString();
636
+ const newEntry: TimeEntry = { ...entry, id, loggedAt: now };
637
+
638
+ const stmt = this.db.prepare(`
639
+ INSERT INTO time_entries (id, taskId, userId, hours, isBillable, description, loggedAt)
640
+ VALUES (?, ?, ?, ?, ?, ?, ?)
641
+ `);
642
+ stmt.run(
643
+ newEntry.id,
644
+ newEntry.taskId,
645
+ newEntry.userId,
646
+ newEntry.hours,
647
+ newEntry.isBillable !== undefined ? (newEntry.isBillable ? 1 : 0) : null,
648
+ newEntry.description || null,
649
+ newEntry.loggedAt
650
+ );
651
+ return newEntry;
652
+ }
653
+
654
+ // --- Dependencies ---
655
+ async getDependencies(taskId: string): Promise<TaskDependency[]> {
656
+ const stmt = this.db.prepare('SELECT * FROM dependencies WHERE taskId = ? OR dependsOnTaskId = ?');
657
+ return stmt.all(taskId, taskId) as any[];
658
+ }
659
+
660
+ async addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency> {
661
+ const id = `dep_${Math.random().toString(36).substring(2, 9)}`;
662
+ const newDep: TaskDependency = { ...dep, id };
663
+
664
+ const stmt = this.db.prepare(`
665
+ INSERT INTO dependencies (id, taskId, dependsOnTaskId, type)
666
+ VALUES (?, ?, ?, ?)
667
+ `);
668
+ stmt.run(newDep.id, newDep.taskId, newDep.dependsOnTaskId, newDep.type);
669
+ return newDep;
670
+ }
671
+
672
+ // --- Webhooks ---
673
+ async getWebhooks(): Promise<Webhook[]> {
674
+ const stmt = this.db.prepare('SELECT * FROM webhooks');
675
+ const rows = stmt.all() as any[];
676
+ return rows.map((r) => ({
677
+ ...r,
678
+ events: JSON.parse(r.events),
679
+ active: Boolean(r.active)
680
+ }));
681
+ }
682
+
683
+ async addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook> {
684
+ const id = `wh_${Math.random().toString(36).substring(2, 9)}`;
685
+ const now = new Date().toISOString();
686
+ const newWh: Webhook = { ...webhook, id, createdAt: now };
687
+
688
+ const stmt = this.db.prepare(`
689
+ INSERT INTO webhooks (id, url, events, secret, active, createdAt)
690
+ VALUES (?, ?, ?, ?, ?, ?)
691
+ `);
692
+ stmt.run(
693
+ newWh.id,
694
+ newWh.url,
695
+ JSON.stringify(newWh.events),
696
+ newWh.secret || null,
697
+ newWh.active ? 1 : 0,
698
+ newWh.createdAt
699
+ );
700
+ return newWh;
701
+ }
702
+
703
+ // Helper mappers
704
+ private mapProject(row: any): Project {
705
+ return {
706
+ ...row,
707
+ members: row.members ? JSON.parse(row.members) : [],
708
+ teamIds: row.teamIds ? JSON.parse(row.teamIds) : [],
709
+ statusDefinitions: row.statusDefinitions ? JSON.parse(row.statusDefinitions) : [],
710
+ priorityDefinitions: row.priorityDefinitions ? JSON.parse(row.priorityDefinitions) : [],
711
+ customFieldDefinitions: row.customFieldDefinitions ? JSON.parse(row.customFieldDefinitions) : []
712
+ };
713
+ }
714
+
715
+ private mapTeam(row: any): Team {
716
+ return {
717
+ ...row,
718
+ memberIds: row.memberIds ? JSON.parse(row.memberIds) : []
719
+ };
720
+ }
721
+
722
+ private mapTask(row: any): Task {
723
+ return {
724
+ ...row,
725
+ tags: row.tags ? JSON.parse(row.tags) : [],
726
+ customFields: row.customFields ? JSON.parse(row.customFields) : {}
727
+ };
728
+ }
729
+ }