@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.
- package/CHANGELOG.md +40 -0
- package/README.md +57 -21
- package/dist/domain/custom-fields.d.ts +9 -0
- package/dist/domain/custom-fields.d.ts.map +1 -0
- package/dist/domain/custom-fields.js +72 -0
- package/dist/domain/custom-fields.js.map +1 -0
- package/dist/domain/ddd.test.d.ts +2 -0
- package/dist/domain/ddd.test.d.ts.map +1 -0
- package/dist/domain/ddd.test.js +246 -0
- package/dist/domain/ddd.test.js.map +1 -0
- package/dist/domain/entities.d.ts +82 -0
- package/dist/domain/entities.d.ts.map +1 -0
- package/dist/domain/entities.js +321 -0
- package/dist/domain/entities.js.map +1 -0
- package/dist/domain/events.d.ts +153 -0
- package/dist/domain/events.d.ts.map +1 -0
- package/dist/domain/events.js +35 -0
- package/dist/domain/events.js.map +1 -0
- package/dist/domain/graph.d.ts +19 -0
- package/dist/domain/graph.d.ts.map +1 -0
- package/dist/domain/graph.js +58 -0
- package/dist/domain/graph.js.map +1 -0
- package/dist/domain/index.d.ts +5 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +5 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/engine/index.d.ts +32 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +416 -6
- package/dist/engine/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.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 +12 -1
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +63 -1
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/index.d.ts +49 -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 +57 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/domain/custom-fields.ts +131 -0
- package/src/domain/ddd.test.ts +307 -0
- package/src/domain/entities.ts +390 -0
- package/src/domain/events.ts +196 -0
- package/src/domain/graph.ts +70 -0
- package/src/domain/index.ts +4 -0
- package/src/engine/index.ts +518 -8
- 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.ts +64 -1
- package/src/store/index.ts +99 -11
- package/src/store/sqlite.test.ts +59 -1
- package/src/store/sqlite.ts +172 -6
- package/src/types/index.ts +70 -0
- package/tsconfig.tsbuildinfo +1 -1
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,
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
export interface DependencyRepository {
|
|
73
94
|
getDependencies(taskId: string): Promise<TaskDependency[]>;
|
|
74
95
|
addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency>;
|
|
96
|
+
}
|
|
75
97
|
|
|
76
|
-
|
|
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) => {
|
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[] = [];
|
package/src/types/index.ts
CHANGED
|
@@ -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?: {
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -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"}
|