@critical-path/core 0.1.2 → 0.2.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 +13 -0
- package/README.md +43 -26
- package/dist/store/firebase.d.ts +101 -0
- package/dist/store/firebase.d.ts.map +1 -0
- package/dist/store/firebase.js +237 -0
- package/dist/store/firebase.js.map +1 -0
- package/dist/store/firebase.test.d.ts +2 -0
- package/dist/store/firebase.test.d.ts.map +1 -0
- package/dist/store/firebase.test.js +42 -0
- package/dist/store/firebase.test.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +48 -0
- package/dist/store/sqlite.d.ts.map +1 -0
- package/dist/store/sqlite.js +354 -0
- package/dist/store/sqlite.js.map +1 -0
- package/dist/store/sqlite.test.d.ts +2 -0
- package/dist/store/sqlite.test.d.ts.map +1 -0
- package/dist/store/sqlite.test.js +63 -0
- package/dist/store/sqlite.test.js.map +1 -0
- package/package.json +5 -1
- package/src/store/firebase.test.ts +53 -0
- package/src/store/firebase.ts +289 -0
- package/src/store/index.ts +3 -1
- package/src/store/sqlite.test.ts +78 -0
- package/src/store/sqlite.ts +501 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { StorageAdapter } from './index.js';
|
|
2
|
+
import type {
|
|
3
|
+
Project,
|
|
4
|
+
Task,
|
|
5
|
+
Sprint,
|
|
6
|
+
Comment,
|
|
7
|
+
TimeEntry,
|
|
8
|
+
Activity,
|
|
9
|
+
Webhook,
|
|
10
|
+
TaskDependency
|
|
11
|
+
} from '../types/index.js';
|
|
12
|
+
|
|
13
|
+
export interface FirestoreDBInterface {
|
|
14
|
+
collection(name: string): {
|
|
15
|
+
doc(id?: string): {
|
|
16
|
+
id: string;
|
|
17
|
+
get(): Promise<{ exists: boolean; id: string; data(): any }>;
|
|
18
|
+
set(data: any, options?: { merge?: boolean }): Promise<void>;
|
|
19
|
+
delete(): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
get(): Promise<{ docs: Array<{ id: string; data(): any }> }>;
|
|
22
|
+
where(field: string, op: string, value: any): {
|
|
23
|
+
get(): Promise<{ docs: Array<{ id: string; data(): any }> }>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class InMemoryFirestoreMock implements FirestoreDBInterface {
|
|
29
|
+
private collections = new Map<string, Map<string, any>>();
|
|
30
|
+
|
|
31
|
+
private getCollectionMap(name: string) {
|
|
32
|
+
if (!this.collections.has(name)) {
|
|
33
|
+
this.collections.set(name, new Map());
|
|
34
|
+
}
|
|
35
|
+
return this.collections.get(name)!;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
collection(name: string) {
|
|
39
|
+
const colMap = this.getCollectionMap(name);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
doc(id?: string) {
|
|
43
|
+
const docId = id || `doc_${Math.random().toString(36).substring(2, 9)}`;
|
|
44
|
+
return {
|
|
45
|
+
id: docId,
|
|
46
|
+
async get() {
|
|
47
|
+
const data = colMap.get(docId);
|
|
48
|
+
return {
|
|
49
|
+
exists: !!data,
|
|
50
|
+
id: docId,
|
|
51
|
+
data: () => data
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
async set(data: any, options?: { merge?: boolean }) {
|
|
55
|
+
if (options?.merge && colMap.has(docId)) {
|
|
56
|
+
colMap.set(docId, { ...colMap.get(docId), ...data });
|
|
57
|
+
} else {
|
|
58
|
+
colMap.set(docId, data);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
async delete() {
|
|
62
|
+
colMap.delete(docId);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
async get() {
|
|
67
|
+
const docs = Array.from(colMap.entries()).map(([id, data]) => ({
|
|
68
|
+
id,
|
|
69
|
+
data: () => data
|
|
70
|
+
}));
|
|
71
|
+
return { docs };
|
|
72
|
+
},
|
|
73
|
+
where(field: string, op: string, value: any) {
|
|
74
|
+
return {
|
|
75
|
+
async get() {
|
|
76
|
+
const docs = Array.from(colMap.entries())
|
|
77
|
+
.filter(([_, data]) => {
|
|
78
|
+
if (op === '==') return data[field] === value;
|
|
79
|
+
return true;
|
|
80
|
+
})
|
|
81
|
+
.map(([id, data]) => ({
|
|
82
|
+
id,
|
|
83
|
+
data: () => data
|
|
84
|
+
}));
|
|
85
|
+
return { docs };
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface FirebaseStoreConfig {
|
|
94
|
+
/**
|
|
95
|
+
* Firestore instance or mockable DB interface.
|
|
96
|
+
*/
|
|
97
|
+
db?: FirestoreDBInterface;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class FirebaseStore implements StorageAdapter {
|
|
101
|
+
private db: FirestoreDBInterface;
|
|
102
|
+
|
|
103
|
+
constructor(config: FirebaseStoreConfig = {}) {
|
|
104
|
+
this.db = config.db || new InMemoryFirestoreMock();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Projects ---
|
|
108
|
+
async getProjects(): Promise<Project[]> {
|
|
109
|
+
const snap = await this.db.collection('projects').get();
|
|
110
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getProject(id: string): Promise<Project | null> {
|
|
114
|
+
const snap = await this.db.collection('projects').doc(id).get();
|
|
115
|
+
if (!snap.exists) return null;
|
|
116
|
+
return { ...snap.data(), id: snap.id };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
|
|
120
|
+
const docRef = this.db.collection('projects').doc();
|
|
121
|
+
const now = new Date().toISOString();
|
|
122
|
+
const newProj: Project = { ...project, id: docRef.id, createdAt: now, updatedAt: now };
|
|
123
|
+
await docRef.set(newProj);
|
|
124
|
+
return newProj;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async updateProject(id: string, updates: Partial<Project>): Promise<Project | null> {
|
|
128
|
+
const existing = await this.getProject(id);
|
|
129
|
+
if (!existing) return null;
|
|
130
|
+
|
|
131
|
+
const updated: Project = {
|
|
132
|
+
...existing,
|
|
133
|
+
...updates,
|
|
134
|
+
updatedAt: new Date().toISOString()
|
|
135
|
+
};
|
|
136
|
+
await this.db.collection('projects').doc(id).set(updated, { merge: true });
|
|
137
|
+
return updated;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async deleteProject(id: string): Promise<boolean> {
|
|
141
|
+
const existing = await this.getProject(id);
|
|
142
|
+
if (!existing) return false;
|
|
143
|
+
await this.db.collection('projects').doc(id).delete();
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Tasks ---
|
|
148
|
+
async getTasks(projectId?: string): Promise<Task[]> {
|
|
149
|
+
let snap;
|
|
150
|
+
if (projectId) {
|
|
151
|
+
snap = await this.db.collection('tasks').where('projectId', '==', projectId).get();
|
|
152
|
+
} else {
|
|
153
|
+
snap = await this.db.collection('tasks').get();
|
|
154
|
+
}
|
|
155
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getTask(id: string): Promise<Task | null> {
|
|
159
|
+
const snap = await this.db.collection('tasks').doc(id).get();
|
|
160
|
+
if (!snap.exists) return null;
|
|
161
|
+
return { ...snap.data(), id: snap.id };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
|
|
165
|
+
const docRef = this.db.collection('tasks').doc();
|
|
166
|
+
const now = new Date().toISOString();
|
|
167
|
+
const newTask: Task = { ...task, id: docRef.id, createdAt: now, updatedAt: now };
|
|
168
|
+
await docRef.set(newTask);
|
|
169
|
+
return newTask;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async updateTask(id: string, updates: Partial<Task>): Promise<Task | null> {
|
|
173
|
+
const existing = await this.getTask(id);
|
|
174
|
+
if (!existing) return null;
|
|
175
|
+
|
|
176
|
+
const updated: Task = {
|
|
177
|
+
...existing,
|
|
178
|
+
...updates,
|
|
179
|
+
updatedAt: new Date().toISOString()
|
|
180
|
+
};
|
|
181
|
+
await this.db.collection('tasks').doc(id).set(updated, { merge: true });
|
|
182
|
+
return updated;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async deleteTask(id: string): Promise<boolean> {
|
|
186
|
+
const existing = await this.getTask(id);
|
|
187
|
+
if (!existing) return false;
|
|
188
|
+
await this.db.collection('tasks').doc(id).delete();
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Sprints ---
|
|
193
|
+
async getSprints(projectId: string): Promise<Sprint[]> {
|
|
194
|
+
const snap = await this.db.collection('sprints').where('projectId', '==', projectId).get();
|
|
195
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async createSprint(sprint: Omit<Sprint, 'id' | 'createdAt'>): Promise<Sprint> {
|
|
199
|
+
const docRef = this.db.collection('sprints').doc();
|
|
200
|
+
const now = new Date().toISOString();
|
|
201
|
+
const newSprint: Sprint = { ...sprint, id: docRef.id, createdAt: now };
|
|
202
|
+
await docRef.set(newSprint);
|
|
203
|
+
return newSprint;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async updateSprint(id: string, updates: Partial<Sprint>): Promise<Sprint | null> {
|
|
207
|
+
const snap = await this.db.collection('sprints').doc(id).get();
|
|
208
|
+
if (!snap.exists) return null;
|
|
209
|
+
|
|
210
|
+
const updated: Sprint = { ...snap.data(), id, ...updates };
|
|
211
|
+
await this.db.collection('sprints').doc(id).set(updated, { merge: true });
|
|
212
|
+
return updated;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Comments & Activity ---
|
|
216
|
+
async getComments(taskId: string): Promise<Comment[]> {
|
|
217
|
+
const snap = await this.db.collection('comments').where('taskId', '==', taskId).get();
|
|
218
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
|
|
222
|
+
const docRef = this.db.collection('comments').doc();
|
|
223
|
+
const now = new Date().toISOString();
|
|
224
|
+
const newComment: Comment = { ...comment, id: docRef.id, createdAt: now, updatedAt: now };
|
|
225
|
+
await docRef.set(newComment);
|
|
226
|
+
return newComment;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
|
|
230
|
+
let snap;
|
|
231
|
+
if (filter?.taskId) {
|
|
232
|
+
snap = await this.db.collection('activities').where('taskId', '==', filter.taskId).get();
|
|
233
|
+
} else if (filter?.projectId) {
|
|
234
|
+
snap = await this.db.collection('activities').where('projectId', '==', filter.projectId).get();
|
|
235
|
+
} else {
|
|
236
|
+
snap = await this.db.collection('activities').get();
|
|
237
|
+
}
|
|
238
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async logActivity(activity: Omit<Activity, 'id' | 'createdAt'>): Promise<Activity> {
|
|
242
|
+
const docRef = this.db.collection('activities').doc();
|
|
243
|
+
const now = new Date().toISOString();
|
|
244
|
+
const newAct: Activity = { ...activity, id: docRef.id, createdAt: now };
|
|
245
|
+
await docRef.set(newAct);
|
|
246
|
+
return newAct;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- Time Tracking ---
|
|
250
|
+
async getTimeEntries(taskId: string): Promise<TimeEntry[]> {
|
|
251
|
+
const snap = await this.db.collection('time_entries').where('taskId', '==', taskId).get();
|
|
252
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt'>): Promise<TimeEntry> {
|
|
256
|
+
const docRef = this.db.collection('time_entries').doc();
|
|
257
|
+
const now = new Date().toISOString();
|
|
258
|
+
const newEntry: TimeEntry = { ...entry, id: docRef.id, loggedAt: now };
|
|
259
|
+
await docRef.set(newEntry);
|
|
260
|
+
return newEntry;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- Dependencies ---
|
|
264
|
+
async getDependencies(taskId: string): Promise<TaskDependency[]> {
|
|
265
|
+
const snap = await this.db.collection('dependencies').where('taskId', '==', taskId).get();
|
|
266
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency> {
|
|
270
|
+
const docRef = this.db.collection('dependencies').doc();
|
|
271
|
+
const newDep: TaskDependency = { ...dep, id: docRef.id };
|
|
272
|
+
await docRef.set(newDep);
|
|
273
|
+
return newDep;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- Webhooks ---
|
|
277
|
+
async getWebhooks(): Promise<Webhook[]> {
|
|
278
|
+
const snap = await this.db.collection('webhooks').get();
|
|
279
|
+
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook> {
|
|
283
|
+
const docRef = this.db.collection('webhooks').doc();
|
|
284
|
+
const now = new Date().toISOString();
|
|
285
|
+
const newWh: Webhook = { ...webhook, id: docRef.id, createdAt: now };
|
|
286
|
+
await docRef.set(newWh);
|
|
287
|
+
return newWh;
|
|
288
|
+
}
|
|
289
|
+
}
|
package/src/store/index.ts
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { SQLiteStore } from './sqlite.js';
|
|
3
|
+
|
|
4
|
+
describe('SQLiteStore', () => {
|
|
5
|
+
let store: SQLiteStore;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
store = new SQLiteStore({ filename: ':memory:' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should create and retrieve projects', async () => {
|
|
12
|
+
const project = await store.createProject({
|
|
13
|
+
key: 'SQLITE',
|
|
14
|
+
name: 'SQLite Test Project',
|
|
15
|
+
description: 'Testing SQLite adapter'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(project.id).toMatch(/^proj_/);
|
|
19
|
+
expect(project.name).toBe('SQLite Test Project');
|
|
20
|
+
|
|
21
|
+
const fetched = await store.getProject(project.id);
|
|
22
|
+
expect(fetched).not.toBeNull();
|
|
23
|
+
expect(fetched?.key).toBe('SQLITE');
|
|
24
|
+
|
|
25
|
+
const all = await store.getProjects();
|
|
26
|
+
expect(all).toHaveLength(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should create, update, filter, and delete tasks', async () => {
|
|
30
|
+
const project = await store.createProject({ key: 'P1', name: 'Proj 1' });
|
|
31
|
+
const task = await store.createTask({
|
|
32
|
+
projectId: project.id,
|
|
33
|
+
title: 'SQLite Task 1',
|
|
34
|
+
status: 'todo',
|
|
35
|
+
priority: 'high',
|
|
36
|
+
tags: ['sqlite', 'db']
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(task.id).toMatch(/^task_/);
|
|
40
|
+
expect(task.priority).toBe('high');
|
|
41
|
+
expect(task.tags).toContain('sqlite');
|
|
42
|
+
|
|
43
|
+
const updated = await store.updateTask(task.id, { status: 'in_progress' });
|
|
44
|
+
expect(updated?.status).toBe('in_progress');
|
|
45
|
+
|
|
46
|
+
const projectTasks = await store.getTasks(project.id);
|
|
47
|
+
expect(projectTasks).toHaveLength(1);
|
|
48
|
+
|
|
49
|
+
const deleted = await store.deleteTask(task.id);
|
|
50
|
+
expect(deleted).toBe(true);
|
|
51
|
+
|
|
52
|
+
const remaining = await store.getTasks(project.id);
|
|
53
|
+
expect(remaining).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle comments, activities, and time tracking', async () => {
|
|
57
|
+
const comment = await store.addComment({
|
|
58
|
+
taskId: 'task_1',
|
|
59
|
+
authorId: 'user_1',
|
|
60
|
+
content: 'Great work!'
|
|
61
|
+
});
|
|
62
|
+
expect(comment.id).toMatch(/^cmt_/);
|
|
63
|
+
|
|
64
|
+
const comments = await store.getComments('task_1');
|
|
65
|
+
expect(comments).toHaveLength(1);
|
|
66
|
+
|
|
67
|
+
const timeEntry = await store.logTime({
|
|
68
|
+
taskId: 'task_1',
|
|
69
|
+
userId: 'user_1',
|
|
70
|
+
hours: 2.5,
|
|
71
|
+
description: 'Code review'
|
|
72
|
+
});
|
|
73
|
+
expect(timeEntry.hours).toBe(2.5);
|
|
74
|
+
|
|
75
|
+
const entries = await store.getTimeEntries('task_1');
|
|
76
|
+
expect(entries).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
});
|