@agentuity/runtime 1.0.22 → 1.0.23
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/dist/_context.d.ts +4 -1
- package/dist/_context.d.ts.map +1 -1
- package/dist/_context.js +3 -0
- package/dist/_context.js.map +1 -1
- package/dist/_services.d.ts +4 -1
- package/dist/_services.d.ts.map +1 -1
- package/dist/_services.js +28 -3
- package/dist/_services.js.map +1 -1
- package/dist/_standalone.d.ts +4 -1
- package/dist/_standalone.d.ts.map +1 -1
- package/dist/_standalone.js +3 -0
- package/dist/_standalone.js.map +1 -1
- package/dist/agent.d.ts +43 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts +12 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js.map +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +6 -0
- package/dist/middleware.js.map +1 -1
- package/dist/services/local/_db.d.ts.map +1 -1
- package/dist/services/local/_db.js +49 -1
- package/dist/services/local/_db.js.map +1 -1
- package/dist/services/local/email.d.ts +20 -0
- package/dist/services/local/email.d.ts.map +1 -0
- package/dist/services/local/email.js +46 -0
- package/dist/services/local/email.js.map +1 -0
- package/dist/services/local/index.d.ts +2 -0
- package/dist/services/local/index.d.ts.map +1 -1
- package/dist/services/local/index.js +2 -0
- package/dist/services/local/index.js.map +1 -1
- package/dist/services/local/task.d.ts +16 -0
- package/dist/services/local/task.d.ts.map +1 -0
- package/dist/services/local/task.js +425 -0
- package/dist/services/local/task.js.map +1 -0
- package/package.json +7 -7
- package/src/_context.ts +6 -0
- package/src/_services.ts +33 -1
- package/src/_standalone.ts +6 -0
- package/src/agent.ts +48 -0
- package/src/app.ts +14 -0
- package/src/middleware.ts +7 -3
- package/src/services/local/_db.ts +64 -3
- package/src/services/local/email.ts +75 -0
- package/src/services/local/index.ts +2 -0
- package/src/services/local/task.ts +595 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import type {
|
|
3
|
+
TaskStorage,
|
|
4
|
+
Task,
|
|
5
|
+
TaskChangelogEntry,
|
|
6
|
+
TaskPriority,
|
|
7
|
+
TaskStatus,
|
|
8
|
+
TaskType,
|
|
9
|
+
CreateTaskParams,
|
|
10
|
+
UpdateTaskParams,
|
|
11
|
+
ListTasksParams,
|
|
12
|
+
ListTasksResult,
|
|
13
|
+
TaskChangelogResult,
|
|
14
|
+
} from '@agentuity/core';
|
|
15
|
+
import { StructuredError } from '@agentuity/core';
|
|
16
|
+
import { now } from './_util';
|
|
17
|
+
|
|
18
|
+
const TaskTitleRequiredError = StructuredError(
|
|
19
|
+
'TaskTitleRequiredError',
|
|
20
|
+
'Task title is required and must be a non-empty string'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const TaskNotFoundError = StructuredError('TaskNotFoundError', 'Task not found');
|
|
24
|
+
|
|
25
|
+
const TaskAlreadyClosedError = StructuredError('TaskAlreadyClosedError', 'Task is already closed');
|
|
26
|
+
|
|
27
|
+
type TaskRow = {
|
|
28
|
+
id: string;
|
|
29
|
+
created_at: number;
|
|
30
|
+
updated_at: number;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string | null;
|
|
33
|
+
metadata: string | null;
|
|
34
|
+
priority: TaskPriority;
|
|
35
|
+
parent_id: string | null;
|
|
36
|
+
type: TaskType;
|
|
37
|
+
status: TaskStatus;
|
|
38
|
+
open_date: string | null;
|
|
39
|
+
in_progress_date: string | null;
|
|
40
|
+
closed_date: string | null;
|
|
41
|
+
created_id: string;
|
|
42
|
+
assigned_id: string | null;
|
|
43
|
+
closed_id: string | null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type TaskChangelogRow = {
|
|
47
|
+
id: string;
|
|
48
|
+
created_at: number;
|
|
49
|
+
task_id: string;
|
|
50
|
+
field: string;
|
|
51
|
+
old_value: string | null;
|
|
52
|
+
new_value: string | null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const DEFAULT_LIMIT = 100;
|
|
56
|
+
|
|
57
|
+
const SORT_FIELDS: Record<string, string> = {
|
|
58
|
+
created_at: 'created_at',
|
|
59
|
+
updated_at: 'updated_at',
|
|
60
|
+
title: 'title',
|
|
61
|
+
priority: 'priority',
|
|
62
|
+
status: 'status',
|
|
63
|
+
type: 'type',
|
|
64
|
+
open_date: 'open_date',
|
|
65
|
+
in_progress_date: 'in_progress_date',
|
|
66
|
+
closed_date: 'closed_date',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function generateTaskId(): string {
|
|
70
|
+
return `task_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function generateChangelogId(): string {
|
|
74
|
+
return `taskch_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toTask(row: TaskRow): Task {
|
|
78
|
+
return {
|
|
79
|
+
id: row.id,
|
|
80
|
+
created_at: new Date(row.created_at).toISOString(),
|
|
81
|
+
updated_at: new Date(row.updated_at).toISOString(),
|
|
82
|
+
title: row.title,
|
|
83
|
+
description: row.description ?? undefined,
|
|
84
|
+
metadata: row.metadata ? (JSON.parse(row.metadata) as Record<string, unknown>) : undefined,
|
|
85
|
+
priority: row.priority,
|
|
86
|
+
parent_id: row.parent_id ?? undefined,
|
|
87
|
+
type: row.type,
|
|
88
|
+
status: row.status,
|
|
89
|
+
open_date: row.open_date ?? undefined,
|
|
90
|
+
in_progress_date: row.in_progress_date ?? undefined,
|
|
91
|
+
closed_date: row.closed_date ?? undefined,
|
|
92
|
+
created_id: row.created_id,
|
|
93
|
+
assigned_id: row.assigned_id ?? undefined,
|
|
94
|
+
closed_id: row.closed_id ?? undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toChangelogEntry(row: TaskChangelogRow): TaskChangelogEntry {
|
|
99
|
+
return {
|
|
100
|
+
id: row.id,
|
|
101
|
+
created_at: new Date(row.created_at).toISOString(),
|
|
102
|
+
task_id: row.task_id,
|
|
103
|
+
field: row.field,
|
|
104
|
+
old_value: row.old_value ?? undefined,
|
|
105
|
+
new_value: row.new_value ?? undefined,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class LocalTaskStorage implements TaskStorage {
|
|
110
|
+
#db: Database;
|
|
111
|
+
#projectPath: string;
|
|
112
|
+
|
|
113
|
+
constructor(db: Database, projectPath: string) {
|
|
114
|
+
this.#db = db;
|
|
115
|
+
this.#projectPath = projectPath;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async create(params: CreateTaskParams): Promise<Task> {
|
|
119
|
+
const trimmedTitle = params?.title?.trim();
|
|
120
|
+
if (!trimmedTitle) {
|
|
121
|
+
throw new TaskTitleRequiredError();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const id = generateTaskId();
|
|
125
|
+
const timestamp = now();
|
|
126
|
+
const status: TaskStatus = params.status ?? 'open';
|
|
127
|
+
const priority: TaskPriority = params.priority ?? 'none';
|
|
128
|
+
const openDate = status === 'open' ? new Date(timestamp).toISOString() : null;
|
|
129
|
+
const inProgressDate = status === 'in_progress' ? new Date(timestamp).toISOString() : null;
|
|
130
|
+
const closedDate = status === 'closed' ? new Date(timestamp).toISOString() : null;
|
|
131
|
+
|
|
132
|
+
const stmt = this.#db.prepare(`
|
|
133
|
+
INSERT INTO task_storage (
|
|
134
|
+
project_path,
|
|
135
|
+
id,
|
|
136
|
+
title,
|
|
137
|
+
description,
|
|
138
|
+
metadata,
|
|
139
|
+
priority,
|
|
140
|
+
parent_id,
|
|
141
|
+
type,
|
|
142
|
+
status,
|
|
143
|
+
open_date,
|
|
144
|
+
in_progress_date,
|
|
145
|
+
closed_date,
|
|
146
|
+
created_id,
|
|
147
|
+
assigned_id,
|
|
148
|
+
closed_id,
|
|
149
|
+
created_at,
|
|
150
|
+
updated_at
|
|
151
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
152
|
+
`);
|
|
153
|
+
|
|
154
|
+
const row: TaskRow = {
|
|
155
|
+
id,
|
|
156
|
+
created_at: timestamp,
|
|
157
|
+
updated_at: timestamp,
|
|
158
|
+
title: trimmedTitle,
|
|
159
|
+
description: params.description ?? null,
|
|
160
|
+
metadata: params.metadata ? JSON.stringify(params.metadata) : null,
|
|
161
|
+
priority,
|
|
162
|
+
parent_id: params.parent_id ?? null,
|
|
163
|
+
type: params.type,
|
|
164
|
+
status,
|
|
165
|
+
open_date: openDate,
|
|
166
|
+
in_progress_date: inProgressDate,
|
|
167
|
+
closed_date: closedDate,
|
|
168
|
+
created_id: params.created_id,
|
|
169
|
+
assigned_id: params.assigned_id ?? null,
|
|
170
|
+
closed_id: null,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
stmt.run(
|
|
174
|
+
this.#projectPath,
|
|
175
|
+
row.id,
|
|
176
|
+
row.title,
|
|
177
|
+
row.description,
|
|
178
|
+
row.metadata,
|
|
179
|
+
row.priority,
|
|
180
|
+
row.parent_id,
|
|
181
|
+
row.type,
|
|
182
|
+
row.status,
|
|
183
|
+
row.open_date,
|
|
184
|
+
row.in_progress_date,
|
|
185
|
+
row.closed_date,
|
|
186
|
+
row.created_id,
|
|
187
|
+
row.assigned_id,
|
|
188
|
+
row.closed_id,
|
|
189
|
+
row.created_at,
|
|
190
|
+
row.updated_at
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return toTask(row);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async get(id: string): Promise<Task | null> {
|
|
197
|
+
const query = this.#db.query(`
|
|
198
|
+
SELECT
|
|
199
|
+
id,
|
|
200
|
+
created_at,
|
|
201
|
+
updated_at,
|
|
202
|
+
title,
|
|
203
|
+
description,
|
|
204
|
+
metadata,
|
|
205
|
+
priority,
|
|
206
|
+
parent_id,
|
|
207
|
+
type,
|
|
208
|
+
status,
|
|
209
|
+
open_date,
|
|
210
|
+
in_progress_date,
|
|
211
|
+
closed_date,
|
|
212
|
+
created_id,
|
|
213
|
+
assigned_id,
|
|
214
|
+
closed_id
|
|
215
|
+
FROM task_storage
|
|
216
|
+
WHERE project_path = ? AND id = ?
|
|
217
|
+
`);
|
|
218
|
+
|
|
219
|
+
const row = query.get(this.#projectPath, id) as TaskRow | null;
|
|
220
|
+
if (!row) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return toTask(row);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async list(params?: ListTasksParams): Promise<ListTasksResult> {
|
|
228
|
+
const filters: string[] = ['project_path = ?'];
|
|
229
|
+
const values: Array<string | number> = [this.#projectPath];
|
|
230
|
+
|
|
231
|
+
if (params?.status) {
|
|
232
|
+
filters.push('status = ?');
|
|
233
|
+
values.push(params.status);
|
|
234
|
+
}
|
|
235
|
+
if (params?.type) {
|
|
236
|
+
filters.push('type = ?');
|
|
237
|
+
values.push(params.type);
|
|
238
|
+
}
|
|
239
|
+
if (params?.priority) {
|
|
240
|
+
filters.push('priority = ?');
|
|
241
|
+
values.push(params.priority);
|
|
242
|
+
}
|
|
243
|
+
if (params?.assigned_id) {
|
|
244
|
+
filters.push('assigned_id = ?');
|
|
245
|
+
values.push(params.assigned_id);
|
|
246
|
+
}
|
|
247
|
+
if (params?.parent_id) {
|
|
248
|
+
filters.push('parent_id = ?');
|
|
249
|
+
values.push(params.parent_id);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
|
|
253
|
+
const sortField =
|
|
254
|
+
params?.sort && SORT_FIELDS[params.sort] ? SORT_FIELDS[params.sort] : 'created_at';
|
|
255
|
+
const sortOrder = params?.order === 'asc' ? 'ASC' : 'DESC';
|
|
256
|
+
const limit = params?.limit ?? DEFAULT_LIMIT;
|
|
257
|
+
const offset = params?.offset ?? 0;
|
|
258
|
+
|
|
259
|
+
const totalQuery = this.#db.query(
|
|
260
|
+
`SELECT COUNT(*) as count FROM task_storage ${whereClause}`
|
|
261
|
+
);
|
|
262
|
+
const totalRow = totalQuery.get(...values) as { count: number };
|
|
263
|
+
|
|
264
|
+
const query = this.#db.query(`
|
|
265
|
+
SELECT
|
|
266
|
+
id,
|
|
267
|
+
created_at,
|
|
268
|
+
updated_at,
|
|
269
|
+
title,
|
|
270
|
+
description,
|
|
271
|
+
metadata,
|
|
272
|
+
priority,
|
|
273
|
+
parent_id,
|
|
274
|
+
type,
|
|
275
|
+
status,
|
|
276
|
+
open_date,
|
|
277
|
+
in_progress_date,
|
|
278
|
+
closed_date,
|
|
279
|
+
created_id,
|
|
280
|
+
assigned_id,
|
|
281
|
+
closed_id
|
|
282
|
+
FROM task_storage
|
|
283
|
+
${whereClause}
|
|
284
|
+
ORDER BY ${sortField} ${sortOrder}
|
|
285
|
+
LIMIT ? OFFSET ?
|
|
286
|
+
`);
|
|
287
|
+
|
|
288
|
+
const rows = query.all(...values, limit, offset) as TaskRow[];
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
tasks: rows.map(toTask),
|
|
292
|
+
total: totalRow.count,
|
|
293
|
+
limit,
|
|
294
|
+
offset,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async update(id: string, params: UpdateTaskParams): Promise<Task> {
|
|
299
|
+
const updateInTransaction = this.#db.transaction(() => {
|
|
300
|
+
const existingQuery = this.#db.query(`
|
|
301
|
+
SELECT
|
|
302
|
+
id,
|
|
303
|
+
created_at,
|
|
304
|
+
updated_at,
|
|
305
|
+
title,
|
|
306
|
+
description,
|
|
307
|
+
metadata,
|
|
308
|
+
priority,
|
|
309
|
+
parent_id,
|
|
310
|
+
type,
|
|
311
|
+
status,
|
|
312
|
+
open_date,
|
|
313
|
+
in_progress_date,
|
|
314
|
+
closed_date,
|
|
315
|
+
created_id,
|
|
316
|
+
assigned_id,
|
|
317
|
+
closed_id
|
|
318
|
+
FROM task_storage
|
|
319
|
+
WHERE project_path = ? AND id = ?
|
|
320
|
+
`);
|
|
321
|
+
|
|
322
|
+
const existing = existingQuery.get(this.#projectPath, id) as TaskRow | null;
|
|
323
|
+
if (!existing) {
|
|
324
|
+
throw new TaskNotFoundError();
|
|
325
|
+
}
|
|
326
|
+
const trimmedTitle = params.title !== undefined ? params.title?.trim() : undefined;
|
|
327
|
+
if (params.title !== undefined && !trimmedTitle) {
|
|
328
|
+
throw new TaskTitleRequiredError();
|
|
329
|
+
}
|
|
330
|
+
const timestamp = now();
|
|
331
|
+
const nowIso = new Date(timestamp).toISOString();
|
|
332
|
+
|
|
333
|
+
const updated: TaskRow = {
|
|
334
|
+
...existing,
|
|
335
|
+
title: trimmedTitle ?? existing.title,
|
|
336
|
+
description:
|
|
337
|
+
params.description !== undefined ? params.description : existing.description,
|
|
338
|
+
metadata:
|
|
339
|
+
params.metadata !== undefined
|
|
340
|
+
? params.metadata
|
|
341
|
+
? JSON.stringify(params.metadata)
|
|
342
|
+
: null
|
|
343
|
+
: existing.metadata,
|
|
344
|
+
priority: params.priority ?? existing.priority,
|
|
345
|
+
parent_id: params.parent_id !== undefined ? params.parent_id : existing.parent_id,
|
|
346
|
+
type: params.type ?? existing.type,
|
|
347
|
+
status: params.status ?? existing.status,
|
|
348
|
+
assigned_id:
|
|
349
|
+
params.assigned_id !== undefined ? params.assigned_id : existing.assigned_id,
|
|
350
|
+
closed_id: params.closed_id !== undefined ? params.closed_id : existing.closed_id,
|
|
351
|
+
updated_at: timestamp,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if (params.status && params.status !== existing.status) {
|
|
355
|
+
if (params.status === 'open' && !existing.open_date) {
|
|
356
|
+
updated.open_date = nowIso;
|
|
357
|
+
}
|
|
358
|
+
if (params.status === 'in_progress' && !existing.in_progress_date) {
|
|
359
|
+
updated.in_progress_date = nowIso;
|
|
360
|
+
}
|
|
361
|
+
if (params.status === 'closed' && !existing.closed_date) {
|
|
362
|
+
updated.closed_date = nowIso;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const changelogEntries: Array<{
|
|
367
|
+
field: string;
|
|
368
|
+
oldValue: string | null;
|
|
369
|
+
newValue: string | null;
|
|
370
|
+
}> = [];
|
|
371
|
+
|
|
372
|
+
const compare = (field: string, oldValue: string | null, newValue: string | null) => {
|
|
373
|
+
if (oldValue !== newValue) {
|
|
374
|
+
changelogEntries.push({ field, oldValue, newValue });
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
if (params.title !== undefined) {
|
|
379
|
+
compare('title', existing.title, updated.title);
|
|
380
|
+
}
|
|
381
|
+
if (params.description !== undefined) {
|
|
382
|
+
compare('description', existing.description, updated.description);
|
|
383
|
+
}
|
|
384
|
+
if (params.metadata !== undefined) {
|
|
385
|
+
compare('metadata', existing.metadata, updated.metadata);
|
|
386
|
+
}
|
|
387
|
+
if (params.priority !== undefined) {
|
|
388
|
+
compare('priority', existing.priority, updated.priority);
|
|
389
|
+
}
|
|
390
|
+
if (params.parent_id !== undefined) {
|
|
391
|
+
compare('parent_id', existing.parent_id, updated.parent_id);
|
|
392
|
+
}
|
|
393
|
+
if (params.type !== undefined) {
|
|
394
|
+
compare('type', existing.type, updated.type);
|
|
395
|
+
}
|
|
396
|
+
if (params.status !== undefined) {
|
|
397
|
+
compare('status', existing.status, updated.status);
|
|
398
|
+
}
|
|
399
|
+
if (params.assigned_id !== undefined) {
|
|
400
|
+
compare('assigned_id', existing.assigned_id, updated.assigned_id);
|
|
401
|
+
}
|
|
402
|
+
if (params.closed_id !== undefined) {
|
|
403
|
+
compare('closed_id', existing.closed_id, updated.closed_id);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const updateStmt = this.#db.prepare(`
|
|
407
|
+
UPDATE task_storage
|
|
408
|
+
SET
|
|
409
|
+
title = ?,
|
|
410
|
+
description = ?,
|
|
411
|
+
metadata = ?,
|
|
412
|
+
priority = ?,
|
|
413
|
+
parent_id = ?,
|
|
414
|
+
type = ?,
|
|
415
|
+
status = ?,
|
|
416
|
+
open_date = ?,
|
|
417
|
+
in_progress_date = ?,
|
|
418
|
+
closed_date = ?,
|
|
419
|
+
created_id = ?,
|
|
420
|
+
assigned_id = ?,
|
|
421
|
+
closed_id = ?,
|
|
422
|
+
updated_at = ?
|
|
423
|
+
WHERE project_path = ? AND id = ?
|
|
424
|
+
`);
|
|
425
|
+
|
|
426
|
+
updateStmt.run(
|
|
427
|
+
updated.title,
|
|
428
|
+
updated.description,
|
|
429
|
+
updated.metadata,
|
|
430
|
+
updated.priority,
|
|
431
|
+
updated.parent_id,
|
|
432
|
+
updated.type,
|
|
433
|
+
updated.status,
|
|
434
|
+
updated.open_date,
|
|
435
|
+
updated.in_progress_date,
|
|
436
|
+
updated.closed_date,
|
|
437
|
+
updated.created_id,
|
|
438
|
+
updated.assigned_id,
|
|
439
|
+
updated.closed_id,
|
|
440
|
+
updated.updated_at,
|
|
441
|
+
this.#projectPath,
|
|
442
|
+
id
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
if (changelogEntries.length > 0) {
|
|
446
|
+
const changelogStmt = this.#db.prepare(`
|
|
447
|
+
INSERT INTO task_changelog_storage (
|
|
448
|
+
project_path,
|
|
449
|
+
id,
|
|
450
|
+
task_id,
|
|
451
|
+
field,
|
|
452
|
+
old_value,
|
|
453
|
+
new_value,
|
|
454
|
+
created_at
|
|
455
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
456
|
+
`);
|
|
457
|
+
|
|
458
|
+
for (const entry of changelogEntries) {
|
|
459
|
+
changelogStmt.run(
|
|
460
|
+
this.#projectPath,
|
|
461
|
+
generateChangelogId(),
|
|
462
|
+
id,
|
|
463
|
+
entry.field,
|
|
464
|
+
entry.oldValue,
|
|
465
|
+
entry.newValue,
|
|
466
|
+
timestamp
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return toTask(updated);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
return updateInTransaction.immediate();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async close(id: string): Promise<Task> {
|
|
478
|
+
const closeInTransaction = this.#db.transaction(() => {
|
|
479
|
+
const existingQuery = this.#db.query(`
|
|
480
|
+
SELECT
|
|
481
|
+
id,
|
|
482
|
+
created_at,
|
|
483
|
+
updated_at,
|
|
484
|
+
title,
|
|
485
|
+
description,
|
|
486
|
+
metadata,
|
|
487
|
+
priority,
|
|
488
|
+
parent_id,
|
|
489
|
+
type,
|
|
490
|
+
status,
|
|
491
|
+
open_date,
|
|
492
|
+
in_progress_date,
|
|
493
|
+
closed_date,
|
|
494
|
+
created_id,
|
|
495
|
+
assigned_id,
|
|
496
|
+
closed_id
|
|
497
|
+
FROM task_storage
|
|
498
|
+
WHERE project_path = ? AND id = ?
|
|
499
|
+
`);
|
|
500
|
+
|
|
501
|
+
const existing = existingQuery.get(this.#projectPath, id) as TaskRow | null;
|
|
502
|
+
if (!existing) {
|
|
503
|
+
throw new TaskNotFoundError();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (existing.status === 'closed') {
|
|
507
|
+
throw new TaskAlreadyClosedError();
|
|
508
|
+
}
|
|
509
|
+
const timestamp = now();
|
|
510
|
+
const nowIso = new Date(timestamp).toISOString();
|
|
511
|
+
const updated: TaskRow = {
|
|
512
|
+
...existing,
|
|
513
|
+
status: 'closed',
|
|
514
|
+
closed_date: existing.closed_date ?? nowIso,
|
|
515
|
+
updated_at: timestamp,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const updateStmt = this.#db.prepare(`
|
|
519
|
+
UPDATE task_storage
|
|
520
|
+
SET status = ?, closed_date = ?, updated_at = ?
|
|
521
|
+
WHERE project_path = ? AND id = ?
|
|
522
|
+
`);
|
|
523
|
+
|
|
524
|
+
updateStmt.run(
|
|
525
|
+
updated.status,
|
|
526
|
+
updated.closed_date,
|
|
527
|
+
updated.updated_at,
|
|
528
|
+
this.#projectPath,
|
|
529
|
+
id
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const changelogStmt = this.#db.prepare(`
|
|
533
|
+
INSERT INTO task_changelog_storage (
|
|
534
|
+
project_path,
|
|
535
|
+
id,
|
|
536
|
+
task_id,
|
|
537
|
+
field,
|
|
538
|
+
old_value,
|
|
539
|
+
new_value,
|
|
540
|
+
created_at
|
|
541
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
542
|
+
`);
|
|
543
|
+
|
|
544
|
+
changelogStmt.run(
|
|
545
|
+
this.#projectPath,
|
|
546
|
+
generateChangelogId(),
|
|
547
|
+
id,
|
|
548
|
+
'status',
|
|
549
|
+
existing.status,
|
|
550
|
+
updated.status,
|
|
551
|
+
timestamp
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
return toTask(updated);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
return closeInTransaction.immediate();
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async changelog(
|
|
561
|
+
id: string,
|
|
562
|
+
params?: { limit?: number; offset?: number }
|
|
563
|
+
): Promise<TaskChangelogResult> {
|
|
564
|
+
const limit = params?.limit ?? DEFAULT_LIMIT;
|
|
565
|
+
const offset = params?.offset ?? 0;
|
|
566
|
+
|
|
567
|
+
const totalQuery = this.#db.query(
|
|
568
|
+
`SELECT COUNT(*) as count FROM task_changelog_storage WHERE project_path = ? AND task_id = ?`
|
|
569
|
+
);
|
|
570
|
+
const totalRow = totalQuery.get(this.#projectPath, id) as { count: number };
|
|
571
|
+
|
|
572
|
+
const query = this.#db.query(`
|
|
573
|
+
SELECT
|
|
574
|
+
id,
|
|
575
|
+
created_at,
|
|
576
|
+
task_id,
|
|
577
|
+
field,
|
|
578
|
+
old_value,
|
|
579
|
+
new_value
|
|
580
|
+
FROM task_changelog_storage
|
|
581
|
+
WHERE project_path = ? AND task_id = ?
|
|
582
|
+
ORDER BY created_at DESC
|
|
583
|
+
LIMIT ? OFFSET ?
|
|
584
|
+
`);
|
|
585
|
+
|
|
586
|
+
const rows = query.all(this.#projectPath, id, limit, offset) as TaskChangelogRow[];
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
changelog: rows.map(toChangelogEntry),
|
|
590
|
+
total: totalRow.count,
|
|
591
|
+
limit,
|
|
592
|
+
offset,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|