@allpepper/task-orchestrator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/package.json +51 -0
- package/src/db/client.ts +34 -0
- package/src/db/index.ts +1 -0
- package/src/db/migrate.ts +51 -0
- package/src/db/migrations/001_initial_schema.sql +160 -0
- package/src/domain/index.ts +1 -0
- package/src/domain/types.ts +225 -0
- package/src/index.ts +7 -0
- package/src/repos/base.ts +151 -0
- package/src/repos/dependencies.ts +356 -0
- package/src/repos/features.ts +507 -0
- package/src/repos/index.ts +4 -0
- package/src/repos/projects.ts +350 -0
- package/src/repos/sections.ts +505 -0
- package/src/repos/tags.example.ts +125 -0
- package/src/repos/tags.ts +175 -0
- package/src/repos/tasks.ts +581 -0
- package/src/repos/templates.ts +649 -0
- package/src/server.ts +121 -0
- package/src/services/index.ts +2 -0
- package/src/services/status-validator.ts +100 -0
- package/src/services/workflow.ts +104 -0
- package/src/tools/apply-template.ts +129 -0
- package/src/tools/get-blocked-tasks.ts +63 -0
- package/src/tools/get-next-status.ts +183 -0
- package/src/tools/get-next-task.ts +75 -0
- package/src/tools/get-tag-usage.ts +54 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-tags.ts +56 -0
- package/src/tools/manage-container.ts +333 -0
- package/src/tools/manage-dependency.ts +198 -0
- package/src/tools/manage-sections.ts +388 -0
- package/src/tools/manage-template.ts +313 -0
- package/src/tools/query-container.ts +296 -0
- package/src/tools/query-dependencies.ts +68 -0
- package/src/tools/query-sections.ts +70 -0
- package/src/tools/query-templates.ts +137 -0
- package/src/tools/query-workflow-state.ts +198 -0
- package/src/tools/registry.ts +180 -0
- package/src/tools/rename-tag.ts +64 -0
- package/src/tools/setup-project.ts +189 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import {
|
|
2
|
+
db,
|
|
3
|
+
generateId,
|
|
4
|
+
now,
|
|
5
|
+
queryOne,
|
|
6
|
+
queryAll,
|
|
7
|
+
execute,
|
|
8
|
+
ok,
|
|
9
|
+
err,
|
|
10
|
+
buildSearchVector,
|
|
11
|
+
loadTags,
|
|
12
|
+
saveTags,
|
|
13
|
+
deleteTags,
|
|
14
|
+
buildPaginationClause,
|
|
15
|
+
} from './base';
|
|
16
|
+
import type { Result, Task } from '../domain/types';
|
|
17
|
+
import { TaskStatus, Priority, LockStatus, EntityType, ValidationError } from '../domain/types';
|
|
18
|
+
import { isValidTransition, getAllowedTransitions, isTerminalStatus } from '../services/status-validator';
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Row Mapping
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
interface TaskRow {
|
|
25
|
+
id: string;
|
|
26
|
+
project_id: string | null;
|
|
27
|
+
feature_id: string | null;
|
|
28
|
+
title: string;
|
|
29
|
+
summary: string;
|
|
30
|
+
description: string | null;
|
|
31
|
+
status: string;
|
|
32
|
+
priority: string;
|
|
33
|
+
complexity: number;
|
|
34
|
+
version: number;
|
|
35
|
+
last_modified_by: string | null;
|
|
36
|
+
lock_status: string;
|
|
37
|
+
created_at: string;
|
|
38
|
+
modified_at: string;
|
|
39
|
+
search_vector: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rowToTask(row: TaskRow): Task {
|
|
43
|
+
return {
|
|
44
|
+
id: row.id,
|
|
45
|
+
projectId: row.project_id ?? undefined,
|
|
46
|
+
featureId: row.feature_id ?? undefined,
|
|
47
|
+
title: row.title,
|
|
48
|
+
summary: row.summary,
|
|
49
|
+
description: row.description ?? undefined,
|
|
50
|
+
status: row.status as TaskStatus,
|
|
51
|
+
priority: row.priority as Priority,
|
|
52
|
+
complexity: row.complexity,
|
|
53
|
+
version: row.version,
|
|
54
|
+
lastModifiedBy: row.last_modified_by ?? undefined,
|
|
55
|
+
lockStatus: row.lock_status as LockStatus,
|
|
56
|
+
createdAt: new Date(row.created_at),
|
|
57
|
+
modifiedAt: new Date(row.modified_at),
|
|
58
|
+
searchVector: row.search_vector ?? undefined,
|
|
59
|
+
tags: loadTags(row.id, EntityType.TASK),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Validation
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
function validateComplexity(complexity: number): boolean {
|
|
68
|
+
return Number.isInteger(complexity) && complexity >= 1 && complexity <= 10;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Repository Functions
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
export function createTask(params: {
|
|
76
|
+
projectId?: string;
|
|
77
|
+
featureId?: string;
|
|
78
|
+
title: string;
|
|
79
|
+
summary: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
status?: TaskStatus;
|
|
82
|
+
priority: Priority;
|
|
83
|
+
complexity: number;
|
|
84
|
+
tags?: string[];
|
|
85
|
+
}): Result<Task> {
|
|
86
|
+
try {
|
|
87
|
+
// Validate complexity
|
|
88
|
+
if (!validateComplexity(params.complexity)) {
|
|
89
|
+
return err('Complexity must be an integer between 1 and 10', 'VALIDATION_ERROR');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Validate required fields
|
|
93
|
+
if (!params.title?.trim()) {
|
|
94
|
+
return err('Title is required', 'VALIDATION_ERROR');
|
|
95
|
+
}
|
|
96
|
+
if (!params.summary?.trim()) {
|
|
97
|
+
return err('Summary is required', 'VALIDATION_ERROR');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const id = generateId();
|
|
101
|
+
const timestamp = now();
|
|
102
|
+
const status = params.status ?? TaskStatus.PENDING;
|
|
103
|
+
const searchVector = buildSearchVector(params.title, params.summary, params.description);
|
|
104
|
+
|
|
105
|
+
db.run('BEGIN TRANSACTION');
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
execute(
|
|
109
|
+
`INSERT INTO tasks (
|
|
110
|
+
id, project_id, feature_id, title, summary, description,
|
|
111
|
+
status, priority, complexity, version, last_modified_by,
|
|
112
|
+
lock_status, created_at, modified_at, search_vector
|
|
113
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
114
|
+
[
|
|
115
|
+
id,
|
|
116
|
+
params.projectId ?? null,
|
|
117
|
+
params.featureId ?? null,
|
|
118
|
+
params.title.trim(),
|
|
119
|
+
params.summary.trim(),
|
|
120
|
+
params.description?.trim() ?? null,
|
|
121
|
+
status,
|
|
122
|
+
params.priority,
|
|
123
|
+
params.complexity,
|
|
124
|
+
1, // version
|
|
125
|
+
null, // last_modified_by
|
|
126
|
+
LockStatus.UNLOCKED,
|
|
127
|
+
timestamp,
|
|
128
|
+
timestamp,
|
|
129
|
+
searchVector,
|
|
130
|
+
]
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Save tags if provided
|
|
134
|
+
if (params.tags && params.tags.length > 0) {
|
|
135
|
+
saveTags(id, EntityType.TASK, params.tags);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
db.run('COMMIT');
|
|
139
|
+
|
|
140
|
+
const row = queryOne<TaskRow>('SELECT * FROM tasks WHERE id = ?', [id]);
|
|
141
|
+
if (!row) {
|
|
142
|
+
return err('Failed to create task', 'INTERNAL_ERROR');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return ok(rowToTask(row));
|
|
146
|
+
} catch (error) {
|
|
147
|
+
db.run('ROLLBACK');
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getTask(id: string): Result<Task> {
|
|
156
|
+
try {
|
|
157
|
+
const row = queryOne<TaskRow>('SELECT * FROM tasks WHERE id = ?', [id]);
|
|
158
|
+
|
|
159
|
+
if (!row) {
|
|
160
|
+
return err(`Task not found: ${id}`, 'NOT_FOUND');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return ok(rowToTask(row));
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function updateTask(
|
|
170
|
+
id: string,
|
|
171
|
+
params: {
|
|
172
|
+
title?: string;
|
|
173
|
+
summary?: string;
|
|
174
|
+
description?: string;
|
|
175
|
+
status?: TaskStatus;
|
|
176
|
+
priority?: Priority;
|
|
177
|
+
complexity?: number;
|
|
178
|
+
projectId?: string;
|
|
179
|
+
featureId?: string;
|
|
180
|
+
lastModifiedBy?: string;
|
|
181
|
+
tags?: string[];
|
|
182
|
+
version: number;
|
|
183
|
+
}
|
|
184
|
+
): Result<Task> {
|
|
185
|
+
try {
|
|
186
|
+
// Validate complexity if provided
|
|
187
|
+
if (params.complexity !== undefined && !validateComplexity(params.complexity)) {
|
|
188
|
+
return err('Complexity must be an integer between 1 and 10', 'VALIDATION_ERROR');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if task exists and version matches (optimistic locking)
|
|
192
|
+
const existing = queryOne<TaskRow>('SELECT * FROM tasks WHERE id = ?', [id]);
|
|
193
|
+
if (!existing) {
|
|
194
|
+
return err(`Task not found: ${id}`, 'NOT_FOUND');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (existing.version !== params.version) {
|
|
198
|
+
return err(
|
|
199
|
+
`Version conflict: expected ${params.version}, got ${existing.version}`,
|
|
200
|
+
'CONFLICT'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
db.run('BEGIN TRANSACTION');
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Validate status transition if status is being changed
|
|
208
|
+
if (params.status !== undefined && params.status !== existing.status) {
|
|
209
|
+
const currentStatus = existing.status;
|
|
210
|
+
|
|
211
|
+
// Check if current status is terminal
|
|
212
|
+
if (isTerminalStatus('task', currentStatus)) {
|
|
213
|
+
db.run('ROLLBACK');
|
|
214
|
+
return err(
|
|
215
|
+
`Invalid status transition: no transitions are allowed from terminal status '${currentStatus}'`,
|
|
216
|
+
'VALIDATION_ERROR'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if the transition is valid
|
|
221
|
+
if (!isValidTransition('task', currentStatus, params.status)) {
|
|
222
|
+
const allowed = getAllowedTransitions('task', currentStatus);
|
|
223
|
+
db.run('ROLLBACK');
|
|
224
|
+
return err(
|
|
225
|
+
`Invalid status transition from '${currentStatus}' to '${params.status}'. Allowed transitions: ${allowed.join(', ')}`,
|
|
226
|
+
'VALIDATION_ERROR'
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const updates: string[] = [];
|
|
232
|
+
const values: any[] = [];
|
|
233
|
+
|
|
234
|
+
if (params.title !== undefined) {
|
|
235
|
+
if (!params.title.trim()) {
|
|
236
|
+
db.run('ROLLBACK');
|
|
237
|
+
return err('Title cannot be empty', 'VALIDATION_ERROR');
|
|
238
|
+
}
|
|
239
|
+
updates.push('title = ?');
|
|
240
|
+
values.push(params.title.trim());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (params.summary !== undefined) {
|
|
244
|
+
if (!params.summary.trim()) {
|
|
245
|
+
db.run('ROLLBACK');
|
|
246
|
+
return err('Summary cannot be empty', 'VALIDATION_ERROR');
|
|
247
|
+
}
|
|
248
|
+
updates.push('summary = ?');
|
|
249
|
+
values.push(params.summary.trim());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (params.description !== undefined) {
|
|
253
|
+
updates.push('description = ?');
|
|
254
|
+
values.push(params.description?.trim() ?? null);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (params.status !== undefined) {
|
|
258
|
+
updates.push('status = ?');
|
|
259
|
+
values.push(params.status);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (params.priority !== undefined) {
|
|
263
|
+
updates.push('priority = ?');
|
|
264
|
+
values.push(params.priority);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (params.complexity !== undefined) {
|
|
268
|
+
updates.push('complexity = ?');
|
|
269
|
+
values.push(params.complexity);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (params.projectId !== undefined) {
|
|
273
|
+
updates.push('project_id = ?');
|
|
274
|
+
values.push(params.projectId ?? null);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (params.featureId !== undefined) {
|
|
278
|
+
updates.push('feature_id = ?');
|
|
279
|
+
values.push(params.featureId ?? null);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (params.lastModifiedBy !== undefined) {
|
|
283
|
+
updates.push('last_modified_by = ?');
|
|
284
|
+
values.push(params.lastModifiedBy ?? null);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Update search vector if any searchable field changed
|
|
288
|
+
if (params.title !== undefined || params.summary !== undefined || params.description !== undefined) {
|
|
289
|
+
const title = params.title ?? existing.title;
|
|
290
|
+
const summary = params.summary ?? existing.summary;
|
|
291
|
+
const description = params.description !== undefined ? params.description : existing.description;
|
|
292
|
+
updates.push('search_vector = ?');
|
|
293
|
+
values.push(buildSearchVector(title, summary, description));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Always update version and modified_at
|
|
297
|
+
updates.push('version = version + 1');
|
|
298
|
+
updates.push('modified_at = ?');
|
|
299
|
+
values.push(now());
|
|
300
|
+
|
|
301
|
+
// Add id to WHERE clause
|
|
302
|
+
values.push(id);
|
|
303
|
+
values.push(params.version);
|
|
304
|
+
|
|
305
|
+
const sql = `UPDATE tasks SET ${updates.join(', ')} WHERE id = ? AND version = ?`;
|
|
306
|
+
const changes = execute(sql, values);
|
|
307
|
+
|
|
308
|
+
if (changes === 0) {
|
|
309
|
+
db.run('ROLLBACK');
|
|
310
|
+
return err('Update failed: version conflict', 'CONFLICT');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Update tags if provided
|
|
314
|
+
if (params.tags !== undefined) {
|
|
315
|
+
saveTags(id, EntityType.TASK, params.tags);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
db.run('COMMIT');
|
|
319
|
+
|
|
320
|
+
const updated = queryOne<TaskRow>('SELECT * FROM tasks WHERE id = ?', [id]);
|
|
321
|
+
if (!updated) {
|
|
322
|
+
return err('Failed to retrieve updated task', 'INTERNAL_ERROR');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return ok(rowToTask(updated));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
db.run('ROLLBACK');
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function deleteTask(id: string): Result<boolean> {
|
|
336
|
+
try {
|
|
337
|
+
// Check if task exists
|
|
338
|
+
const existing = queryOne<TaskRow>('SELECT id FROM tasks WHERE id = ?', [id]);
|
|
339
|
+
if (!existing) {
|
|
340
|
+
return err(`Task not found: ${id}`, 'NOT_FOUND');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
db.run('BEGIN TRANSACTION');
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
// Delete related dependencies
|
|
347
|
+
execute('DELETE FROM dependencies WHERE from_task_id = ? OR to_task_id = ?', [id, id]);
|
|
348
|
+
|
|
349
|
+
// Delete related sections
|
|
350
|
+
execute('DELETE FROM sections WHERE entity_type = ? AND entity_id = ?', [EntityType.TASK, id]);
|
|
351
|
+
|
|
352
|
+
// Delete related tags
|
|
353
|
+
deleteTags(id, EntityType.TASK);
|
|
354
|
+
|
|
355
|
+
// Delete the task
|
|
356
|
+
execute('DELETE FROM tasks WHERE id = ?', [id]);
|
|
357
|
+
|
|
358
|
+
db.run('COMMIT');
|
|
359
|
+
|
|
360
|
+
return ok(true);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
db.run('ROLLBACK');
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function searchTasks(params: {
|
|
371
|
+
query?: string;
|
|
372
|
+
status?: string;
|
|
373
|
+
priority?: string;
|
|
374
|
+
projectId?: string;
|
|
375
|
+
featureId?: string;
|
|
376
|
+
tags?: string;
|
|
377
|
+
limit?: number;
|
|
378
|
+
offset?: number;
|
|
379
|
+
}): Result<Task[]> {
|
|
380
|
+
try {
|
|
381
|
+
const conditions: string[] = [];
|
|
382
|
+
const values: any[] = [];
|
|
383
|
+
|
|
384
|
+
// Text search
|
|
385
|
+
if (params.query?.trim()) {
|
|
386
|
+
conditions.push('search_vector LIKE ?');
|
|
387
|
+
values.push(`%${params.query.toLowerCase()}%`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Status filter (supports multi-value and negation)
|
|
391
|
+
if (params.status) {
|
|
392
|
+
const statusFilters = params.status.split(',').map(s => s.trim());
|
|
393
|
+
const negated = statusFilters.filter(s => s.startsWith('!'));
|
|
394
|
+
const positive = statusFilters.filter(s => !s.startsWith('!'));
|
|
395
|
+
|
|
396
|
+
if (positive.length > 0) {
|
|
397
|
+
conditions.push(`status IN (${positive.map(() => '?').join(', ')})`);
|
|
398
|
+
values.push(...positive);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (negated.length > 0) {
|
|
402
|
+
const negatedValues = negated.map(s => s.substring(1));
|
|
403
|
+
conditions.push(`status NOT IN (${negatedValues.map(() => '?').join(', ')})`);
|
|
404
|
+
values.push(...negatedValues);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Priority filter (supports multi-value and negation)
|
|
409
|
+
if (params.priority) {
|
|
410
|
+
const priorityFilters = params.priority.split(',').map(p => p.trim());
|
|
411
|
+
const negated = priorityFilters.filter(p => p.startsWith('!'));
|
|
412
|
+
const positive = priorityFilters.filter(p => !p.startsWith('!'));
|
|
413
|
+
|
|
414
|
+
if (positive.length > 0) {
|
|
415
|
+
conditions.push(`priority IN (${positive.map(() => '?').join(', ')})`);
|
|
416
|
+
values.push(...positive);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (negated.length > 0) {
|
|
420
|
+
const negatedValues = negated.map(p => p.substring(1));
|
|
421
|
+
conditions.push(`priority NOT IN (${negatedValues.map(() => '?').join(', ')})`);
|
|
422
|
+
values.push(...negatedValues);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Project filter
|
|
427
|
+
if (params.projectId) {
|
|
428
|
+
conditions.push('project_id = ?');
|
|
429
|
+
values.push(params.projectId);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Feature filter
|
|
433
|
+
if (params.featureId) {
|
|
434
|
+
conditions.push('feature_id = ?');
|
|
435
|
+
values.push(params.featureId);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Tags filter (supports multi-value and negation)
|
|
439
|
+
if (params.tags) {
|
|
440
|
+
const tagFilters = params.tags.split(',').map(t => t.trim().toLowerCase());
|
|
441
|
+
const negated = tagFilters.filter(t => t.startsWith('!'));
|
|
442
|
+
const positive = tagFilters.filter(t => !t.startsWith('!'));
|
|
443
|
+
|
|
444
|
+
if (positive.length > 0) {
|
|
445
|
+
conditions.push(
|
|
446
|
+
`id IN (SELECT entity_id FROM entity_tags WHERE entity_type = '${EntityType.TASK}' AND tag IN (${positive.map(() => '?').join(', ')}))`
|
|
447
|
+
);
|
|
448
|
+
values.push(...positive);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (negated.length > 0) {
|
|
452
|
+
const negatedValues = negated.map(t => t.substring(1));
|
|
453
|
+
conditions.push(
|
|
454
|
+
`id NOT IN (SELECT entity_id FROM entity_tags WHERE entity_type = '${EntityType.TASK}' AND tag IN (${negatedValues.map(() => '?').join(', ')}))`
|
|
455
|
+
);
|
|
456
|
+
values.push(...negatedValues);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
461
|
+
const paginationClause = buildPaginationClause({ limit: params.limit, offset: params.offset });
|
|
462
|
+
|
|
463
|
+
const sql = `SELECT * FROM tasks ${whereClause} ORDER BY created_at DESC${paginationClause}`;
|
|
464
|
+
const rows = queryAll<TaskRow>(sql, values);
|
|
465
|
+
|
|
466
|
+
return ok(rows.map(rowToTask));
|
|
467
|
+
} catch (error) {
|
|
468
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function setTaskStatus(id: string, status: TaskStatus, version: number): Result<Task> {
|
|
473
|
+
try {
|
|
474
|
+
// Check if task exists and version matches
|
|
475
|
+
const existing = queryOne<TaskRow>('SELECT * FROM tasks WHERE id = ?', [id]);
|
|
476
|
+
if (!existing) {
|
|
477
|
+
return err(`Task not found: ${id}`, 'NOT_FOUND');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (existing.version !== version) {
|
|
481
|
+
return err(
|
|
482
|
+
`Version conflict: expected ${version}, got ${existing.version}`,
|
|
483
|
+
'CONFLICT'
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Validate status transition if status is being changed
|
|
488
|
+
if (status !== existing.status) {
|
|
489
|
+
// Check if current status is terminal
|
|
490
|
+
if (isTerminalStatus('task', existing.status)) {
|
|
491
|
+
return err(
|
|
492
|
+
`Cannot transition from terminal status ${existing.status}`,
|
|
493
|
+
'VALIDATION_ERROR'
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Check if the transition is valid
|
|
498
|
+
if (!isValidTransition('task', existing.status, status)) {
|
|
499
|
+
const allowed = getAllowedTransitions('task', existing.status);
|
|
500
|
+
return err(
|
|
501
|
+
`Invalid status transition from ${existing.status} to ${status}. Allowed transitions: ${allowed.join(', ')}`,
|
|
502
|
+
'VALIDATION_ERROR'
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const changes = execute(
|
|
508
|
+
'UPDATE tasks SET status = ?, version = version + 1, modified_at = ? WHERE id = ? AND version = ?',
|
|
509
|
+
[status, now(), id, version]
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
if (changes === 0) {
|
|
513
|
+
return err('Update failed: version conflict', 'CONFLICT');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const updated = queryOne<TaskRow>('SELECT * FROM tasks WHERE id = ?', [id]);
|
|
517
|
+
if (!updated) {
|
|
518
|
+
return err('Failed to retrieve updated task', 'INTERNAL_ERROR');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return ok(rowToTask(updated));
|
|
522
|
+
} catch (error) {
|
|
523
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function bulkUpdateTasks(
|
|
528
|
+
ids: string[],
|
|
529
|
+
updates: {
|
|
530
|
+
status?: TaskStatus;
|
|
531
|
+
priority?: Priority;
|
|
532
|
+
}
|
|
533
|
+
): Result<number> {
|
|
534
|
+
try {
|
|
535
|
+
if (ids.length === 0) {
|
|
536
|
+
return ok(0);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (!updates.status && !updates.priority) {
|
|
540
|
+
return err('At least one update field (status or priority) must be provided', 'VALIDATION_ERROR');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
db.run('BEGIN TRANSACTION');
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
const updateFields: string[] = [];
|
|
547
|
+
const values: any[] = [];
|
|
548
|
+
|
|
549
|
+
if (updates.status !== undefined) {
|
|
550
|
+
updateFields.push('status = ?');
|
|
551
|
+
values.push(updates.status);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (updates.priority !== undefined) {
|
|
555
|
+
updateFields.push('priority = ?');
|
|
556
|
+
values.push(updates.priority);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Always update version and modified_at
|
|
560
|
+
updateFields.push('version = version + 1');
|
|
561
|
+
updateFields.push('modified_at = ?');
|
|
562
|
+
values.push(now());
|
|
563
|
+
|
|
564
|
+
// Add ids to WHERE clause
|
|
565
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
566
|
+
values.push(...ids);
|
|
567
|
+
|
|
568
|
+
const sql = `UPDATE tasks SET ${updateFields.join(', ')} WHERE id IN (${placeholders})`;
|
|
569
|
+
const changes = execute(sql, values);
|
|
570
|
+
|
|
571
|
+
db.run('COMMIT');
|
|
572
|
+
|
|
573
|
+
return ok(changes);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
db.run('ROLLBACK');
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
} catch (error) {
|
|
579
|
+
return err(error instanceof Error ? error.message : 'Unknown error', 'INTERNAL_ERROR');
|
|
580
|
+
}
|
|
581
|
+
}
|