@critical-path/core 0.35.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/README.md +11 -0
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +25 -2
- package/dist/engine/index.js.map +1 -1
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +5 -1
- package/CHANGELOG.md +0 -584
- 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/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 -2472
- 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/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 -859
- 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/sqlite.ts
DELETED
|
@@ -1,1480 +0,0 @@
|
|
|
1
|
-
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import type { StorageAdapter } from './index.js';
|
|
3
|
-
import { decodeCursor, encodeCursor, pageSize, type ActivityQuery, type Page, type TaskQuery } from './query.js';
|
|
4
|
-
import type {
|
|
5
|
-
Project,
|
|
6
|
-
Task,
|
|
7
|
-
Iteration,
|
|
8
|
-
Team,
|
|
9
|
-
TaskContainer,
|
|
10
|
-
Comment,
|
|
11
|
-
Attachment,
|
|
12
|
-
TimeEntry,
|
|
13
|
-
Activity,
|
|
14
|
-
Webhook,
|
|
15
|
-
TaskDependency,
|
|
16
|
-
Workflow,
|
|
17
|
-
Deliverable,
|
|
18
|
-
CreateDeliverableInput
|
|
19
|
-
} from '../types/index.js';
|
|
20
|
-
import { generateProjectKey } from '../utils/key.js';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* SQLite returns NULL columns as `null`, while domain objects leave unset optional fields out.
|
|
24
|
-
* Dropping nulls keeps SQLiteStore results identical to the other adapters.
|
|
25
|
-
*/
|
|
26
|
-
const EXTRA_TABLES = ['projects', 'tasks', 'teams', 'attachments'] as const;
|
|
27
|
-
type ExtraTable = (typeof EXTRA_TABLES)[number];
|
|
28
|
-
|
|
29
|
-
function dropNulls<T extends object>(row: T): T {
|
|
30
|
-
return Object.fromEntries(Object.entries(row).filter(([, value]) => value !== null)) as T;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface SQLiteStoreConfig {
|
|
34
|
-
/**
|
|
35
|
-
* Database file path (e.g., 'critical-path.db' or ':memory:').
|
|
36
|
-
* Defaults to ':memory:'.
|
|
37
|
-
*/
|
|
38
|
-
filename?: string;
|
|
39
|
-
/**
|
|
40
|
-
* Existing DatabaseSync instance or custom SQLite instance.
|
|
41
|
-
*/
|
|
42
|
-
db?: DatabaseSync;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class SQLiteStore implements StorageAdapter {
|
|
46
|
-
private db: DatabaseSync;
|
|
47
|
-
|
|
48
|
-
constructor(config: SQLiteStoreConfig = {}) {
|
|
49
|
-
if (config.db) {
|
|
50
|
-
this.db = config.db;
|
|
51
|
-
} else {
|
|
52
|
-
if (typeof window !== 'undefined') {
|
|
53
|
-
throw new Error('SQLiteStore is only available in Node.js environments.');
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
// `require` is undefined in ESM, and a static import would break browser bundles of core.
|
|
57
|
-
const nodeSqlite = (globalThis as any).process?.getBuiltinModule?.('node:sqlite') as
|
|
58
|
-
| typeof import('node:sqlite')
|
|
59
|
-
| undefined;
|
|
60
|
-
if (!nodeSqlite) {
|
|
61
|
-
throw new Error('process.getBuiltinModule is unavailable (requires Node.js 22.3+)');
|
|
62
|
-
}
|
|
63
|
-
this.db = new nodeSqlite.DatabaseSync(config.filename || ':memory:');
|
|
64
|
-
} catch (err: any) {
|
|
65
|
-
throw new Error(`Failed to load node:sqlite module: ${err.message}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
this.initTables();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private initTables(): void {
|
|
72
|
-
this.db.exec(`
|
|
73
|
-
CREATE TABLE IF NOT EXISTS projects (
|
|
74
|
-
id TEXT PRIMARY KEY,
|
|
75
|
-
key TEXT NOT NULL,
|
|
76
|
-
name TEXT NOT NULL,
|
|
77
|
-
description TEXT,
|
|
78
|
-
ownerId TEXT,
|
|
79
|
-
members TEXT,
|
|
80
|
-
teamIds TEXT,
|
|
81
|
-
workflowId TEXT,
|
|
82
|
-
taskTypes TEXT,
|
|
83
|
-
statusDefinitions TEXT,
|
|
84
|
-
priorityDefinitions TEXT,
|
|
85
|
-
customFieldDefinitions TEXT,
|
|
86
|
-
createdAt TEXT NOT NULL,
|
|
87
|
-
updatedAt TEXT NOT NULL
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
CREATE TABLE IF NOT EXISTS workflows (
|
|
91
|
-
id TEXT PRIMARY KEY,
|
|
92
|
-
name TEXT NOT NULL,
|
|
93
|
-
description TEXT,
|
|
94
|
-
statuses TEXT NOT NULL,
|
|
95
|
-
transitions TEXT NOT NULL,
|
|
96
|
-
taskTypes TEXT,
|
|
97
|
-
defaultStatusKey TEXT,
|
|
98
|
-
isDefault INTEGER,
|
|
99
|
-
createdAt TEXT NOT NULL,
|
|
100
|
-
updatedAt TEXT NOT NULL
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
CREATE TABLE IF NOT EXISTS teams (
|
|
104
|
-
id TEXT PRIMARY KEY,
|
|
105
|
-
name TEXT NOT NULL,
|
|
106
|
-
description TEXT,
|
|
107
|
-
leaderId TEXT,
|
|
108
|
-
memberIds TEXT NOT NULL,
|
|
109
|
-
createdAt TEXT NOT NULL,
|
|
110
|
-
updatedAt TEXT NOT NULL
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
CREATE TABLE IF NOT EXISTS containers (
|
|
114
|
-
id TEXT PRIMARY KEY,
|
|
115
|
-
projectId TEXT NOT NULL,
|
|
116
|
-
name TEXT NOT NULL,
|
|
117
|
-
description TEXT,
|
|
118
|
-
parentId TEXT,
|
|
119
|
-
type TEXT,
|
|
120
|
-
color TEXT,
|
|
121
|
-
createdAt TEXT NOT NULL,
|
|
122
|
-
updatedAt TEXT NOT NULL
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
126
|
-
id TEXT PRIMARY KEY,
|
|
127
|
-
projectId TEXT NOT NULL,
|
|
128
|
-
title TEXT NOT NULL,
|
|
129
|
-
description TEXT,
|
|
130
|
-
status TEXT NOT NULL,
|
|
131
|
-
priority TEXT NOT NULL,
|
|
132
|
-
taskType TEXT,
|
|
133
|
-
assigneeId TEXT,
|
|
134
|
-
assignees TEXT,
|
|
135
|
-
reporterId TEXT,
|
|
136
|
-
reviewerId TEXT,
|
|
137
|
-
iterationId TEXT,
|
|
138
|
-
teamId TEXT,
|
|
139
|
-
containerId TEXT,
|
|
140
|
-
deliverableId TEXT,
|
|
141
|
-
plannedStartDate TEXT,
|
|
142
|
-
actualStartDate TEXT,
|
|
143
|
-
actualEndDate TEXT,
|
|
144
|
-
completedAt TEXT,
|
|
145
|
-
dueDate TEXT,
|
|
146
|
-
estimatedHours REAL,
|
|
147
|
-
loggedHours REAL,
|
|
148
|
-
actualHours REAL,
|
|
149
|
-
billableHours REAL,
|
|
150
|
-
estimatedDurationMinutes REAL,
|
|
151
|
-
actualDurationMinutes REAL,
|
|
152
|
-
billableDurationMinutes REAL,
|
|
153
|
-
actualDurationSeconds REAL,
|
|
154
|
-
inProgressSince TEXT,
|
|
155
|
-
blockedDurationSeconds REAL,
|
|
156
|
-
blockedSince TEXT,
|
|
157
|
-
progress REAL,
|
|
158
|
-
isBlocked INTEGER,
|
|
159
|
-
blockedReason TEXT,
|
|
160
|
-
tags TEXT,
|
|
161
|
-
customFields TEXT,
|
|
162
|
-
parentId TEXT,
|
|
163
|
-
createdAt TEXT NOT NULL,
|
|
164
|
-
updatedAt TEXT NOT NULL
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
CREATE TABLE IF NOT EXISTS deliverables (
|
|
168
|
-
id TEXT PRIMARY KEY,
|
|
169
|
-
projectId TEXT NOT NULL,
|
|
170
|
-
title TEXT NOT NULL,
|
|
171
|
-
description TEXT,
|
|
172
|
-
status TEXT NOT NULL,
|
|
173
|
-
format TEXT,
|
|
174
|
-
specs TEXT,
|
|
175
|
-
leadId TEXT,
|
|
176
|
-
reviewerId TEXT,
|
|
177
|
-
dueDate TEXT,
|
|
178
|
-
deliveredAt TEXT,
|
|
179
|
-
outputUrls TEXT,
|
|
180
|
-
customFields TEXT,
|
|
181
|
-
createdAt TEXT NOT NULL,
|
|
182
|
-
updatedAt TEXT NOT NULL
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
CREATE TABLE IF NOT EXISTS iterations (
|
|
186
|
-
id TEXT PRIMARY KEY,
|
|
187
|
-
projectId TEXT NOT NULL,
|
|
188
|
-
name TEXT NOT NULL,
|
|
189
|
-
goal TEXT,
|
|
190
|
-
type TEXT,
|
|
191
|
-
startDate TEXT,
|
|
192
|
-
endDate TEXT,
|
|
193
|
-
status TEXT NOT NULL,
|
|
194
|
-
createdAt TEXT NOT NULL
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
CREATE TABLE IF NOT EXISTS comments (
|
|
198
|
-
id TEXT PRIMARY KEY,
|
|
199
|
-
taskId TEXT NOT NULL,
|
|
200
|
-
authorId TEXT NOT NULL,
|
|
201
|
-
authorType TEXT,
|
|
202
|
-
parentId TEXT,
|
|
203
|
-
content TEXT NOT NULL,
|
|
204
|
-
mentions TEXT,
|
|
205
|
-
reactions TEXT,
|
|
206
|
-
metadata TEXT,
|
|
207
|
-
createdAt TEXT NOT NULL,
|
|
208
|
-
updatedAt TEXT NOT NULL
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
CREATE TABLE IF NOT EXISTS attachments (
|
|
212
|
-
id TEXT PRIMARY KEY,
|
|
213
|
-
taskId TEXT,
|
|
214
|
-
projectId TEXT,
|
|
215
|
-
commentId TEXT,
|
|
216
|
-
uploaderId TEXT NOT NULL,
|
|
217
|
-
uploaderType TEXT,
|
|
218
|
-
filename TEXT NOT NULL,
|
|
219
|
-
mimeType TEXT NOT NULL,
|
|
220
|
-
sizeBytes INTEGER NOT NULL,
|
|
221
|
-
url TEXT NOT NULL,
|
|
222
|
-
storageKey TEXT,
|
|
223
|
-
metadata TEXT,
|
|
224
|
-
createdAt TEXT NOT NULL,
|
|
225
|
-
updatedAt TEXT NOT NULL
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
CREATE TABLE IF NOT EXISTS activities (
|
|
229
|
-
id TEXT PRIMARY KEY,
|
|
230
|
-
projectId TEXT,
|
|
231
|
-
taskId TEXT,
|
|
232
|
-
actorId TEXT NOT NULL,
|
|
233
|
-
action TEXT NOT NULL,
|
|
234
|
-
details TEXT,
|
|
235
|
-
createdAt TEXT NOT NULL
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
CREATE TABLE IF NOT EXISTS time_entries (
|
|
239
|
-
id TEXT PRIMARY KEY,
|
|
240
|
-
taskId TEXT NOT NULL,
|
|
241
|
-
userId TEXT NOT NULL,
|
|
242
|
-
hours REAL NOT NULL,
|
|
243
|
-
isBillable INTEGER,
|
|
244
|
-
description TEXT,
|
|
245
|
-
loggedAt TEXT NOT NULL
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
CREATE TABLE IF NOT EXISTS dependencies (
|
|
249
|
-
id TEXT PRIMARY KEY,
|
|
250
|
-
taskId TEXT NOT NULL,
|
|
251
|
-
dependsOnTaskId TEXT NOT NULL,
|
|
252
|
-
type TEXT NOT NULL
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
CREATE TABLE IF NOT EXISTS webhooks (
|
|
256
|
-
id TEXT PRIMARY KEY,
|
|
257
|
-
url TEXT NOT NULL,
|
|
258
|
-
events TEXT NOT NULL,
|
|
259
|
-
secret TEXT,
|
|
260
|
-
active INTEGER NOT NULL,
|
|
261
|
-
createdAt TEXT NOT NULL
|
|
262
|
-
);
|
|
263
|
-
`);
|
|
264
|
-
|
|
265
|
-
try {
|
|
266
|
-
this.db.exec('ALTER TABLE comments ADD COLUMN reactions TEXT');
|
|
267
|
-
} catch {
|
|
268
|
-
// Column may already exist
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
this.db.exec('ALTER TABLE comments ADD COLUMN mentions TEXT');
|
|
273
|
-
} catch {
|
|
274
|
-
// Column may already exist
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
this.db.exec('ALTER TABLE tasks ADD COLUMN assignees TEXT');
|
|
279
|
-
} catch {
|
|
280
|
-
// Column may already exist
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
this.db.exec('ALTER TABLE tasks ADD COLUMN deliverableId TEXT');
|
|
285
|
-
} catch {
|
|
286
|
-
// Column may already exist
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
this.db.exec('ALTER TABLE tasks ADD COLUMN completedAt TEXT');
|
|
291
|
-
} catch {
|
|
292
|
-
// Column may already exist
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Indexes for filtered queries and keyset pagination
|
|
296
|
-
this.db.exec(`
|
|
297
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_project_created ON tasks (projectId, createdAt, id);
|
|
298
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks (parentId);
|
|
299
|
-
CREATE INDEX IF NOT EXISTS idx_activities_project_created ON activities (projectId, createdAt, id);
|
|
300
|
-
CREATE INDEX IF NOT EXISTS idx_activities_task_created ON activities (taskId, createdAt, id);
|
|
301
|
-
CREATE INDEX IF NOT EXISTS idx_comments_task ON comments (taskId);
|
|
302
|
-
CREATE INDEX IF NOT EXISTS idx_dependencies_task ON dependencies (taskId);
|
|
303
|
-
CREATE INDEX IF NOT EXISTS idx_dependencies_upstream ON dependencies (dependsOnTaskId);
|
|
304
|
-
`);
|
|
305
|
-
|
|
306
|
-
for (const column of ['name TEXT', 'tenantId TEXT']) {
|
|
307
|
-
try {
|
|
308
|
-
this.db.exec(`ALTER TABLE webhooks ADD COLUMN ${column}`);
|
|
309
|
-
} catch {
|
|
310
|
-
// Column may already exist
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Fields without a dedicated column are kept in a JSON `extra` column, so entities round-trip
|
|
315
|
-
// even when their types gain fields this schema does not know about.
|
|
316
|
-
for (const table of EXTRA_TABLES) {
|
|
317
|
-
try {
|
|
318
|
-
this.db.exec(`ALTER TABLE ${table} ADD COLUMN extra TEXT`);
|
|
319
|
-
} catch {
|
|
320
|
-
// Column may already exist
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
for (const table of ['projects', 'workflows', 'teams']) {
|
|
325
|
-
try {
|
|
326
|
-
this.db.exec(`ALTER TABLE ${table} ADD COLUMN tenantId TEXT`);
|
|
327
|
-
} catch {
|
|
328
|
-
// Column may already exist
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
private columnCache = new Map<string, Set<string>>();
|
|
334
|
-
|
|
335
|
-
private columnsOf(table: ExtraTable): Set<string> {
|
|
336
|
-
let columns = this.columnCache.get(table);
|
|
337
|
-
if (!columns) {
|
|
338
|
-
const info = this.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
339
|
-
columns = new Set(info.map((c) => c.name));
|
|
340
|
-
this.columnCache.set(table, columns);
|
|
341
|
-
}
|
|
342
|
-
return columns;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** Stores every defined field that has no dedicated column in the row's `extra` JSON. */
|
|
346
|
-
private writeExtra(table: ExtraTable, entity: object): void {
|
|
347
|
-
const columns = this.columnsOf(table);
|
|
348
|
-
const extra = Object.fromEntries(
|
|
349
|
-
Object.entries(entity).filter(([key, value]) => !columns.has(key) && value !== undefined)
|
|
350
|
-
);
|
|
351
|
-
this.db
|
|
352
|
-
.prepare(`UPDATE ${table} SET extra = ? WHERE id = ?`)
|
|
353
|
-
.run(Object.keys(extra).length ? JSON.stringify(extra) : null, (entity as { id: string }).id);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/** Merges the `extra` JSON back into a row. */
|
|
357
|
-
private expandExtra(row: any): any {
|
|
358
|
-
const { extra, ...rest } = row;
|
|
359
|
-
return extra ? { ...rest, ...JSON.parse(extra) } : rest;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// --- Projects ---
|
|
363
|
-
async getProjects(): Promise<Project[]> {
|
|
364
|
-
const stmt = this.db.prepare('SELECT * FROM projects');
|
|
365
|
-
const rows = stmt.all() as any[];
|
|
366
|
-
return rows.map((r) => this.mapProject(r));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async getProject(id: string): Promise<Project | null> {
|
|
370
|
-
const stmt = this.db.prepare('SELECT * FROM projects WHERE id = ?');
|
|
371
|
-
const row = stmt.get(id) as any;
|
|
372
|
-
return row ? this.mapProject(row) : null;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
async createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
|
|
376
|
-
const id = `proj_${crypto.randomUUID()}`;
|
|
377
|
-
const now = new Date().toISOString();
|
|
378
|
-
const key = project.key || generateProjectKey(project.name);
|
|
379
|
-
const newProj: Project = { ...project, key, id, createdAt: now, updatedAt: now };
|
|
380
|
-
|
|
381
|
-
const stmt = this.db.prepare(`
|
|
382
|
-
INSERT INTO projects (id, key, name, description, ownerId, members, teamIds, workflowId, taskTypes, statusDefinitions, priorityDefinitions, customFieldDefinitions, tenantId, createdAt, updatedAt)
|
|
383
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
384
|
-
`);
|
|
385
|
-
stmt.run(
|
|
386
|
-
newProj.id,
|
|
387
|
-
newProj.key || '',
|
|
388
|
-
newProj.name,
|
|
389
|
-
newProj.description || null,
|
|
390
|
-
newProj.ownerId || null,
|
|
391
|
-
JSON.stringify(newProj.members || []),
|
|
392
|
-
JSON.stringify(newProj.teamIds || []),
|
|
393
|
-
newProj.workflowId || null,
|
|
394
|
-
newProj.taskTypes ? JSON.stringify(newProj.taskTypes) : null,
|
|
395
|
-
JSON.stringify(newProj.statusDefinitions || []),
|
|
396
|
-
JSON.stringify(newProj.priorityDefinitions || []),
|
|
397
|
-
JSON.stringify(newProj.customFieldDefinitions || []),
|
|
398
|
-
newProj.tenantId ?? null,
|
|
399
|
-
newProj.createdAt,
|
|
400
|
-
newProj.updatedAt
|
|
401
|
-
);
|
|
402
|
-
this.writeExtra('projects', newProj);
|
|
403
|
-
return newProj;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async updateProject(id: string, updates: Partial<Project>): Promise<Project | null> {
|
|
407
|
-
const existing = await this.getProject(id);
|
|
408
|
-
if (!existing) return null;
|
|
409
|
-
|
|
410
|
-
const updated: Project = {
|
|
411
|
-
...existing,
|
|
412
|
-
...updates,
|
|
413
|
-
updatedAt: new Date().toISOString()
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
const stmt = this.db.prepare(`
|
|
417
|
-
UPDATE projects
|
|
418
|
-
SET key = ?, name = ?, description = ?, ownerId = ?, members = ?, teamIds = ?, workflowId = ?, taskTypes = ?, statusDefinitions = ?, priorityDefinitions = ?, customFieldDefinitions = ?, updatedAt = ?
|
|
419
|
-
WHERE id = ?
|
|
420
|
-
`);
|
|
421
|
-
stmt.run(
|
|
422
|
-
updated.key || '',
|
|
423
|
-
updated.name,
|
|
424
|
-
updated.description || null,
|
|
425
|
-
updated.ownerId || null,
|
|
426
|
-
JSON.stringify(updated.members || []),
|
|
427
|
-
JSON.stringify(updated.teamIds || []),
|
|
428
|
-
updated.workflowId || null,
|
|
429
|
-
updated.taskTypes ? JSON.stringify(updated.taskTypes) : null,
|
|
430
|
-
JSON.stringify(updated.statusDefinitions || []),
|
|
431
|
-
JSON.stringify(updated.priorityDefinitions || []),
|
|
432
|
-
JSON.stringify(updated.customFieldDefinitions || []),
|
|
433
|
-
updated.updatedAt,
|
|
434
|
-
id
|
|
435
|
-
);
|
|
436
|
-
this.writeExtra('projects', updated);
|
|
437
|
-
return updated;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// --- Workflows ---
|
|
441
|
-
async getWorkflows(): Promise<Workflow[]> {
|
|
442
|
-
const stmt = this.db.prepare('SELECT * FROM workflows');
|
|
443
|
-
const rows = stmt.all() as any[];
|
|
444
|
-
return rows.map((r) => this.mapWorkflow(r));
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
async getWorkflow(id: string): Promise<Workflow | null> {
|
|
448
|
-
const stmt = this.db.prepare('SELECT * FROM workflows WHERE id = ?');
|
|
449
|
-
const row = stmt.get(id) as any;
|
|
450
|
-
return row ? this.mapWorkflow(row) : null;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
|
|
454
|
-
const id = `wf_${crypto.randomUUID()}`;
|
|
455
|
-
const now = new Date().toISOString();
|
|
456
|
-
const newWf: Workflow = { ...workflow, id, createdAt: now, updatedAt: now };
|
|
457
|
-
|
|
458
|
-
const stmt = this.db.prepare(`
|
|
459
|
-
INSERT INTO workflows (id, name, description, statuses, transitions, taskTypes, defaultStatusKey, isDefault, tenantId, createdAt, updatedAt)
|
|
460
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
461
|
-
`);
|
|
462
|
-
stmt.run(
|
|
463
|
-
newWf.id,
|
|
464
|
-
newWf.name,
|
|
465
|
-
newWf.description || null,
|
|
466
|
-
JSON.stringify(newWf.statuses || []),
|
|
467
|
-
JSON.stringify(newWf.transitions || []),
|
|
468
|
-
newWf.taskTypes ? JSON.stringify(newWf.taskTypes) : null,
|
|
469
|
-
newWf.defaultStatusKey || null,
|
|
470
|
-
newWf.isDefault ? 1 : 0,
|
|
471
|
-
newWf.tenantId ?? null,
|
|
472
|
-
newWf.createdAt,
|
|
473
|
-
newWf.updatedAt
|
|
474
|
-
);
|
|
475
|
-
return newWf;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
async updateWorkflow(id: string, updates: Partial<Workflow>): Promise<Workflow | null> {
|
|
479
|
-
const existing = await this.getWorkflow(id);
|
|
480
|
-
if (!existing) return null;
|
|
481
|
-
|
|
482
|
-
const updated: Workflow = {
|
|
483
|
-
...existing,
|
|
484
|
-
...updates,
|
|
485
|
-
updatedAt: new Date().toISOString()
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
const stmt = this.db.prepare(`
|
|
489
|
-
UPDATE workflows SET name = ?, description = ?, statuses = ?, transitions = ?, taskTypes = ?, defaultStatusKey = ?, isDefault = ?, updatedAt = ?
|
|
490
|
-
WHERE id = ?
|
|
491
|
-
`);
|
|
492
|
-
stmt.run(
|
|
493
|
-
updated.name,
|
|
494
|
-
updated.description || null,
|
|
495
|
-
JSON.stringify(updated.statuses || []),
|
|
496
|
-
JSON.stringify(updated.transitions || []),
|
|
497
|
-
updated.taskTypes ? JSON.stringify(updated.taskTypes) : null,
|
|
498
|
-
updated.defaultStatusKey || null,
|
|
499
|
-
updated.isDefault ? 1 : 0,
|
|
500
|
-
updated.updatedAt,
|
|
501
|
-
id
|
|
502
|
-
);
|
|
503
|
-
return updated;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
async deleteWorkflow(id: string): Promise<boolean> {
|
|
507
|
-
const stmt = this.db.prepare('DELETE FROM workflows WHERE id = ?');
|
|
508
|
-
const result = stmt.run(id);
|
|
509
|
-
return (result.changes ?? 0) > 0;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async deleteProject(id: string): Promise<boolean> {
|
|
513
|
-
const stmt = this.db.prepare('DELETE FROM projects WHERE id = ?');
|
|
514
|
-
const result = stmt.run(id);
|
|
515
|
-
return (result.changes ?? 0) > 0;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// --- Teams ---
|
|
519
|
-
async getTeams(): Promise<Team[]> {
|
|
520
|
-
const stmt = this.db.prepare('SELECT * FROM teams');
|
|
521
|
-
const rows = stmt.all() as any[];
|
|
522
|
-
return rows.map((r) => this.mapTeam(r));
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
async getTeam(id: string): Promise<Team | null> {
|
|
526
|
-
const stmt = this.db.prepare('SELECT * FROM teams WHERE id = ?');
|
|
527
|
-
const row = stmt.get(id) as any;
|
|
528
|
-
return row ? this.mapTeam(row) : null;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
async createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team> {
|
|
532
|
-
const id = `team_${crypto.randomUUID()}`;
|
|
533
|
-
const now = new Date().toISOString();
|
|
534
|
-
const newTeam: Team = { ...team, id, createdAt: now, updatedAt: now };
|
|
535
|
-
|
|
536
|
-
const stmt = this.db.prepare(`
|
|
537
|
-
INSERT INTO teams (id, name, description, leaderId, memberIds, tenantId, createdAt, updatedAt)
|
|
538
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
539
|
-
`);
|
|
540
|
-
stmt.run(
|
|
541
|
-
newTeam.id,
|
|
542
|
-
newTeam.name,
|
|
543
|
-
newTeam.description || null,
|
|
544
|
-
newTeam.leaderId || null,
|
|
545
|
-
JSON.stringify(newTeam.memberIds || []),
|
|
546
|
-
newTeam.tenantId ?? null,
|
|
547
|
-
newTeam.createdAt,
|
|
548
|
-
newTeam.updatedAt
|
|
549
|
-
);
|
|
550
|
-
this.writeExtra('teams', newTeam);
|
|
551
|
-
return newTeam;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
async updateTeam(id: string, updates: Partial<Team>): Promise<Team | null> {
|
|
555
|
-
const existing = await this.getTeam(id);
|
|
556
|
-
if (!existing) return null;
|
|
557
|
-
|
|
558
|
-
const updated: Team = { ...existing, ...updates, updatedAt: new Date().toISOString() };
|
|
559
|
-
const stmt = this.db.prepare(`
|
|
560
|
-
UPDATE teams SET name = ?, description = ?, leaderId = ?, memberIds = ?, updatedAt = ?
|
|
561
|
-
WHERE id = ?
|
|
562
|
-
`);
|
|
563
|
-
stmt.run(
|
|
564
|
-
updated.name,
|
|
565
|
-
updated.description || null,
|
|
566
|
-
updated.leaderId || null,
|
|
567
|
-
JSON.stringify(updated.memberIds || []),
|
|
568
|
-
updated.updatedAt,
|
|
569
|
-
id
|
|
570
|
-
);
|
|
571
|
-
this.writeExtra('teams', updated);
|
|
572
|
-
return updated;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
async deleteTeam(id: string): Promise<boolean> {
|
|
576
|
-
const stmt = this.db.prepare('DELETE FROM teams WHERE id = ?');
|
|
577
|
-
const result = stmt.run(id);
|
|
578
|
-
return (result.changes ?? 0) > 0;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// --- Task Containers ---
|
|
582
|
-
async getContainers(projectId: string): Promise<TaskContainer[]> {
|
|
583
|
-
const stmt = this.db.prepare('SELECT * FROM containers WHERE projectId = ?');
|
|
584
|
-
const rows = stmt.all(projectId) as any[];
|
|
585
|
-
return rows.map(dropNulls);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
async getContainer(id: string): Promise<TaskContainer | null> {
|
|
589
|
-
const stmt = this.db.prepare('SELECT * FROM containers WHERE id = ?');
|
|
590
|
-
const row = stmt.get(id) as any;
|
|
591
|
-
return row ? dropNulls(row) : null;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async createContainer(container: Omit<TaskContainer, 'id' | 'createdAt' | 'updatedAt'>): Promise<TaskContainer> {
|
|
595
|
-
const id = `cnt_${crypto.randomUUID()}`;
|
|
596
|
-
const now = new Date().toISOString();
|
|
597
|
-
const newContainer: TaskContainer = { ...container, id, createdAt: now, updatedAt: now };
|
|
598
|
-
|
|
599
|
-
const stmt = this.db.prepare(`
|
|
600
|
-
INSERT INTO containers (id, projectId, name, description, parentId, type, color, createdAt, updatedAt)
|
|
601
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
602
|
-
`);
|
|
603
|
-
stmt.run(
|
|
604
|
-
newContainer.id,
|
|
605
|
-
newContainer.projectId,
|
|
606
|
-
newContainer.name,
|
|
607
|
-
newContainer.description || null,
|
|
608
|
-
newContainer.parentId || null,
|
|
609
|
-
newContainer.type || null,
|
|
610
|
-
newContainer.color || null,
|
|
611
|
-
newContainer.createdAt,
|
|
612
|
-
newContainer.updatedAt
|
|
613
|
-
);
|
|
614
|
-
return newContainer;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async updateContainer(id: string, updates: Partial<TaskContainer>): Promise<TaskContainer | null> {
|
|
618
|
-
const existing = await this.getContainer(id);
|
|
619
|
-
if (!existing) return null;
|
|
620
|
-
|
|
621
|
-
const updated: TaskContainer = { ...existing, ...updates, updatedAt: new Date().toISOString() };
|
|
622
|
-
const stmt = this.db.prepare(`
|
|
623
|
-
UPDATE containers SET name = ?, description = ?, parentId = ?, type = ?, color = ?, updatedAt = ?
|
|
624
|
-
WHERE id = ?
|
|
625
|
-
`);
|
|
626
|
-
stmt.run(
|
|
627
|
-
updated.name,
|
|
628
|
-
updated.description || null,
|
|
629
|
-
updated.parentId || null,
|
|
630
|
-
updated.type || null,
|
|
631
|
-
updated.color || null,
|
|
632
|
-
updated.updatedAt,
|
|
633
|
-
id
|
|
634
|
-
);
|
|
635
|
-
return updated;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async deleteContainer(id: string): Promise<boolean> {
|
|
639
|
-
const stmt = this.db.prepare('DELETE FROM containers WHERE id = ?');
|
|
640
|
-
const result = stmt.run(id);
|
|
641
|
-
return (result.changes ?? 0) > 0;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// --- Deliverables ---
|
|
645
|
-
async getDeliverables(projectId: string): Promise<Deliverable[]> {
|
|
646
|
-
const stmt = this.db.prepare('SELECT * FROM deliverables WHERE projectId = ?');
|
|
647
|
-
const rows = stmt.all(projectId) as any[];
|
|
648
|
-
return rows.map((r) => this.mapDeliverable(r));
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
async getDeliverable(id: string): Promise<Deliverable | null> {
|
|
652
|
-
const stmt = this.db.prepare('SELECT * FROM deliverables WHERE id = ?');
|
|
653
|
-
const row = stmt.get(id) as any;
|
|
654
|
-
return row ? this.mapDeliverable(row) : null;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
async createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable> {
|
|
658
|
-
const id = `deliv_${crypto.randomUUID()}`;
|
|
659
|
-
const now = new Date().toISOString();
|
|
660
|
-
const newDeliverable: Deliverable = {
|
|
661
|
-
...deliverable,
|
|
662
|
-
id,
|
|
663
|
-
status: deliverable.status || 'planned',
|
|
664
|
-
outputUrls: deliverable.outputUrls ? [...deliverable.outputUrls] : [],
|
|
665
|
-
customFields: deliverable.customFields ? { ...deliverable.customFields } : {},
|
|
666
|
-
createdAt: now,
|
|
667
|
-
updatedAt: now
|
|
668
|
-
};
|
|
669
|
-
|
|
670
|
-
const stmt = this.db.prepare(`
|
|
671
|
-
INSERT INTO deliverables (
|
|
672
|
-
id, projectId, title, description, status, format, specs, leadId, reviewerId,
|
|
673
|
-
dueDate, deliveredAt, outputUrls, customFields, createdAt, updatedAt
|
|
674
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
675
|
-
`);
|
|
676
|
-
stmt.run(
|
|
677
|
-
newDeliverable.id,
|
|
678
|
-
newDeliverable.projectId,
|
|
679
|
-
newDeliverable.title,
|
|
680
|
-
newDeliverable.description || null,
|
|
681
|
-
newDeliverable.status,
|
|
682
|
-
newDeliverable.format || null,
|
|
683
|
-
newDeliverable.specs ? JSON.stringify(newDeliverable.specs) : null,
|
|
684
|
-
newDeliverable.leadId || null,
|
|
685
|
-
newDeliverable.reviewerId || null,
|
|
686
|
-
newDeliverable.dueDate || null,
|
|
687
|
-
newDeliverable.deliveredAt || null,
|
|
688
|
-
JSON.stringify(newDeliverable.outputUrls || []),
|
|
689
|
-
JSON.stringify(newDeliverable.customFields || {}),
|
|
690
|
-
newDeliverable.createdAt,
|
|
691
|
-
newDeliverable.updatedAt
|
|
692
|
-
);
|
|
693
|
-
return newDeliverable;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
async updateDeliverable(id: string, updates: Partial<Deliverable>): Promise<Deliverable | null> {
|
|
697
|
-
const existing = await this.getDeliverable(id);
|
|
698
|
-
if (!existing) return null;
|
|
699
|
-
|
|
700
|
-
const updated: Deliverable = {
|
|
701
|
-
...existing,
|
|
702
|
-
...updates,
|
|
703
|
-
updatedAt: new Date().toISOString()
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
const stmt = this.db.prepare(`
|
|
707
|
-
UPDATE deliverables SET
|
|
708
|
-
title = ?, description = ?, status = ?, format = ?, specs = ?, leadId = ?, reviewerId = ?,
|
|
709
|
-
dueDate = ?, deliveredAt = ?, outputUrls = ?, customFields = ?, updatedAt = ?
|
|
710
|
-
WHERE id = ?
|
|
711
|
-
`);
|
|
712
|
-
stmt.run(
|
|
713
|
-
updated.title,
|
|
714
|
-
updated.description || null,
|
|
715
|
-
updated.status,
|
|
716
|
-
updated.format || null,
|
|
717
|
-
updated.specs ? JSON.stringify(updated.specs) : null,
|
|
718
|
-
updated.leadId || null,
|
|
719
|
-
updated.reviewerId || null,
|
|
720
|
-
updated.dueDate || null,
|
|
721
|
-
updated.deliveredAt || null,
|
|
722
|
-
JSON.stringify(updated.outputUrls || []),
|
|
723
|
-
JSON.stringify(updated.customFields || {}),
|
|
724
|
-
updated.updatedAt,
|
|
725
|
-
id
|
|
726
|
-
);
|
|
727
|
-
return updated;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
async deleteDeliverable(id: string): Promise<boolean> {
|
|
731
|
-
const stmt = this.db.prepare('DELETE FROM deliverables WHERE id = ?');
|
|
732
|
-
const result = stmt.run(id);
|
|
733
|
-
return (result.changes ?? 0) > 0;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// --- Tasks ---
|
|
737
|
-
async getTasks(projectId?: string): Promise<Task[]> {
|
|
738
|
-
let rows: any[];
|
|
739
|
-
if (projectId) {
|
|
740
|
-
const stmt = this.db.prepare('SELECT * FROM tasks WHERE projectId = ?');
|
|
741
|
-
rows = stmt.all(projectId) as any[];
|
|
742
|
-
} else {
|
|
743
|
-
const stmt = this.db.prepare('SELECT * FROM tasks');
|
|
744
|
-
rows = stmt.all() as any[];
|
|
745
|
-
}
|
|
746
|
-
return rows.map((r) => this.mapTask(r));
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
async queryTasks(query: TaskQuery): Promise<Page<Task>> {
|
|
750
|
-
const where: string[] = [];
|
|
751
|
-
const params: Array<string | number> = [];
|
|
752
|
-
const inList = (column: string, values: string[]) => {
|
|
753
|
-
where.push(`${column} IN (${values.map(() => '?').join(', ')})`);
|
|
754
|
-
params.push(...values);
|
|
755
|
-
};
|
|
756
|
-
|
|
757
|
-
if (query.projectId) {
|
|
758
|
-
where.push('projectId = ?');
|
|
759
|
-
params.push(query.projectId);
|
|
760
|
-
}
|
|
761
|
-
if (query.projectIds) {
|
|
762
|
-
if (query.projectIds.length === 0) return { items: [] };
|
|
763
|
-
inList('projectId', query.projectIds);
|
|
764
|
-
}
|
|
765
|
-
if (query.status?.length) inList('status', query.status);
|
|
766
|
-
if (query.priority?.length) inList('priority', query.priority);
|
|
767
|
-
for (const column of ['iterationId', 'deliverableId', 'containerId'] as const) {
|
|
768
|
-
if (query[column]) {
|
|
769
|
-
where.push(`${column} = ?`);
|
|
770
|
-
params.push(query[column]!);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
if (query.parentId === null) where.push('parentId IS NULL');
|
|
774
|
-
else if (query.parentId) {
|
|
775
|
-
where.push('parentId = ?');
|
|
776
|
-
params.push(query.parentId);
|
|
777
|
-
}
|
|
778
|
-
if (query.assigneeId) {
|
|
779
|
-
where.push(
|
|
780
|
-
"(assigneeId = ? OR EXISTS (SELECT 1 FROM json_each(COALESCE(assignees, '[]')) WHERE json_extract(value, '$.id') = ?))"
|
|
781
|
-
);
|
|
782
|
-
params.push(query.assigneeId, query.assigneeId);
|
|
783
|
-
}
|
|
784
|
-
const after = decodeCursor(query.cursor);
|
|
785
|
-
if (after) {
|
|
786
|
-
where.push('(createdAt > ? OR (createdAt = ? AND id > ?))');
|
|
787
|
-
params.push(after[0], after[0], after[1]);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const size = pageSize(query.limit);
|
|
791
|
-
const sql = `SELECT * FROM tasks ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY createdAt ASC, id ASC LIMIT ?`;
|
|
792
|
-
const rows = this.db.prepare(sql).all(...params, size + 1) as any[];
|
|
793
|
-
const items = rows.slice(0, size).map((r) => this.mapTask(r));
|
|
794
|
-
const last = items[items.length - 1];
|
|
795
|
-
return { items, nextCursor: rows.length > size && last ? encodeCursor(last.createdAt, last.id) : undefined };
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
async getTask(id: string): Promise<Task | null> {
|
|
799
|
-
const stmt = this.db.prepare('SELECT * FROM tasks WHERE id = ?');
|
|
800
|
-
const row = stmt.get(id) as any;
|
|
801
|
-
return row ? this.mapTask(row) : null;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
async createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
|
|
805
|
-
const id = `task_${crypto.randomUUID()}`;
|
|
806
|
-
const now = new Date().toISOString();
|
|
807
|
-
const newTask: Task = { ...task, id, createdAt: now, updatedAt: now };
|
|
808
|
-
|
|
809
|
-
const stmt = this.db.prepare(`
|
|
810
|
-
INSERT INTO tasks (
|
|
811
|
-
id, projectId, title, description, status, priority, taskType, assigneeId, assignees, reporterId,
|
|
812
|
-
reviewerId, iterationId, teamId, containerId, deliverableId, plannedStartDate, actualStartDate, actualEndDate, completedAt,
|
|
813
|
-
dueDate, estimatedHours, loggedHours, actualHours, billableHours,
|
|
814
|
-
estimatedDurationMinutes, actualDurationMinutes, billableDurationMinutes,
|
|
815
|
-
actualDurationSeconds, inProgressSince, blockedDurationSeconds, blockedSince, progress,
|
|
816
|
-
isBlocked, blockedReason,
|
|
817
|
-
tags, customFields, parentId, createdAt, updatedAt
|
|
818
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
819
|
-
`);
|
|
820
|
-
stmt.run(
|
|
821
|
-
newTask.id,
|
|
822
|
-
newTask.projectId,
|
|
823
|
-
newTask.title,
|
|
824
|
-
newTask.description || null,
|
|
825
|
-
newTask.status || 'todo',
|
|
826
|
-
newTask.priority || 'medium',
|
|
827
|
-
newTask.taskType || null,
|
|
828
|
-
newTask.assigneeId || null,
|
|
829
|
-
newTask.assignees ? JSON.stringify(newTask.assignees) : null,
|
|
830
|
-
newTask.reporterId || null,
|
|
831
|
-
newTask.reviewerId || null,
|
|
832
|
-
newTask.iterationId || null,
|
|
833
|
-
newTask.teamId || null,
|
|
834
|
-
newTask.containerId || null,
|
|
835
|
-
newTask.deliverableId || null,
|
|
836
|
-
newTask.plannedStartDate || null,
|
|
837
|
-
newTask.actualStartDate || null,
|
|
838
|
-
newTask.actualEndDate || null,
|
|
839
|
-
newTask.completedAt || null,
|
|
840
|
-
newTask.dueDate || null,
|
|
841
|
-
newTask.estimatedHours ?? null,
|
|
842
|
-
newTask.loggedHours ?? null,
|
|
843
|
-
newTask.actualHours ?? null,
|
|
844
|
-
newTask.billableHours ?? null,
|
|
845
|
-
newTask.estimatedDurationMinutes ?? null,
|
|
846
|
-
newTask.actualDurationMinutes ?? null,
|
|
847
|
-
newTask.billableDurationMinutes ?? null,
|
|
848
|
-
newTask.actualDurationSeconds ?? null,
|
|
849
|
-
newTask.inProgressSince ?? null,
|
|
850
|
-
newTask.blockedDurationSeconds ?? null,
|
|
851
|
-
newTask.blockedSince ?? null,
|
|
852
|
-
newTask.progress ?? null,
|
|
853
|
-
newTask.isBlocked !== undefined ? (newTask.isBlocked ? 1 : 0) : null,
|
|
854
|
-
newTask.blockedReason ?? null,
|
|
855
|
-
JSON.stringify(newTask.tags || []),
|
|
856
|
-
JSON.stringify(newTask.customFields || {}),
|
|
857
|
-
newTask.parentId || null,
|
|
858
|
-
newTask.createdAt,
|
|
859
|
-
newTask.updatedAt
|
|
860
|
-
);
|
|
861
|
-
this.writeExtra('tasks', newTask);
|
|
862
|
-
return newTask;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
async updateTask(id: string, updates: Partial<Task>): Promise<Task | null> {
|
|
866
|
-
const existing = await this.getTask(id);
|
|
867
|
-
if (!existing) return null;
|
|
868
|
-
|
|
869
|
-
const updated: Task = {
|
|
870
|
-
...existing,
|
|
871
|
-
...updates,
|
|
872
|
-
updatedAt: new Date().toISOString()
|
|
873
|
-
};
|
|
874
|
-
|
|
875
|
-
const stmt = this.db.prepare(`
|
|
876
|
-
UPDATE tasks SET
|
|
877
|
-
projectId = ?, title = ?, description = ?, status = ?, priority = ?, taskType = ?,
|
|
878
|
-
assigneeId = ?, assignees = ?, reporterId = ?, reviewerId = ?, iterationId = ?, teamId = ?, containerId = ?, deliverableId = ?,
|
|
879
|
-
plannedStartDate = ?, actualStartDate = ?, actualEndDate = ?, completedAt = ?, dueDate = ?,
|
|
880
|
-
estimatedHours = ?, loggedHours = ?, actualHours = ?, billableHours = ?,
|
|
881
|
-
estimatedDurationMinutes = ?, actualDurationMinutes = ?, billableDurationMinutes = ?,
|
|
882
|
-
actualDurationSeconds = ?, inProgressSince = ?, blockedDurationSeconds = ?, blockedSince = ?, progress = ?,
|
|
883
|
-
isBlocked = ?, blockedReason = ?,
|
|
884
|
-
tags = ?, customFields = ?, parentId = ?, updatedAt = ?
|
|
885
|
-
WHERE id = ?
|
|
886
|
-
`);
|
|
887
|
-
stmt.run(
|
|
888
|
-
updated.projectId,
|
|
889
|
-
updated.title,
|
|
890
|
-
updated.description || null,
|
|
891
|
-
updated.status,
|
|
892
|
-
updated.priority,
|
|
893
|
-
updated.taskType || null,
|
|
894
|
-
updated.assigneeId || null,
|
|
895
|
-
updated.assignees ? JSON.stringify(updated.assignees) : null,
|
|
896
|
-
updated.reporterId || null,
|
|
897
|
-
updated.reviewerId || null,
|
|
898
|
-
updated.iterationId || null,
|
|
899
|
-
updated.teamId || null,
|
|
900
|
-
updated.containerId || null,
|
|
901
|
-
updated.deliverableId || null,
|
|
902
|
-
updated.plannedStartDate || null,
|
|
903
|
-
updated.actualStartDate || null,
|
|
904
|
-
updated.actualEndDate || null,
|
|
905
|
-
updated.completedAt || null,
|
|
906
|
-
updated.dueDate || null,
|
|
907
|
-
updated.estimatedHours ?? null,
|
|
908
|
-
updated.loggedHours ?? null,
|
|
909
|
-
updated.actualHours ?? null,
|
|
910
|
-
updated.billableHours ?? null,
|
|
911
|
-
updated.estimatedDurationMinutes ?? null,
|
|
912
|
-
updated.actualDurationMinutes ?? null,
|
|
913
|
-
updated.billableDurationMinutes ?? null,
|
|
914
|
-
updated.actualDurationSeconds ?? null,
|
|
915
|
-
updated.inProgressSince ?? null,
|
|
916
|
-
updated.blockedDurationSeconds ?? null,
|
|
917
|
-
updated.blockedSince ?? null,
|
|
918
|
-
updated.progress ?? null,
|
|
919
|
-
updated.isBlocked !== undefined ? (updated.isBlocked ? 1 : 0) : null,
|
|
920
|
-
updated.blockedReason ?? null,
|
|
921
|
-
JSON.stringify(updated.tags || []),
|
|
922
|
-
JSON.stringify(updated.customFields || {}),
|
|
923
|
-
updated.parentId || null,
|
|
924
|
-
updated.updatedAt,
|
|
925
|
-
id
|
|
926
|
-
);
|
|
927
|
-
this.writeExtra('tasks', updated);
|
|
928
|
-
return updated;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
async deleteTask(id: string): Promise<boolean> {
|
|
932
|
-
const stmt = this.db.prepare('DELETE FROM tasks WHERE id = ?');
|
|
933
|
-
const result = stmt.run(id);
|
|
934
|
-
return (result.changes ?? 0) > 0;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// --- Iterations ---
|
|
938
|
-
async getIterations(projectId: string): Promise<Iteration[]> {
|
|
939
|
-
const stmt = this.db.prepare('SELECT * FROM iterations WHERE projectId = ?');
|
|
940
|
-
const rows = stmt.all(projectId) as any[];
|
|
941
|
-
return rows.map(dropNulls);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
async getIteration(id: string): Promise<Iteration | null> {
|
|
945
|
-
const stmt = this.db.prepare('SELECT * FROM iterations WHERE id = ?');
|
|
946
|
-
const row = stmt.get(id) as any;
|
|
947
|
-
return row ? dropNulls(row) : null;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
async createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration> {
|
|
951
|
-
const id = `iter_${crypto.randomUUID()}`;
|
|
952
|
-
const now = new Date().toISOString();
|
|
953
|
-
const newIteration: Iteration = { ...iteration, id, createdAt: now };
|
|
954
|
-
|
|
955
|
-
const stmt = this.db.prepare(`
|
|
956
|
-
INSERT INTO iterations (id, projectId, name, goal, type, startDate, endDate, status, createdAt)
|
|
957
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
958
|
-
`);
|
|
959
|
-
stmt.run(
|
|
960
|
-
newIteration.id,
|
|
961
|
-
newIteration.projectId,
|
|
962
|
-
newIteration.name,
|
|
963
|
-
newIteration.goal || null,
|
|
964
|
-
newIteration.type || null,
|
|
965
|
-
newIteration.startDate || null,
|
|
966
|
-
newIteration.endDate || null,
|
|
967
|
-
newIteration.status,
|
|
968
|
-
newIteration.createdAt
|
|
969
|
-
);
|
|
970
|
-
return newIteration;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
async updateIteration(id: string, updates: Partial<Iteration>): Promise<Iteration | null> {
|
|
974
|
-
const existing = await this.getIteration(id);
|
|
975
|
-
if (!existing) return null;
|
|
976
|
-
|
|
977
|
-
const updated: Iteration = { ...existing, ...updates };
|
|
978
|
-
|
|
979
|
-
const updateStmt = this.db.prepare(`
|
|
980
|
-
UPDATE iterations SET name = ?, goal = ?, type = ?, startDate = ?, endDate = ?, status = ?
|
|
981
|
-
WHERE id = ?
|
|
982
|
-
`);
|
|
983
|
-
updateStmt.run(
|
|
984
|
-
updated.name,
|
|
985
|
-
updated.goal || null,
|
|
986
|
-
updated.type || null,
|
|
987
|
-
updated.startDate || null,
|
|
988
|
-
updated.endDate || null,
|
|
989
|
-
updated.status,
|
|
990
|
-
id
|
|
991
|
-
);
|
|
992
|
-
return updated;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
async deleteIteration(id: string): Promise<boolean> {
|
|
996
|
-
const stmt = this.db.prepare('DELETE FROM iterations WHERE id = ?');
|
|
997
|
-
const result = stmt.run(id);
|
|
998
|
-
return (result.changes ?? 0) > 0;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// --- Comments ---
|
|
1002
|
-
private mapComment(row: any): Comment {
|
|
1003
|
-
return dropNulls({
|
|
1004
|
-
id: row.id,
|
|
1005
|
-
taskId: row.taskId,
|
|
1006
|
-
authorId: row.authorId,
|
|
1007
|
-
authorType: row.authorType,
|
|
1008
|
-
parentId: row.parentId || undefined,
|
|
1009
|
-
content: row.content,
|
|
1010
|
-
mentions: row.mentions ? JSON.parse(row.mentions) : undefined,
|
|
1011
|
-
reactions: row.reactions ? JSON.parse(row.reactions) : undefined,
|
|
1012
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
1013
|
-
createdAt: row.createdAt,
|
|
1014
|
-
updatedAt: row.updatedAt
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
async getComments(taskId: string): Promise<Comment[]> {
|
|
1019
|
-
const stmt = this.db.prepare('SELECT * FROM comments WHERE taskId = ? ORDER BY createdAt ASC');
|
|
1020
|
-
const rows = stmt.all(taskId) as any[];
|
|
1021
|
-
return rows.map((r) => this.mapComment(r));
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
async getComment(id: string): Promise<Comment | null> {
|
|
1025
|
-
const stmt = this.db.prepare('SELECT * FROM comments WHERE id = ?');
|
|
1026
|
-
const row = stmt.get(id) as any;
|
|
1027
|
-
if (!row) return null;
|
|
1028
|
-
return this.mapComment(row);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
|
|
1032
|
-
const id = `cmt_${crypto.randomUUID()}`;
|
|
1033
|
-
const now = new Date().toISOString();
|
|
1034
|
-
const newComment: Comment = { ...comment, id, createdAt: now, updatedAt: now };
|
|
1035
|
-
|
|
1036
|
-
const stmt = this.db.prepare(`
|
|
1037
|
-
INSERT INTO comments (id, taskId, authorId, authorType, parentId, content, mentions, reactions, metadata, createdAt, updatedAt)
|
|
1038
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1039
|
-
`);
|
|
1040
|
-
stmt.run(
|
|
1041
|
-
newComment.id,
|
|
1042
|
-
newComment.taskId,
|
|
1043
|
-
newComment.authorId,
|
|
1044
|
-
newComment.authorType || 'user',
|
|
1045
|
-
newComment.parentId || null,
|
|
1046
|
-
newComment.content,
|
|
1047
|
-
newComment.mentions ? JSON.stringify(newComment.mentions) : null,
|
|
1048
|
-
newComment.reactions ? JSON.stringify(newComment.reactions) : null,
|
|
1049
|
-
newComment.metadata ? JSON.stringify(newComment.metadata) : null,
|
|
1050
|
-
newComment.createdAt,
|
|
1051
|
-
newComment.updatedAt
|
|
1052
|
-
);
|
|
1053
|
-
return newComment;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
async updateComment(id: string, updates: Partial<Comment>): Promise<Comment | null> {
|
|
1057
|
-
const existing = await this.getComment(id);
|
|
1058
|
-
if (!existing) return null;
|
|
1059
|
-
|
|
1060
|
-
const updated: Comment = {
|
|
1061
|
-
...existing,
|
|
1062
|
-
...updates,
|
|
1063
|
-
updatedAt: new Date().toISOString()
|
|
1064
|
-
};
|
|
1065
|
-
|
|
1066
|
-
const stmt = this.db.prepare(`
|
|
1067
|
-
UPDATE comments SET
|
|
1068
|
-
authorId = ?,
|
|
1069
|
-
authorType = ?,
|
|
1070
|
-
parentId = ?,
|
|
1071
|
-
content = ?,
|
|
1072
|
-
mentions = ?,
|
|
1073
|
-
reactions = ?,
|
|
1074
|
-
metadata = ?,
|
|
1075
|
-
updatedAt = ?
|
|
1076
|
-
WHERE id = ?
|
|
1077
|
-
`);
|
|
1078
|
-
stmt.run(
|
|
1079
|
-
updated.authorId,
|
|
1080
|
-
updated.authorType || 'user',
|
|
1081
|
-
updated.parentId || null,
|
|
1082
|
-
updated.content,
|
|
1083
|
-
updated.mentions ? JSON.stringify(updated.mentions) : null,
|
|
1084
|
-
updated.reactions ? JSON.stringify(updated.reactions) : null,
|
|
1085
|
-
updated.metadata ? JSON.stringify(updated.metadata) : null,
|
|
1086
|
-
updated.updatedAt,
|
|
1087
|
-
id
|
|
1088
|
-
);
|
|
1089
|
-
return updated;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
async deleteComment(id: string): Promise<boolean> {
|
|
1093
|
-
const stmt = this.db.prepare('DELETE FROM comments WHERE id = ?');
|
|
1094
|
-
const result = stmt.run(id);
|
|
1095
|
-
return (result.changes ?? 0) > 0;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
async addReaction(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null> {
|
|
1099
|
-
const existing = await this.getComment(commentId);
|
|
1100
|
-
if (!existing) return null;
|
|
1101
|
-
const reactions = existing.reactions ? [...existing.reactions] : [];
|
|
1102
|
-
const alreadyExists = reactions.some((r) => r.emoji === reaction.emoji && r.userId === reaction.userId);
|
|
1103
|
-
if (!alreadyExists) {
|
|
1104
|
-
reactions.push({
|
|
1105
|
-
emoji: reaction.emoji,
|
|
1106
|
-
userId: reaction.userId,
|
|
1107
|
-
createdAt: new Date().toISOString()
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
return this.updateComment(commentId, { reactions });
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
async removeReaction(commentId: string, reaction: { emoji: string; userId: string }): Promise<Comment | null> {
|
|
1114
|
-
const existing = await this.getComment(commentId);
|
|
1115
|
-
if (!existing) return null;
|
|
1116
|
-
const reactions = (existing.reactions || []).filter(
|
|
1117
|
-
(r) => !(r.emoji === reaction.emoji && r.userId === reaction.userId)
|
|
1118
|
-
);
|
|
1119
|
-
return this.updateComment(commentId, { reactions });
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
// --- Attachments ---
|
|
1123
|
-
async getAttachments(filter?: { taskId?: string; projectId?: string; commentId?: string }): Promise<Attachment[]> {
|
|
1124
|
-
let sql = 'SELECT * FROM attachments';
|
|
1125
|
-
const params: any[] = [];
|
|
1126
|
-
const conditions: string[] = [];
|
|
1127
|
-
|
|
1128
|
-
if (filter?.taskId) {
|
|
1129
|
-
conditions.push('taskId = ?');
|
|
1130
|
-
params.push(filter.taskId);
|
|
1131
|
-
}
|
|
1132
|
-
if (filter?.projectId) {
|
|
1133
|
-
conditions.push('projectId = ?');
|
|
1134
|
-
params.push(filter.projectId);
|
|
1135
|
-
}
|
|
1136
|
-
if (filter?.commentId) {
|
|
1137
|
-
conditions.push('commentId = ?');
|
|
1138
|
-
params.push(filter.commentId);
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
if (conditions.length > 0) {
|
|
1142
|
-
sql += ' WHERE ' + conditions.join(' AND ');
|
|
1143
|
-
}
|
|
1144
|
-
sql += ' ORDER BY createdAt DESC';
|
|
1145
|
-
|
|
1146
|
-
const stmt = this.db.prepare(sql);
|
|
1147
|
-
const rows = stmt.all(...params) as any[];
|
|
1148
|
-
return rows.map((r) =>
|
|
1149
|
-
dropNulls({
|
|
1150
|
-
...this.expandExtra(r),
|
|
1151
|
-
sizeBytes: Number(r.sizeBytes),
|
|
1152
|
-
metadata: r.metadata ? JSON.parse(r.metadata) : undefined
|
|
1153
|
-
})
|
|
1154
|
-
);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
async getAttachment(id: string): Promise<Attachment | null> {
|
|
1158
|
-
const stmt = this.db.prepare('SELECT * FROM attachments WHERE id = ?');
|
|
1159
|
-
const row = stmt.get(id) as any;
|
|
1160
|
-
if (!row) return null;
|
|
1161
|
-
return dropNulls({
|
|
1162
|
-
...this.expandExtra(row),
|
|
1163
|
-
sizeBytes: Number(row.sizeBytes),
|
|
1164
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : undefined
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
async createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment> {
|
|
1169
|
-
const id = `att_${crypto.randomUUID()}`;
|
|
1170
|
-
const now = new Date().toISOString();
|
|
1171
|
-
const newAtt: Attachment = {
|
|
1172
|
-
...attachment,
|
|
1173
|
-
id,
|
|
1174
|
-
createdAt: now,
|
|
1175
|
-
updatedAt: now
|
|
1176
|
-
};
|
|
1177
|
-
|
|
1178
|
-
const stmt = this.db.prepare(`
|
|
1179
|
-
INSERT INTO attachments (id, taskId, projectId, commentId, uploaderId, uploaderType, filename, mimeType, sizeBytes, url, storageKey, metadata, createdAt, updatedAt)
|
|
1180
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1181
|
-
`);
|
|
1182
|
-
stmt.run(
|
|
1183
|
-
newAtt.id,
|
|
1184
|
-
newAtt.taskId || null,
|
|
1185
|
-
newAtt.projectId || null,
|
|
1186
|
-
newAtt.commentId || null,
|
|
1187
|
-
newAtt.uploaderId,
|
|
1188
|
-
newAtt.uploaderType || 'user',
|
|
1189
|
-
newAtt.filename,
|
|
1190
|
-
newAtt.mimeType,
|
|
1191
|
-
newAtt.sizeBytes,
|
|
1192
|
-
newAtt.url,
|
|
1193
|
-
newAtt.storageKey || null,
|
|
1194
|
-
newAtt.metadata ? JSON.stringify(newAtt.metadata) : null,
|
|
1195
|
-
newAtt.createdAt,
|
|
1196
|
-
newAtt.updatedAt
|
|
1197
|
-
);
|
|
1198
|
-
this.writeExtra('attachments', newAtt);
|
|
1199
|
-
return newAtt;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
async deleteAttachment(id: string): Promise<boolean> {
|
|
1203
|
-
const stmt = this.db.prepare('DELETE FROM attachments WHERE id = ?');
|
|
1204
|
-
const result = stmt.run(id);
|
|
1205
|
-
return (result.changes ?? 0) > 0;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
async getActivities(filter?: { projectId?: string; taskId?: string }): Promise<Activity[]> {
|
|
1209
|
-
let sql = 'SELECT * FROM activities';
|
|
1210
|
-
const params: any[] = [];
|
|
1211
|
-
const conditions: string[] = [];
|
|
1212
|
-
|
|
1213
|
-
if (filter?.projectId) {
|
|
1214
|
-
conditions.push('projectId = ?');
|
|
1215
|
-
params.push(filter.projectId);
|
|
1216
|
-
}
|
|
1217
|
-
if (filter?.taskId) {
|
|
1218
|
-
conditions.push('taskId = ?');
|
|
1219
|
-
params.push(filter.taskId);
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
if (conditions.length > 0) {
|
|
1223
|
-
sql += ' WHERE ' + conditions.join(' AND ');
|
|
1224
|
-
}
|
|
1225
|
-
sql += ' ORDER BY createdAt DESC';
|
|
1226
|
-
|
|
1227
|
-
const stmt = this.db.prepare(sql);
|
|
1228
|
-
const rows = stmt.all(...params) as any[];
|
|
1229
|
-
return rows.map((r) =>
|
|
1230
|
-
dropNulls({
|
|
1231
|
-
...r,
|
|
1232
|
-
details: r.details ? JSON.parse(r.details) : undefined
|
|
1233
|
-
})
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
async queryActivities(query: ActivityQuery): Promise<Page<Activity>> {
|
|
1238
|
-
const where: string[] = [];
|
|
1239
|
-
const params: Array<string | number> = [];
|
|
1240
|
-
if (query.projectId) {
|
|
1241
|
-
where.push('projectId = ?');
|
|
1242
|
-
params.push(query.projectId);
|
|
1243
|
-
}
|
|
1244
|
-
if (query.projectIds) {
|
|
1245
|
-
if (query.projectIds.length === 0) return { items: [] };
|
|
1246
|
-
where.push(`projectId IN (${query.projectIds.map(() => '?').join(', ')})`);
|
|
1247
|
-
params.push(...query.projectIds);
|
|
1248
|
-
}
|
|
1249
|
-
if (query.taskId) {
|
|
1250
|
-
where.push('taskId = ?');
|
|
1251
|
-
params.push(query.taskId);
|
|
1252
|
-
}
|
|
1253
|
-
const after = decodeCursor(query.cursor);
|
|
1254
|
-
if (after) {
|
|
1255
|
-
where.push('(createdAt < ? OR (createdAt = ? AND id < ?))');
|
|
1256
|
-
params.push(after[0], after[0], after[1]);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
const size = pageSize(query.limit);
|
|
1260
|
-
const sql = `SELECT * FROM activities ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY createdAt DESC, id DESC LIMIT ?`;
|
|
1261
|
-
const rows = this.db.prepare(sql).all(...params, size + 1) as any[];
|
|
1262
|
-
const items: Activity[] = rows
|
|
1263
|
-
.slice(0, size)
|
|
1264
|
-
.map((r) => dropNulls({ ...r, details: r.details ? JSON.parse(r.details) : undefined }));
|
|
1265
|
-
const last = items[items.length - 1];
|
|
1266
|
-
return { items, nextCursor: rows.length > size && last ? encodeCursor(last.createdAt, last.id) : undefined };
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
async logActivity(activity: Omit<Activity, 'id' | 'createdAt'>): Promise<Activity> {
|
|
1270
|
-
const id = `act_${crypto.randomUUID()}`;
|
|
1271
|
-
const now = new Date().toISOString();
|
|
1272
|
-
const newAct: Activity = { ...activity, id, createdAt: now };
|
|
1273
|
-
|
|
1274
|
-
const stmt = this.db.prepare(`
|
|
1275
|
-
INSERT INTO activities (id, projectId, taskId, actorId, action, details, createdAt)
|
|
1276
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1277
|
-
`);
|
|
1278
|
-
stmt.run(
|
|
1279
|
-
newAct.id,
|
|
1280
|
-
newAct.projectId || null,
|
|
1281
|
-
newAct.taskId || null,
|
|
1282
|
-
newAct.actorId,
|
|
1283
|
-
newAct.action,
|
|
1284
|
-
newAct.details ? JSON.stringify(newAct.details) : null,
|
|
1285
|
-
newAct.createdAt
|
|
1286
|
-
);
|
|
1287
|
-
return newAct;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// --- Time Tracking ---
|
|
1291
|
-
async getTimeEntries(taskId: string): Promise<TimeEntry[]> {
|
|
1292
|
-
const stmt = this.db.prepare('SELECT * FROM time_entries WHERE taskId = ?');
|
|
1293
|
-
const rows = stmt.all(taskId) as any[];
|
|
1294
|
-
return rows.map((r) => dropNulls({ ...r, isBillable: r.isBillable !== null ? Boolean(r.isBillable) : undefined }));
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
async logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt'> & { loggedAt?: string }): Promise<TimeEntry> {
|
|
1298
|
-
const id = `time_${crypto.randomUUID()}`;
|
|
1299
|
-
const now = entry.loggedAt || new Date().toISOString();
|
|
1300
|
-
const newEntry: TimeEntry = { ...entry, id, loggedAt: now };
|
|
1301
|
-
|
|
1302
|
-
const stmt = this.db.prepare(`
|
|
1303
|
-
INSERT INTO time_entries (id, taskId, userId, hours, isBillable, description, loggedAt)
|
|
1304
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1305
|
-
`);
|
|
1306
|
-
stmt.run(
|
|
1307
|
-
newEntry.id,
|
|
1308
|
-
newEntry.taskId,
|
|
1309
|
-
newEntry.userId,
|
|
1310
|
-
newEntry.hours,
|
|
1311
|
-
newEntry.isBillable !== undefined ? (newEntry.isBillable ? 1 : 0) : null,
|
|
1312
|
-
newEntry.description || null,
|
|
1313
|
-
newEntry.loggedAt
|
|
1314
|
-
);
|
|
1315
|
-
return newEntry;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// --- Dependencies ---
|
|
1319
|
-
async getDependencies(taskId: string): Promise<TaskDependency[]> {
|
|
1320
|
-
const stmt = this.db.prepare('SELECT * FROM dependencies WHERE taskId = ? OR dependsOnTaskId = ?');
|
|
1321
|
-
return (stmt.all(taskId, taskId) as any[]).map(dropNulls);
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
async addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency> {
|
|
1325
|
-
const id = `dep_${crypto.randomUUID()}`;
|
|
1326
|
-
const newDep: TaskDependency = { ...dep, id };
|
|
1327
|
-
|
|
1328
|
-
const stmt = this.db.prepare(`
|
|
1329
|
-
INSERT INTO dependencies (id, taskId, dependsOnTaskId, type)
|
|
1330
|
-
VALUES (?, ?, ?, ?)
|
|
1331
|
-
`);
|
|
1332
|
-
stmt.run(newDep.id, newDep.taskId, newDep.dependsOnTaskId, newDep.type);
|
|
1333
|
-
return newDep;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
async getDependency(id: string): Promise<TaskDependency | null> {
|
|
1337
|
-
const row = this.db.prepare('SELECT * FROM dependencies WHERE id = ?').get(id) as any;
|
|
1338
|
-
return row ? { id: row.id, taskId: row.taskId, dependsOnTaskId: row.dependsOnTaskId, type: row.type } : null;
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
async removeDependency(id: string): Promise<boolean> {
|
|
1342
|
-
return Number(this.db.prepare('DELETE FROM dependencies WHERE id = ?').run(id).changes) > 0;
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
async deleteTimeEntry(id: string): Promise<boolean> {
|
|
1346
|
-
return Number(this.db.prepare('DELETE FROM time_entries WHERE id = ?').run(id).changes) > 0;
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
// --- Webhooks ---
|
|
1350
|
-
async getWebhooks(): Promise<Webhook[]> {
|
|
1351
|
-
const stmt = this.db.prepare('SELECT * FROM webhooks');
|
|
1352
|
-
const rows = stmt.all() as any[];
|
|
1353
|
-
return rows.map((r) => this.mapWebhook(r));
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
async getWebhook(id: string): Promise<Webhook | null> {
|
|
1357
|
-
const row = this.db.prepare('SELECT * FROM webhooks WHERE id = ?').get(id) as any;
|
|
1358
|
-
return row ? this.mapWebhook(row) : null;
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
async updateWebhook(id: string, updates: Partial<Omit<Webhook, 'id' | 'createdAt'>>): Promise<Webhook | null> {
|
|
1362
|
-
const existing = await this.getWebhook(id);
|
|
1363
|
-
if (!existing) return null;
|
|
1364
|
-
const updated: Webhook = { ...existing, ...updates, id, createdAt: existing.createdAt };
|
|
1365
|
-
this.db
|
|
1366
|
-
.prepare('UPDATE webhooks SET name = ?, url = ?, events = ?, secret = ?, active = ?, tenantId = ? WHERE id = ?')
|
|
1367
|
-
.run(
|
|
1368
|
-
updated.name ?? null,
|
|
1369
|
-
updated.url,
|
|
1370
|
-
JSON.stringify(updated.events),
|
|
1371
|
-
updated.secret ?? null,
|
|
1372
|
-
updated.active ? 1 : 0,
|
|
1373
|
-
updated.tenantId ?? null,
|
|
1374
|
-
id
|
|
1375
|
-
);
|
|
1376
|
-
return updated;
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
async deleteWebhook(id: string): Promise<boolean> {
|
|
1380
|
-
const result = this.db.prepare('DELETE FROM webhooks WHERE id = ?').run(id);
|
|
1381
|
-
return Number(result.changes) > 0;
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
private mapWebhook(row: any): Webhook {
|
|
1385
|
-
return dropNulls({
|
|
1386
|
-
id: row.id,
|
|
1387
|
-
name: row.name ?? '',
|
|
1388
|
-
url: row.url,
|
|
1389
|
-
events: JSON.parse(row.events),
|
|
1390
|
-
secret: row.secret ?? undefined,
|
|
1391
|
-
active: Boolean(row.active),
|
|
1392
|
-
tenantId: row.tenantId ?? undefined,
|
|
1393
|
-
createdAt: row.createdAt
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
async addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook> {
|
|
1398
|
-
const id = `wh_${crypto.randomUUID()}`;
|
|
1399
|
-
const now = new Date().toISOString();
|
|
1400
|
-
const newWh: Webhook = { ...webhook, id, createdAt: now };
|
|
1401
|
-
|
|
1402
|
-
const stmt = this.db.prepare(`
|
|
1403
|
-
INSERT INTO webhooks (id, name, url, events, secret, active, tenantId, createdAt)
|
|
1404
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1405
|
-
`);
|
|
1406
|
-
stmt.run(
|
|
1407
|
-
newWh.id,
|
|
1408
|
-
newWh.name ?? null,
|
|
1409
|
-
newWh.url,
|
|
1410
|
-
JSON.stringify(newWh.events),
|
|
1411
|
-
newWh.secret || null,
|
|
1412
|
-
newWh.active ? 1 : 0,
|
|
1413
|
-
newWh.tenantId ?? null,
|
|
1414
|
-
newWh.createdAt
|
|
1415
|
-
);
|
|
1416
|
-
return newWh;
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
// Helper mappers
|
|
1420
|
-
private mapProject(row: any): Project {
|
|
1421
|
-
return dropNulls({
|
|
1422
|
-
...this.expandExtra(row),
|
|
1423
|
-
tenantId: row.tenantId ?? undefined,
|
|
1424
|
-
workflowId: row.workflowId || undefined,
|
|
1425
|
-
taskTypes: row.taskTypes ? JSON.parse(row.taskTypes) : undefined,
|
|
1426
|
-
members: row.members ? JSON.parse(row.members) : [],
|
|
1427
|
-
teamIds: row.teamIds ? JSON.parse(row.teamIds) : [],
|
|
1428
|
-
statusDefinitions: row.statusDefinitions ? JSON.parse(row.statusDefinitions) : [],
|
|
1429
|
-
priorityDefinitions: row.priorityDefinitions ? JSON.parse(row.priorityDefinitions) : [],
|
|
1430
|
-
customFieldDefinitions: row.customFieldDefinitions ? JSON.parse(row.customFieldDefinitions) : []
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
private mapWorkflow(row: any): Workflow {
|
|
1435
|
-
return dropNulls({
|
|
1436
|
-
...row,
|
|
1437
|
-
tenantId: row.tenantId ?? undefined,
|
|
1438
|
-
statuses: row.statuses ? JSON.parse(row.statuses) : [],
|
|
1439
|
-
transitions: row.transitions ? JSON.parse(row.transitions) : [],
|
|
1440
|
-
taskTypes: row.taskTypes ? JSON.parse(row.taskTypes) : undefined,
|
|
1441
|
-
isDefault: row.isDefault !== null && row.isDefault !== undefined ? Boolean(row.isDefault) : undefined
|
|
1442
|
-
});
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
private mapTeam(row: any): Team {
|
|
1446
|
-
return dropNulls({
|
|
1447
|
-
...this.expandExtra(row),
|
|
1448
|
-
tenantId: row.tenantId ?? undefined,
|
|
1449
|
-
memberIds: row.memberIds ? JSON.parse(row.memberIds) : []
|
|
1450
|
-
});
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
private mapTask(row: any): Task {
|
|
1454
|
-
return dropNulls({
|
|
1455
|
-
...this.expandExtra(row),
|
|
1456
|
-
actualDurationSeconds: row.actualDurationSeconds ?? undefined,
|
|
1457
|
-
inProgressSince: row.inProgressSince || undefined,
|
|
1458
|
-
blockedDurationSeconds: row.blockedDurationSeconds ?? undefined,
|
|
1459
|
-
blockedSince: row.blockedSince || undefined,
|
|
1460
|
-
actualEndDate: row.actualEndDate || undefined,
|
|
1461
|
-
completedAt: row.completedAt || undefined,
|
|
1462
|
-
isBlocked: row.isBlocked !== null && row.isBlocked !== undefined ? Boolean(row.isBlocked) : undefined,
|
|
1463
|
-
blockedReason: row.blockedReason || undefined,
|
|
1464
|
-
taskType: row.taskType || undefined,
|
|
1465
|
-
deliverableId: row.deliverableId || undefined,
|
|
1466
|
-
tags: row.tags ? JSON.parse(row.tags) : [],
|
|
1467
|
-
assignees: row.assignees ? JSON.parse(row.assignees) : undefined,
|
|
1468
|
-
customFields: row.customFields ? JSON.parse(row.customFields) : {}
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
private mapDeliverable(row: any): Deliverable {
|
|
1473
|
-
return dropNulls({
|
|
1474
|
-
...row,
|
|
1475
|
-
specs: row.specs ? JSON.parse(row.specs) : undefined,
|
|
1476
|
-
outputUrls: row.outputUrls ? JSON.parse(row.outputUrls) : [],
|
|
1477
|
-
customFields: row.customFields ? JSON.parse(row.customFields) : {}
|
|
1478
|
-
});
|
|
1479
|
-
}
|
|
1480
|
-
}
|