@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.
- package/CHANGELOG.md +35 -0
- package/README.md +6 -0
- package/dist/domain/entities.d.ts +4 -0
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +14 -0
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/events.d.ts +32 -4
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js.map +1 -1
- package/dist/engine/index.d.ts +23 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +132 -2
- package/dist/engine/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +26 -0
- package/dist/index.test.js.map +1 -1
- package/dist/storage/file-storage.d.ts +24 -0
- package/dist/storage/file-storage.d.ts.map +1 -0
- package/dist/storage/file-storage.js +73 -0
- package/dist/storage/file-storage.js.map +1 -0
- package/dist/storage/firebase-storage.d.ts +48 -0
- package/dist/storage/firebase-storage.d.ts.map +1 -0
- package/dist/storage/firebase-storage.js +142 -0
- package/dist/storage/firebase-storage.js.map +1 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/s3.d.ts +36 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +135 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/storage.test.d.ts +2 -0
- package/dist/storage/storage.test.d.ts.map +1 -0
- package/dist/storage/storage.test.js +117 -0
- package/dist/storage/storage.test.js.map +1 -0
- package/dist/store/firebase.d.ts +13 -1
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +95 -18
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/firebase.test.js +12 -0
- package/dist/store/firebase.test.js.map +1 -1
- package/dist/store/index.d.ts +27 -2
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +49 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +12 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +128 -6
- package/dist/store/sqlite.js.map +1 -1
- package/dist/store/sqlite.test.js +47 -1
- package/dist/store/sqlite.test.js.map +1 -1
- package/dist/types/index.d.ts +52 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/domain/entities.ts +19 -0
- package/src/domain/events.ts +28 -3
- package/src/engine/index.ts +168 -3
- package/src/index.test.ts +32 -0
- package/src/index.ts +2 -0
- package/src/storage/file-storage.ts +96 -0
- package/src/storage/firebase-storage.ts +177 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/s3.ts +165 -0
- package/src/storage/storage.test.ts +146 -0
- package/src/store/firebase.test.ts +15 -0
- package/src/store/firebase.ts +97 -18
- package/src/store/index.ts +64 -0
- package/src/store/sqlite.test.ts +59 -1
- package/src/store/sqlite.ts +172 -6
- package/src/types/index.ts +63 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/store/firebase.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/store/index.ts
CHANGED
|
@@ -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) => {
|
package/src/store/sqlite.test.ts
CHANGED
|
@@ -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(
|
|
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 () => {
|
package/src/store/sqlite.ts
CHANGED
|
@@ -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
|
|
684
|
+
// --- Comments ---
|
|
664
685
|
async getComments(taskId: string): Promise<Comment[]> {
|
|
665
|
-
const stmt = this.db.prepare('SELECT * FROM comments WHERE taskId = ?');
|
|
666
|
-
|
|
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(
|
|
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[] = [];
|