@critical-path/core 0.36.0 → 0.36.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.
- package/package.json +5 -1
- package/CHANGELOG.md +0 -593
- package/dist/deliverables.test.d.ts +0 -2
- package/dist/deliverables.test.d.ts.map +0 -1
- package/dist/deliverables.test.js +0 -232
- package/dist/deliverables.test.js.map +0 -1
- package/dist/domain/calendar.test.d.ts +0 -2
- package/dist/domain/calendar.test.d.ts.map +0 -1
- package/dist/domain/calendar.test.js +0 -181
- package/dist/domain/calendar.test.js.map +0 -1
- package/dist/domain/cpm.test.d.ts +0 -2
- package/dist/domain/cpm.test.d.ts.map +0 -1
- package/dist/domain/cpm.test.js +0 -166
- package/dist/domain/cpm.test.js.map +0 -1
- package/dist/domain/ddd.test.d.ts +0 -2
- package/dist/domain/ddd.test.d.ts.map +0 -1
- package/dist/domain/ddd.test.js +0 -349
- package/dist/domain/ddd.test.js.map +0 -1
- package/dist/domain/ladder.test.d.ts +0 -2
- package/dist/domain/ladder.test.d.ts.map +0 -1
- package/dist/domain/ladder.test.js +0 -213
- package/dist/domain/ladder.test.js.map +0 -1
- package/dist/domain/metrics.test.d.ts +0 -2
- package/dist/domain/metrics.test.d.ts.map +0 -1
- package/dist/domain/metrics.test.js +0 -252
- package/dist/domain/metrics.test.js.map +0 -1
- package/dist/domain/task-duration.test.d.ts +0 -2
- package/dist/domain/task-duration.test.d.ts.map +0 -1
- package/dist/domain/task-duration.test.js +0 -307
- package/dist/domain/task-duration.test.js.map +0 -1
- package/dist/domain/workload.test.d.ts +0 -2
- package/dist/domain/workload.test.d.ts.map +0 -1
- package/dist/domain/workload.test.js +0 -261
- package/dist/domain/workload.test.js.map +0 -1
- package/dist/engine/actor.test.d.ts +0 -2
- package/dist/engine/actor.test.d.ts.map +0 -1
- package/dist/engine/actor.test.js +0 -54
- package/dist/engine/actor.test.js.map +0 -1
- package/dist/engine/authorization.test.d.ts +0 -2
- package/dist/engine/authorization.test.d.ts.map +0 -1
- package/dist/engine/authorization.test.js +0 -154
- package/dist/engine/authorization.test.js.map +0 -1
- package/dist/engine/cascade.test.d.ts +0 -2
- package/dist/engine/cascade.test.d.ts.map +0 -1
- package/dist/engine/cascade.test.js +0 -99
- package/dist/engine/cascade.test.js.map +0 -1
- package/dist/engine/config.test.d.ts +0 -2
- package/dist/engine/config.test.d.ts.map +0 -1
- package/dist/engine/config.test.js +0 -46
- package/dist/engine/config.test.js.map +0 -1
- package/dist/engine/entity-events.test.d.ts +0 -2
- package/dist/engine/entity-events.test.d.ts.map +0 -1
- package/dist/engine/entity-events.test.js +0 -61
- package/dist/engine/entity-events.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -429
- package/dist/index.test.js.map +0 -1
- package/dist/plugins/plugins.test.d.ts +0 -2
- package/dist/plugins/plugins.test.d.ts.map +0 -1
- package/dist/plugins/plugins.test.js +0 -137
- package/dist/plugins/plugins.test.js.map +0 -1
- package/dist/schemas/schemas.test.d.ts +0 -2
- package/dist/schemas/schemas.test.d.ts.map +0 -1
- package/dist/schemas/schemas.test.js +0 -58
- package/dist/schemas/schemas.test.js.map +0 -1
- package/dist/status.test.d.ts +0 -2
- package/dist/status.test.d.ts.map +0 -1
- package/dist/status.test.js +0 -295
- package/dist/status.test.js.map +0 -1
- package/dist/storage/storage.test.d.ts +0 -2
- package/dist/storage/storage.test.d.ts.map +0 -1
- package/dist/storage/storage.test.js +0 -236
- package/dist/storage/storage.test.js.map +0 -1
- package/dist/storage/upload-limits.test.d.ts +0 -2
- package/dist/storage/upload-limits.test.d.ts.map +0 -1
- package/dist/storage/upload-limits.test.js +0 -31
- package/dist/storage/upload-limits.test.js.map +0 -1
- package/dist/store/conformance.test.d.ts +0 -2
- package/dist/store/conformance.test.d.ts.map +0 -1
- package/dist/store/conformance.test.js +0 -15
- package/dist/store/conformance.test.js.map +0 -1
- package/dist/store/firebase.test.d.ts +0 -2
- package/dist/store/firebase.test.d.ts.map +0 -1
- package/dist/store/firebase.test.js +0 -103
- package/dist/store/firebase.test.js.map +0 -1
- package/dist/store/query.test.d.ts +0 -2
- package/dist/store/query.test.d.ts.map +0 -1
- package/dist/store/query.test.js +0 -89
- package/dist/store/query.test.js.map +0 -1
- package/dist/store/sqlite.test.d.ts +0 -2
- package/dist/store/sqlite.test.d.ts.map +0 -1
- package/dist/store/sqlite.test.js +0 -177
- package/dist/store/sqlite.test.js.map +0 -1
- package/dist/task-state.test.d.ts +0 -2
- package/dist/task-state.test.d.ts.map +0 -1
- package/dist/task-state.test.js +0 -423
- package/dist/task-state.test.js.map +0 -1
- package/dist/use-case-validation.test.d.ts +0 -2
- package/dist/use-case-validation.test.d.ts.map +0 -1
- package/dist/use-case-validation.test.js +0 -281
- package/dist/use-case-validation.test.js.map +0 -1
- package/dist/utils/fractional-index.test.d.ts +0 -2
- package/dist/utils/fractional-index.test.d.ts.map +0 -1
- package/dist/utils/fractional-index.test.js +0 -182
- package/dist/utils/fractional-index.test.js.map +0 -1
- package/dist/utils/key.test.d.ts +0 -2
- package/dist/utils/key.test.d.ts.map +0 -1
- package/dist/utils/key.test.js +0 -65
- package/dist/utils/key.test.js.map +0 -1
- package/dist/utils/mentions.test.d.ts +0 -2
- package/dist/utils/mentions.test.d.ts.map +0 -1
- package/dist/utils/mentions.test.js +0 -114
- package/dist/utils/mentions.test.js.map +0 -1
- package/dist/webhooks/webhooks.test.d.ts +0 -2
- package/dist/webhooks/webhooks.test.d.ts.map +0 -1
- package/dist/webhooks/webhooks.test.js +0 -154
- package/dist/webhooks/webhooks.test.js.map +0 -1
- package/src/deliverables.test.ts +0 -284
- package/src/domain/authorization.ts +0 -82
- package/src/domain/calendar.test.ts +0 -216
- package/src/domain/calendar.ts +0 -502
- package/src/domain/cpm.test.ts +0 -189
- package/src/domain/cpm.ts +0 -258
- package/src/domain/custom-fields.ts +0 -172
- package/src/domain/ddd.test.ts +0 -442
- package/src/domain/entities.ts +0 -798
- package/src/domain/errors.ts +0 -38
- package/src/domain/events.ts +0 -369
- package/src/domain/graph.ts +0 -70
- package/src/domain/index.ts +0 -12
- package/src/domain/ladder.test.ts +0 -251
- package/src/domain/ladder.ts +0 -474
- package/src/domain/metrics.test.ts +0 -288
- package/src/domain/metrics.ts +0 -497
- package/src/domain/task-duration.test.ts +0 -354
- package/src/domain/workload.test.ts +0 -311
- package/src/domain/workload.ts +0 -457
- package/src/engine/actor.test.ts +0 -63
- package/src/engine/authorization.test.ts +0 -199
- package/src/engine/cascade.test.ts +0 -121
- package/src/engine/config.test.ts +0 -51
- package/src/engine/entity-events.test.ts +0 -67
- package/src/engine/index.ts +0 -2495
- package/src/index.test.ts +0 -514
- package/src/index.ts +0 -9
- package/src/plugins/index.ts +0 -103
- package/src/plugins/plugins.test.ts +0 -161
- package/src/schemas/index.ts +0 -402
- package/src/schemas/schemas.test.ts +0 -116
- package/src/status.test.ts +0 -354
- package/src/storage/file-storage.ts +0 -200
- package/src/storage/firebase-storage.ts +0 -209
- package/src/storage/index.ts +0 -3
- package/src/storage/s3.ts +0 -151
- package/src/storage/storage.test.ts +0 -274
- package/src/storage/upload-limits.test.ts +0 -41
- package/src/store/conformance.test.ts +0 -15
- package/src/store/firebase.test.ts +0 -131
- package/src/store/firebase.ts +0 -658
- package/src/store/index.ts +0 -623
- package/src/store/query.test.ts +0 -110
- package/src/store/query.ts +0 -113
- package/src/store/sqlite.test.ts +0 -218
- package/src/store/sqlite.ts +0 -1480
- package/src/task-state.test.ts +0 -506
- package/src/testing/conformance.ts +0 -319
- package/src/testing/index.ts +0 -1
- package/src/types/index.ts +0 -871
- package/src/use-case-validation.test.ts +0 -324
- package/src/utils/fractional-index.test.ts +0 -228
- package/src/utils/fractional-index.ts +0 -254
- package/src/utils/index.ts +0 -7
- package/src/utils/key.test.ts +0 -76
- package/src/utils/key.ts +0 -62
- package/src/utils/mentions.test.ts +0 -132
- package/src/utils/mentions.ts +0 -131
- package/src/utils/status.ts +0 -419
- package/src/utils/workflow.ts +0 -306
- package/src/webhooks/dispatcher.ts +0 -190
- package/src/webhooks/index.ts +0 -2
- package/src/webhooks/signature.ts +0 -57
- package/src/webhooks/webhooks.test.ts +0 -201
- package/tsconfig.json +0 -8
- package/tsconfig.tsbuildinfo +0 -1
package/src/store/firebase.ts
DELETED
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
import type { StorageAdapter } from './index.js';
|
|
2
|
-
import { matchesActivityQuery, matchesTaskQuery, paginate, type ActivityQuery, type Page, type TaskQuery } from './query.js';
|
|
3
|
-
import type {
|
|
4
|
-
Project,
|
|
5
|
-
Task,
|
|
6
|
-
Iteration,
|
|
7
|
-
Team,
|
|
8
|
-
TaskContainer,
|
|
9
|
-
Comment,
|
|
10
|
-
Attachment,
|
|
11
|
-
TimeEntry,
|
|
12
|
-
Activity,
|
|
13
|
-
Webhook,
|
|
14
|
-
TaskDependency,
|
|
15
|
-
Workflow,
|
|
16
|
-
Deliverable,
|
|
17
|
-
CreateDeliverableInput
|
|
18
|
-
} from '../types/index.js';
|
|
19
|
-
import { generateProjectKey } from '../utils/key.js';
|
|
20
|
-
|
|
21
|
-
export interface FirestoreDBInterface {
|
|
22
|
-
collection(name: string): {
|
|
23
|
-
doc(id?: string): {
|
|
24
|
-
id: string;
|
|
25
|
-
get(): Promise<{ exists: boolean; id: string; data(): any }>;
|
|
26
|
-
set(data: any, options?: { merge?: boolean }): Promise<void>;
|
|
27
|
-
delete(): Promise<void>;
|
|
28
|
-
};
|
|
29
|
-
get(): Promise<{ docs: Array<{ id: string; data(): any }> }>;
|
|
30
|
-
where(field: string, op: string, value: any): {
|
|
31
|
-
get(): Promise<{ docs: Array<{ id: string; data(): any }> }>;
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class InMemoryFirestoreMock implements FirestoreDBInterface {
|
|
37
|
-
private collections = new Map<string, Map<string, any>>();
|
|
38
|
-
|
|
39
|
-
private getCollectionMap(name: string) {
|
|
40
|
-
if (!this.collections.has(name)) {
|
|
41
|
-
this.collections.set(name, new Map());
|
|
42
|
-
}
|
|
43
|
-
return this.collections.get(name)!;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
collection(name: string) {
|
|
47
|
-
const colMap = this.getCollectionMap(name);
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
doc(id?: string) {
|
|
51
|
-
const docId = id || `doc_${Math.random().toString(36).substring(2, 9)}`;
|
|
52
|
-
return {
|
|
53
|
-
id: docId,
|
|
54
|
-
async get() {
|
|
55
|
-
const data = colMap.get(docId);
|
|
56
|
-
return {
|
|
57
|
-
exists: !!data,
|
|
58
|
-
id: docId,
|
|
59
|
-
data: () => structuredClone(data)
|
|
60
|
-
};
|
|
61
|
-
},
|
|
62
|
-
async set(data: any, options?: { merge?: boolean }) {
|
|
63
|
-
if (options?.merge && colMap.has(docId)) {
|
|
64
|
-
colMap.set(docId, structuredClone({ ...colMap.get(docId), ...data }));
|
|
65
|
-
} else {
|
|
66
|
-
colMap.set(docId, structuredClone(data));
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
async delete() {
|
|
70
|
-
colMap.delete(docId);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
},
|
|
74
|
-
async get() {
|
|
75
|
-
const docs = Array.from(colMap.entries()).map(([id, data]) => ({
|
|
76
|
-
id,
|
|
77
|
-
data: () => structuredClone(data)
|
|
78
|
-
}));
|
|
79
|
-
return { docs };
|
|
80
|
-
},
|
|
81
|
-
where(field: string, op: string, value: any) {
|
|
82
|
-
return {
|
|
83
|
-
async get() {
|
|
84
|
-
const docs = Array.from(colMap.entries())
|
|
85
|
-
.filter(([_, data]) => {
|
|
86
|
-
if (op === '==') return data[field] === value;
|
|
87
|
-
return true;
|
|
88
|
-
})
|
|
89
|
-
.map(([id, data]) => ({
|
|
90
|
-
id,
|
|
91
|
-
data: () => structuredClone(data)
|
|
92
|
-
}));
|
|
93
|
-
return { docs };
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface FirebaseStoreConfig {
|
|
102
|
-
/**
|
|
103
|
-
* Firestore instance or mockable DB interface. Required.
|
|
104
|
-
*/
|
|
105
|
-
db: FirestoreDBInterface;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Removes `undefined` values, which Firestore rejects. Update methods write the complete merged
|
|
110
|
-
* record without `{ merge: true }`, so a field cleared with `undefined` is actually removed.
|
|
111
|
-
*/
|
|
112
|
-
export function sanitizeFirestoreData<T>(obj: T): T {
|
|
113
|
-
if (obj === null || typeof obj !== 'object') {
|
|
114
|
-
return obj;
|
|
115
|
-
}
|
|
116
|
-
if (Array.isArray(obj)) {
|
|
117
|
-
return obj.map(sanitizeFirestoreData) as unknown as T;
|
|
118
|
-
}
|
|
119
|
-
const clean: Record<string, any> = {};
|
|
120
|
-
for (const [key, value] of Object.entries(obj as Record<string, any>)) {
|
|
121
|
-
if (value !== undefined) {
|
|
122
|
-
clean[key] = sanitizeFirestoreData(value);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return clean as T;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export class FirebaseStore implements StorageAdapter {
|
|
129
|
-
private db: FirestoreDBInterface;
|
|
130
|
-
|
|
131
|
-
constructor(config: FirebaseStoreConfig) {
|
|
132
|
-
if (!config?.db) {
|
|
133
|
-
throw new Error(
|
|
134
|
-
'FirebaseStore requires a valid Firestore db instance (e.g. FirebaseStoreConfig.db). Automatic fallback to InMemoryFirestoreMock has been removed.'
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
this.db = config.db;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// --- Projects ---
|
|
141
|
-
async getProjects(): Promise<Project[]> {
|
|
142
|
-
const snap = await this.db.collection('projects').get();
|
|
143
|
-
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async getProject(id: string): Promise<Project | null> {
|
|
147
|
-
const snap = await this.db.collection('projects').doc(id).get();
|
|
148
|
-
if (!snap.exists) return null;
|
|
149
|
-
return { ...snap.data(), id: snap.id };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
|
|
153
|
-
const docRef = this.db.collection('projects').doc();
|
|
154
|
-
const now = new Date().toISOString();
|
|
155
|
-
const key = project.key || generateProjectKey(project.name);
|
|
156
|
-
const newProj: Project = { ...project, key, id: docRef.id, createdAt: now, updatedAt: now };
|
|
157
|
-
await docRef.set(sanitizeFirestoreData(newProj));
|
|
158
|
-
return newProj;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async updateProject(id: string, updates: Partial<Project>): Promise<Project | null> {
|
|
162
|
-
const existing = await this.getProject(id);
|
|
163
|
-
if (!existing) return null;
|
|
164
|
-
|
|
165
|
-
const updated: Project = {
|
|
166
|
-
...existing,
|
|
167
|
-
...updates,
|
|
168
|
-
updatedAt: new Date().toISOString()
|
|
169
|
-
};
|
|
170
|
-
await this.db.collection('projects').doc(id).set(sanitizeFirestoreData(updated));
|
|
171
|
-
return updated;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async deleteProject(id: string): Promise<boolean> {
|
|
175
|
-
const existing = await this.getProject(id);
|
|
176
|
-
if (!existing) return false;
|
|
177
|
-
await this.db.collection('projects').doc(id).delete();
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// --- Workflows ---
|
|
182
|
-
async getWorkflows(): Promise<Workflow[]> {
|
|
183
|
-
const snap = await this.db.collection('workflows').get();
|
|
184
|
-
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async getWorkflow(id: string): Promise<Workflow | null> {
|
|
188
|
-
const snap = await this.db.collection('workflows').doc(id).get();
|
|
189
|
-
if (!snap.exists) return null;
|
|
190
|
-
return { ...snap.data(), id: snap.id };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
|
|
194
|
-
const docRef = this.db.collection('workflows').doc();
|
|
195
|
-
const now = new Date().toISOString();
|
|
196
|
-
const newWf: Workflow = { ...workflow, id: docRef.id, createdAt: now, updatedAt: now };
|
|
197
|
-
await docRef.set(sanitizeFirestoreData(newWf));
|
|
198
|
-
return newWf;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null> {
|
|
202
|
-
const existing = await this.getWorkflow(id);
|
|
203
|
-
if (!existing) return null;
|
|
204
|
-
|
|
205
|
-
const updated: Workflow = {
|
|
206
|
-
...existing,
|
|
207
|
-
...updates,
|
|
208
|
-
updatedAt: new Date().toISOString()
|
|
209
|
-
};
|
|
210
|
-
await this.db.collection('workflows').doc(id).set(sanitizeFirestoreData(updated));
|
|
211
|
-
return updated;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async deleteWorkflow(id: string): Promise<boolean> {
|
|
215
|
-
const existing = await this.getWorkflow(id);
|
|
216
|
-
if (!existing) return false;
|
|
217
|
-
await this.db.collection('workflows').doc(id).delete();
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// --- Teams ---
|
|
222
|
-
async getTeams(): Promise<Team[]> {
|
|
223
|
-
const snap = await this.db.collection('teams').get();
|
|
224
|
-
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async getTeam(id: string): Promise<Team | null> {
|
|
228
|
-
const snap = await this.db.collection('teams').doc(id).get();
|
|
229
|
-
if (!snap.exists) return null;
|
|
230
|
-
return { ...snap.data(), id: snap.id };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team> {
|
|
234
|
-
const docRef = this.db.collection('teams').doc();
|
|
235
|
-
const now = new Date().toISOString();
|
|
236
|
-
const newTeam: Team = { ...team, id: docRef.id, createdAt: now, updatedAt: now };
|
|
237
|
-
await docRef.set(sanitizeFirestoreData(newTeam));
|
|
238
|
-
return newTeam;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async updateTeam(id: string, updates: Partial<Team>): Promise<Team | null> {
|
|
242
|
-
const existing = await this.getTeam(id);
|
|
243
|
-
if (!existing) return null;
|
|
244
|
-
|
|
245
|
-
const updated: Team = { ...existing, ...updates, updatedAt: new Date().toISOString() };
|
|
246
|
-
await this.db.collection('teams').doc(id).set(sanitizeFirestoreData(updated));
|
|
247
|
-
return updated;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async deleteTeam(id: string): Promise<boolean> {
|
|
251
|
-
const existing = await this.getTeam(id);
|
|
252
|
-
if (!existing) return false;
|
|
253
|
-
await this.db.collection('teams').doc(id).delete();
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// --- Containers ---
|
|
258
|
-
async getContainers(projectId: string): Promise<TaskContainer[]> {
|
|
259
|
-
const snap = await this.db.collection('containers').where('projectId', '==', projectId).get();
|
|
260
|
-
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async getContainer(id: string): Promise<TaskContainer | null> {
|
|
264
|
-
const snap = await this.db.collection('containers').doc(id).get();
|
|
265
|
-
if (!snap.exists) return null;
|
|
266
|
-
return { ...snap.data(), id: snap.id };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async createContainer(container: Omit<TaskContainer, 'id' | 'createdAt' | 'updatedAt'>): Promise<TaskContainer> {
|
|
270
|
-
const docRef = this.db.collection('containers').doc();
|
|
271
|
-
const now = new Date().toISOString();
|
|
272
|
-
const newContainer: TaskContainer = { ...container, id: docRef.id, createdAt: now, updatedAt: now };
|
|
273
|
-
await docRef.set(sanitizeFirestoreData(newContainer));
|
|
274
|
-
return newContainer;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async updateContainer(id: string, updates: Partial<TaskContainer>): Promise<TaskContainer | null> {
|
|
278
|
-
const existing = await this.getContainer(id);
|
|
279
|
-
if (!existing) return null;
|
|
280
|
-
|
|
281
|
-
const updated: TaskContainer = { ...existing, ...updates, updatedAt: new Date().toISOString() };
|
|
282
|
-
await this.db.collection('containers').doc(id).set(sanitizeFirestoreData(updated));
|
|
283
|
-
return updated;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async deleteContainer(id: string): Promise<boolean> {
|
|
287
|
-
const existing = await this.getContainer(id);
|
|
288
|
-
if (!existing) return false;
|
|
289
|
-
await this.db.collection('containers').doc(id).delete();
|
|
290
|
-
return true;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// --- Deliverables ---
|
|
294
|
-
async getDeliverables(projectId: string): Promise<Deliverable[]> {
|
|
295
|
-
const snap = await this.db.collection('deliverables').where('projectId', '==', projectId).get();
|
|
296
|
-
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
async getDeliverable(id: string): Promise<Deliverable | null> {
|
|
300
|
-
const snap = await this.db.collection('deliverables').doc(id).get();
|
|
301
|
-
if (!snap.exists) return null;
|
|
302
|
-
return { ...snap.data(), id: snap.id };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable> {
|
|
306
|
-
const docRef = this.db.collection('deliverables').doc();
|
|
307
|
-
const now = new Date().toISOString();
|
|
308
|
-
const newDeliverable: Deliverable = {
|
|
309
|
-
...deliverable,
|
|
310
|
-
id: docRef.id,
|
|
311
|
-
status: deliverable.status || 'planned',
|
|
312
|
-
outputUrls: deliverable.outputUrls ? [...deliverable.outputUrls] : [],
|
|
313
|
-
customFields: deliverable.customFields ? { ...deliverable.customFields } : {},
|
|
314
|
-
createdAt: now,
|
|
315
|
-
updatedAt: now
|
|
316
|
-
};
|
|
317
|
-
await docRef.set(sanitizeFirestoreData(newDeliverable));
|
|
318
|
-
return newDeliverable;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
async updateDeliverable(id: string, updates: Partial<Deliverable>): Promise<Deliverable | null> {
|
|
322
|
-
const existing = await this.getDeliverable(id);
|
|
323
|
-
if (!existing) return null;
|
|
324
|
-
|
|
325
|
-
const updated: Deliverable = { ...existing, ...updates, updatedAt: new Date().toISOString() };
|
|
326
|
-
await this.db.collection('deliverables').doc(id).set(sanitizeFirestoreData(updated));
|
|
327
|
-
return updated;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async deleteDeliverable(id: string): Promise<boolean> {
|
|
331
|
-
const existing = await this.getDeliverable(id);
|
|
332
|
-
if (!existing) return false;
|
|
333
|
-
await this.db.collection('deliverables').doc(id).delete();
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// --- Tasks ---
|
|
338
|
-
async getTasks(projectId?: string): Promise<Task[]> {
|
|
339
|
-
let snap;
|
|
340
|
-
if (projectId) {
|
|
341
|
-
snap = await this.db.collection('tasks').where('projectId', '==', projectId).get();
|
|
342
|
-
} else {
|
|
343
|
-
snap = await this.db.collection('tasks').get();
|
|
344
|
-
}
|
|
345
|
-
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/** Pushes down the project filter; remaining filters and pagination are applied in memory. */
|
|
349
|
-
async queryTasks(query: TaskQuery): Promise<Page<Task>> {
|
|
350
|
-
const tasks = (await this.getTasks(query.projectId)).filter((t) => matchesTaskQuery(t, query));
|
|
351
|
-
return paginate(tasks, (t) => t.createdAt, 'asc', query.limit, query.cursor);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
async getTask(id: string): Promise<Task | null> {
|
|
355
|
-
const snap = await this.db.collection('tasks').doc(id).get();
|
|
356
|
-
if (!snap.exists) return null;
|
|
357
|
-
return { ...snap.data(), id: snap.id };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
|
|
361
|
-
const docRef = this.db.collection('tasks').doc();
|
|
362
|
-
const now = new Date().toISOString();
|
|
363
|
-
const newTask: Task = { ...task, id: docRef.id, createdAt: now, updatedAt: now };
|
|
364
|
-
await docRef.set(sanitizeFirestoreData(newTask));
|
|
365
|
-
return newTask;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async updateTask(id: string, updates: Partial<Task>): Promise<Task | null> {
|
|
369
|
-
const existing = await this.getTask(id);
|
|
370
|
-
if (!existing) return null;
|
|
371
|
-
|
|
372
|
-
const updated: Task = {
|
|
373
|
-
...existing,
|
|
374
|
-
...updates,
|
|
375
|
-
updatedAt: new Date().toISOString()
|
|
376
|
-
};
|
|
377
|
-
await this.db.collection('tasks').doc(id).set(sanitizeFirestoreData(updated));
|
|
378
|
-
return updated;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async deleteTask(id: string): Promise<boolean> {
|
|
382
|
-
const existing = await this.getTask(id);
|
|
383
|
-
if (!existing) return false;
|
|
384
|
-
await this.db.collection('tasks').doc(id).delete();
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// --- Iterations ---
|
|
389
|
-
async getIterations(projectId: string): Promise<Iteration[]> {
|
|
390
|
-
const snap = await this.db.collection('iterations').where('projectId', '==', projectId).get();
|
|
391
|
-
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
async getIteration(id: string): Promise<Iteration | null> {
|
|
395
|
-
const snap = await this.db.collection('iterations').doc(id).get();
|
|
396
|
-
if (!snap.exists) return null;
|
|
397
|
-
return { ...snap.data(), id: snap.id };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
async createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration> {
|
|
401
|
-
const docRef = this.db.collection('iterations').doc();
|
|
402
|
-
const now = new Date().toISOString();
|
|
403
|
-
const newIteration: Iteration = { ...iteration, id: docRef.id, createdAt: now };
|
|
404
|
-
await docRef.set(sanitizeFirestoreData(newIteration));
|
|
405
|
-
return newIteration;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
async updateIteration(id: string, updates: Partial<Iteration>): Promise<Iteration | null> {
|
|
409
|
-
const snap = await this.db.collection('iterations').doc(id).get();
|
|
410
|
-
if (!snap.exists) return null;
|
|
411
|
-
|
|
412
|
-
const updated: Iteration = { ...snap.data(), id, ...updates };
|
|
413
|
-
await this.db.collection('iterations').doc(id).set(sanitizeFirestoreData(updated));
|
|
414
|
-
return updated;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async deleteIteration(id: string): Promise<boolean> {
|
|
418
|
-
const existing = await this.getIteration(id);
|
|
419
|
-
if (!existing) return false;
|
|
420
|
-
await this.db.collection('iterations').doc(id).delete();
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// --- Comments ---
|
|
425
|
-
async getComments(taskId: string): Promise<Comment[]> {
|
|
426
|
-
const snap = await this.db.collection('comments').where('taskId', '==', taskId).get();
|
|
427
|
-
return snap.docs
|
|
428
|
-
.map((doc) => ({ ...doc.data(), id: doc.id }) as Comment)
|
|
429
|
-
.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async getComment(id: string): Promise<Comment | null> {
|
|
433
|
-
const snap = await this.db.collection('comments').doc(id).get();
|
|
434
|
-
if (!snap.exists) return null;
|
|
435
|
-
return { ...snap.data(), id: snap.id };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
|
|
439
|
-
const docRef = this.db.collection('comments').doc();
|
|
440
|
-
const now = new Date().toISOString();
|
|
441
|
-
const newComment: Comment = { ...comment, id: docRef.id, createdAt: now, updatedAt: now };
|
|
442
|
-
await docRef.set(sanitizeFirestoreData(newComment));
|
|
443
|
-
return newComment;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async updateComment(id: string, updates: Partial<Comment>): Promise<Comment | null> {
|
|
447
|
-
const existing = await this.getComment(id);
|
|
448
|
-
if (!existing) return null;
|
|
449
|
-
|
|
450
|
-
const updated: Comment = {
|
|
451
|
-
...existing,
|
|
452
|
-
...updates,
|
|
453
|
-
updatedAt: new Date().toISOString()
|
|
454
|
-
};
|
|
455
|
-
await this.db.collection('comments').doc(id).set(sanitizeFirestoreData(updated));
|
|
456
|
-
return updated;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async deleteComment(id: string): Promise<boolean> {
|
|
460
|
-
const existing = await this.getComment(id);
|
|
461
|
-
if (!existing) return false;
|
|
462
|
-
await this.db.collection('comments').doc(id).delete();
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
async addReaction(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null> {
|
|
467
|
-
const existing = await this.getComment(commentId);
|
|
468
|
-
if (!existing) return null;
|
|
469
|
-
const reactions = existing.reactions ? [...existing.reactions] : [];
|
|
470
|
-
const alreadyExists = reactions.some((r) => r.emoji === reaction.emoji && r.userId === reaction.userId);
|
|
471
|
-
if (!alreadyExists) {
|
|
472
|
-
reactions.push({
|
|
473
|
-
emoji: reaction.emoji,
|
|
474
|
-
userId: reaction.userId,
|
|
475
|
-
createdAt: new Date().toISOString()
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
return this.updateComment(commentId, { reactions });
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
async removeReaction(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null> {
|
|
482
|
-
const existing = await this.getComment(commentId);
|
|
483
|
-
if (!existing) return null;
|
|
484
|
-
const reactions = (existing.reactions || []).filter(
|
|
485
|
-
(r) => !(r.emoji === reaction.emoji && r.userId === reaction.userId)
|
|
486
|
-
);
|
|
487
|
-
return this.updateComment(commentId, { reactions });
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// --- Attachments ---
|
|
491
|
-
async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]> {
|
|
492
|
-
let snap;
|
|
493
|
-
if (filter?.taskId) {
|
|
494
|
-
snap = await this.db.collection('attachments').where('taskId', '==', filter.taskId).get();
|
|
495
|
-
} else if (filter?.projectId) {
|
|
496
|
-
snap = await this.db.collection('attachments').where('projectId', '==', filter.projectId).get();
|
|
497
|
-
} else if (filter?.commentId) {
|
|
498
|
-
snap = await this.db.collection('attachments').where('commentId', '==', filter.commentId).get();
|
|
499
|
-
} else {
|
|
500
|
-
snap = await this.db.collection('attachments').get();
|
|
501
|
-
}
|
|
502
|
-
// One filter is pushed down to Firestore; the others are applied here.
|
|
503
|
-
return snap.docs
|
|
504
|
-
.map((doc) => ({ ...doc.data(), id: doc.id }) as Attachment)
|
|
505
|
-
.filter(
|
|
506
|
-
(a) =>
|
|
507
|
-
(!filter?.taskId || a.taskId === filter.taskId) &&
|
|
508
|
-
(!filter?.projectId || a.projectId === filter.projectId) &&
|
|
509
|
-
(!filter?.commentId || a.commentId === filter.commentId)
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
async getAttachment(id: string): Promise<Attachment | null> {
|
|
514
|
-
const snap = await this.db.collection('attachments').doc(id).get();
|
|
515
|
-
if (!snap.exists) return null;
|
|
516
|
-
return { ...snap.data(), id: snap.id };
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
async createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment> {
|
|
520
|
-
const docRef = this.db.collection('attachments').doc();
|
|
521
|
-
const now = new Date().toISOString();
|
|
522
|
-
const newAtt: Attachment = { ...attachment, id: docRef.id, createdAt: now, updatedAt: now };
|
|
523
|
-
await docRef.set(sanitizeFirestoreData(newAtt));
|
|
524
|
-
return newAtt;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
async deleteAttachment(id: string): Promise<boolean> {
|
|
528
|
-
const existing = await this.getAttachment(id);
|
|
529
|
-
if (!existing) return false;
|
|
530
|
-
await this.db.collection('attachments').doc(id).delete();
|
|
531
|
-
return true;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
|
|
535
|
-
let snap;
|
|
536
|
-
if (filter?.taskId) {
|
|
537
|
-
snap = await this.db.collection('activities').where('taskId', '==', filter.taskId).get();
|
|
538
|
-
} else if (filter?.projectId) {
|
|
539
|
-
snap = await this.db.collection('activities').where('projectId', '==', filter.projectId).get();
|
|
540
|
-
} else {
|
|
541
|
-
snap = await this.db.collection('activities').get();
|
|
542
|
-
}
|
|
543
|
-
// Apply the remaining filter and return newest first, like the other adapters.
|
|
544
|
-
return snap.docs
|
|
545
|
-
.map((doc) => ({ ...doc.data(), id: doc.id }) as Activity)
|
|
546
|
-
.filter((a) => (!filter?.projectId || a.projectId === filter.projectId) && (!filter?.taskId || a.taskId === filter.taskId))
|
|
547
|
-
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : a.createdAt > b.createdAt ? -1 : 0));
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async queryActivities(query: ActivityQuery): Promise<Page<Activity>> {
|
|
551
|
-
const activities = (await this.getActivities({ projectId: query.projectId, taskId: query.taskId })).filter((a) =>
|
|
552
|
-
matchesActivityQuery(a, query)
|
|
553
|
-
);
|
|
554
|
-
return paginate(activities, (a) => a.createdAt, 'desc', query.limit, query.cursor);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
async logActivity(activity: Omit<Activity, 'id' | 'createdAt'>): Promise<Activity> {
|
|
558
|
-
const docRef = this.db.collection('activities').doc();
|
|
559
|
-
const now = new Date().toISOString();
|
|
560
|
-
const newAct: Activity = { ...activity, id: docRef.id, createdAt: now };
|
|
561
|
-
await docRef.set(sanitizeFirestoreData(newAct));
|
|
562
|
-
return newAct;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// --- Time Tracking ---
|
|
566
|
-
async getTimeEntries(taskId: string): Promise<TimeEntry[]> {
|
|
567
|
-
const snap = await this.db.collection('time_entries').where('taskId', '==', taskId).get();
|
|
568
|
-
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
async logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt'> & { loggedAt?: string }): Promise<TimeEntry> {
|
|
572
|
-
const docRef = this.db.collection('time_entries').doc();
|
|
573
|
-
const now = entry.loggedAt || new Date().toISOString();
|
|
574
|
-
const newEntry: TimeEntry = { ...entry, id: docRef.id, loggedAt: now };
|
|
575
|
-
await docRef.set(sanitizeFirestoreData(newEntry));
|
|
576
|
-
return newEntry;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// --- Dependencies ---
|
|
580
|
-
async getDependencies(taskId: string): Promise<TaskDependency[]> {
|
|
581
|
-
const [snap1, snap2] = await Promise.all([
|
|
582
|
-
this.db.collection('dependencies').where('taskId', '==', taskId).get(),
|
|
583
|
-
this.db.collection('dependencies').where('dependsOnTaskId', '==', taskId).get()
|
|
584
|
-
]);
|
|
585
|
-
const depMap = new Map<string, TaskDependency>();
|
|
586
|
-
for (const doc of snap1.docs) {
|
|
587
|
-
depMap.set(doc.id, { ...doc.data(), id: doc.id } as TaskDependency);
|
|
588
|
-
}
|
|
589
|
-
for (const doc of snap2.docs) {
|
|
590
|
-
depMap.set(doc.id, { ...doc.data(), id: doc.id } as TaskDependency);
|
|
591
|
-
}
|
|
592
|
-
return Array.from(depMap.values());
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
async addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency> {
|
|
596
|
-
const docRef = this.db.collection('dependencies').doc();
|
|
597
|
-
const newDep: TaskDependency = { ...dep, id: docRef.id };
|
|
598
|
-
await docRef.set(sanitizeFirestoreData(newDep));
|
|
599
|
-
return newDep;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async getDependency(id: string): Promise<TaskDependency | null> {
|
|
603
|
-
const doc = await this.db.collection('dependencies').doc(id).get();
|
|
604
|
-
return doc.exists ? ({ ...doc.data(), id: doc.id } as TaskDependency) : null;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
async removeDependency(id: string): Promise<boolean> {
|
|
608
|
-
return this.deleteDoc('dependencies', id);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async deleteTimeEntry(id: string): Promise<boolean> {
|
|
612
|
-
return this.deleteDoc('time_entries', id);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
private async deleteDoc(collection: string, id: string): Promise<boolean> {
|
|
616
|
-
const ref = this.db.collection(collection).doc(id);
|
|
617
|
-
const doc = await ref.get();
|
|
618
|
-
if (!doc.exists) return false;
|
|
619
|
-
await ref.delete();
|
|
620
|
-
return true;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
// --- Webhooks ---
|
|
624
|
-
async getWebhooks(): Promise<Webhook[]> {
|
|
625
|
-
const snap = await this.db.collection('webhooks').get();
|
|
626
|
-
return snap.docs.map((doc) => ({ ...doc.data(), id: doc.id }));
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
async getWebhook(id: string): Promise<Webhook | null> {
|
|
630
|
-
const doc = await this.db.collection('webhooks').doc(id).get();
|
|
631
|
-
return doc.exists ? ({ ...doc.data(), id: doc.id } as Webhook) : null;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
async addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook> {
|
|
635
|
-
const docRef = this.db.collection('webhooks').doc();
|
|
636
|
-
const now = new Date().toISOString();
|
|
637
|
-
const newWh: Webhook = { ...webhook, id: docRef.id, createdAt: now };
|
|
638
|
-
await docRef.set(sanitizeFirestoreData(newWh));
|
|
639
|
-
return newWh;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
async updateWebhook(id: string, updates: Partial<Omit<Webhook, 'id' | 'createdAt'>>): Promise<Webhook | null> {
|
|
643
|
-
const existing = await this.getWebhook(id);
|
|
644
|
-
if (!existing) return null;
|
|
645
|
-
const updated: Webhook = { ...existing, ...updates, id, createdAt: existing.createdAt };
|
|
646
|
-
// Write the whole document (no merge) so fields cleared with `undefined` are removed.
|
|
647
|
-
await this.db.collection('webhooks').doc(id).set(sanitizeFirestoreData(updated));
|
|
648
|
-
return updated;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
async deleteWebhook(id: string): Promise<boolean> {
|
|
652
|
-
const ref = this.db.collection('webhooks').doc(id);
|
|
653
|
-
const doc = await ref.get();
|
|
654
|
-
if (!doc.exists) return false;
|
|
655
|
-
await ref.delete();
|
|
656
|
-
return true;
|
|
657
|
-
}
|
|
658
|
-
}
|