@kaban-board/core 0.3.1 → 0.3.3
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/db/bun-adapter.d.ts.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrator.d.ts.map +1 -1
- package/dist/db/recovery.d.ts +59 -0
- package/dist/db/recovery.d.ts.map +1 -0
- package/dist/db/schema.d.ts +344 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/index-bun.d.ts +1 -0
- package/dist/index-bun.d.ts.map +1 -1
- package/dist/index-bun.js +811 -65
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1267 -66
- package/dist/schemas.d.ts +61 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/services/audit.d.ts +45 -0
- package/dist/services/audit.d.ts.map +1 -0
- package/dist/services/board.d.ts.map +1 -1
- package/dist/services/dependency.d.ts +20 -0
- package/dist/services/dependency.d.ts.map +1 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/link.d.ts +21 -0
- package/dist/services/link.d.ts.map +1 -0
- package/dist/services/markdown.d.ts +37 -0
- package/dist/services/markdown.d.ts.map +1 -0
- package/dist/services/scoring/index.d.ts +17 -0
- package/dist/services/scoring/index.d.ts.map +1 -0
- package/dist/services/scoring/scorers/blocking.d.ts +7 -0
- package/dist/services/scoring/scorers/blocking.d.ts.map +1 -0
- package/dist/services/scoring/scorers/due-date.d.ts +7 -0
- package/dist/services/scoring/scorers/due-date.d.ts.map +1 -0
- package/dist/services/scoring/scorers/fifo.d.ts +7 -0
- package/dist/services/scoring/scorers/fifo.d.ts.map +1 -0
- package/dist/services/scoring/scorers/priority.d.ts +7 -0
- package/dist/services/scoring/scorers/priority.d.ts.map +1 -0
- package/dist/services/scoring/types.d.ts +27 -0
- package/dist/services/scoring/types.d.ts.map +1 -0
- package/dist/services/task.d.ts +6 -1
- package/dist/services/task.d.ts.map +1 -1
- package/dist/utils/date-parser.d.ts +14 -0
- package/dist/utils/date-parser.d.ts.map +1 -0
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -75,15 +75,19 @@ var exports_schema = {};
|
|
|
75
75
|
__export(exports_schema, {
|
|
76
76
|
undoLog: () => undoLog,
|
|
77
77
|
tasks: () => tasks,
|
|
78
|
+
taskLinks: () => taskLinks,
|
|
79
|
+
linkTypes: () => linkTypes,
|
|
78
80
|
columns: () => columns,
|
|
79
|
-
boards: () => boards
|
|
81
|
+
boards: () => boards,
|
|
82
|
+
audits: () => audits
|
|
80
83
|
});
|
|
81
84
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
82
|
-
var boards, columns, tasks, undoLog;
|
|
85
|
+
var boards, columns, tasks, undoLog, audits, linkTypes, taskLinks;
|
|
83
86
|
var init_schema = __esm(() => {
|
|
84
87
|
boards = sqliteTable("boards", {
|
|
85
88
|
id: text("id").primaryKey(),
|
|
86
89
|
name: text("name").notNull(),
|
|
90
|
+
maxBoardTaskId: integer("max_board_task_id").notNull().default(0),
|
|
87
91
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
88
92
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
|
|
89
93
|
});
|
|
@@ -97,6 +101,7 @@ var init_schema = __esm(() => {
|
|
|
97
101
|
});
|
|
98
102
|
tasks = sqliteTable("tasks", {
|
|
99
103
|
id: text("id").primaryKey(),
|
|
104
|
+
boardTaskId: integer("board_task_id"),
|
|
100
105
|
title: text("title").notNull(),
|
|
101
106
|
description: text("description"),
|
|
102
107
|
columnId: text("column_id").notNull().references(() => columns.id),
|
|
@@ -113,8 +118,10 @@ var init_schema = __esm(() => {
|
|
|
113
118
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
114
119
|
startedAt: integer("started_at", { mode: "timestamp" }),
|
|
115
120
|
completedAt: integer("completed_at", { mode: "timestamp" }),
|
|
121
|
+
dueDate: integer("due_date", { mode: "timestamp" }),
|
|
116
122
|
archived: integer("archived", { mode: "boolean" }).notNull().default(false),
|
|
117
|
-
archivedAt: integer("archived_at", { mode: "timestamp" })
|
|
123
|
+
archivedAt: integer("archived_at", { mode: "timestamp" }),
|
|
124
|
+
updatedBy: text("updated_by")
|
|
118
125
|
});
|
|
119
126
|
undoLog = sqliteTable("undo_log", {
|
|
120
127
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
@@ -122,6 +129,25 @@ var init_schema = __esm(() => {
|
|
|
122
129
|
data: text("data", { mode: "json" }).notNull(),
|
|
123
130
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull()
|
|
124
131
|
});
|
|
132
|
+
audits = sqliteTable("audits", {
|
|
133
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
134
|
+
timestamp: integer("timestamp", { mode: "timestamp" }).notNull(),
|
|
135
|
+
eventType: text("event_type", { enum: ["CREATE", "UPDATE", "DELETE"] }).notNull(),
|
|
136
|
+
objectType: text("object_type", { enum: ["task", "column", "board"] }).notNull(),
|
|
137
|
+
objectId: text("object_id").notNull(),
|
|
138
|
+
fieldName: text("field_name"),
|
|
139
|
+
oldValue: text("old_value"),
|
|
140
|
+
newValue: text("new_value"),
|
|
141
|
+
actor: text("actor")
|
|
142
|
+
});
|
|
143
|
+
linkTypes = ["blocks", "blocked_by", "related"];
|
|
144
|
+
taskLinks = sqliteTable("task_links", {
|
|
145
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
146
|
+
fromTaskId: text("from_task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
|
|
147
|
+
toTaskId: text("to_task_id").notNull().references(() => tasks.id, { onDelete: "cascade" }),
|
|
148
|
+
linkType: text("link_type", { enum: linkTypes }).notNull(),
|
|
149
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull()
|
|
150
|
+
});
|
|
125
151
|
});
|
|
126
152
|
|
|
127
153
|
// drizzle/meta/_journal.json
|
|
@@ -151,6 +177,41 @@ var init__journal = __esm(() => {
|
|
|
151
177
|
when: 1769351200000,
|
|
152
178
|
tag: "0002_add_fts5",
|
|
153
179
|
breakpoints: true
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
idx: 3,
|
|
183
|
+
version: "6",
|
|
184
|
+
when: 1769695200000,
|
|
185
|
+
tag: "0003_fix_fts_compatibility",
|
|
186
|
+
breakpoints: true
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
idx: 4,
|
|
190
|
+
version: "6",
|
|
191
|
+
when: 1769700000000,
|
|
192
|
+
tag: "0004_board_scoped_ids",
|
|
193
|
+
breakpoints: true
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
idx: 5,
|
|
197
|
+
version: "6",
|
|
198
|
+
when: 1769800000000,
|
|
199
|
+
tag: "0005_audit_log",
|
|
200
|
+
breakpoints: true
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
idx: 6,
|
|
204
|
+
version: "6",
|
|
205
|
+
when: 1769900000000,
|
|
206
|
+
tag: "0006_due_date",
|
|
207
|
+
breakpoints: true
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
idx: 7,
|
|
211
|
+
version: "6",
|
|
212
|
+
when: 1770000000000,
|
|
213
|
+
tag: "0007_task_links",
|
|
214
|
+
breakpoints: true
|
|
154
215
|
}
|
|
155
216
|
]
|
|
156
217
|
};
|
|
@@ -211,6 +272,216 @@ WHERE NOT EXISTS (SELECT 1 FROM tasks_fts LIMIT 1);
|
|
|
211
272
|
`;
|
|
212
273
|
var init_0002_add_fts5 = () => {};
|
|
213
274
|
|
|
275
|
+
// drizzle/0003_fix_fts_compatibility.sql
|
|
276
|
+
var _0003_fix_fts_compatibility_default = `-- Fix FTS5 for cross-driver compatibility (bun:sqlite + libsql)
|
|
277
|
+
-- Replaces external-content FTS with simple standalone FTS
|
|
278
|
+
-- Uses DELETE/UPDATE triggers instead of FTS5 'delete' command
|
|
279
|
+
DROP TABLE IF EXISTS tasks_fts;
|
|
280
|
+
|
|
281
|
+
--> statement-breakpoint
|
|
282
|
+
DROP TRIGGER IF EXISTS tasks_fts_insert;
|
|
283
|
+
|
|
284
|
+
--> statement-breakpoint
|
|
285
|
+
DROP TRIGGER IF EXISTS tasks_fts_delete;
|
|
286
|
+
|
|
287
|
+
--> statement-breakpoint
|
|
288
|
+
DROP TRIGGER IF EXISTS tasks_fts_update;
|
|
289
|
+
|
|
290
|
+
--> statement-breakpoint
|
|
291
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
|
|
292
|
+
id, title, description,
|
|
293
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
--> statement-breakpoint
|
|
297
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_insert AFTER INSERT ON tasks BEGIN
|
|
298
|
+
INSERT INTO tasks_fts (id, title, description)
|
|
299
|
+
VALUES (NEW.id, NEW.title, COALESCE(NEW.description, ''));
|
|
300
|
+
END;
|
|
301
|
+
|
|
302
|
+
--> statement-breakpoint
|
|
303
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_delete AFTER DELETE ON tasks BEGIN
|
|
304
|
+
DELETE FROM tasks_fts WHERE id = OLD.id;
|
|
305
|
+
END;
|
|
306
|
+
|
|
307
|
+
--> statement-breakpoint
|
|
308
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_update AFTER UPDATE ON tasks BEGIN
|
|
309
|
+
UPDATE tasks_fts
|
|
310
|
+
SET title = NEW.title, description = COALESCE(NEW.description, '')
|
|
311
|
+
WHERE id = OLD.id;
|
|
312
|
+
END;
|
|
313
|
+
|
|
314
|
+
--> statement-breakpoint
|
|
315
|
+
INSERT INTO tasks_fts (id, title, description)
|
|
316
|
+
SELECT id, title, COALESCE(description, '') FROM tasks
|
|
317
|
+
WHERE NOT EXISTS (SELECT 1 FROM tasks_fts LIMIT 1);
|
|
318
|
+
`;
|
|
319
|
+
var init_0003_fix_fts_compatibility = () => {};
|
|
320
|
+
|
|
321
|
+
// drizzle/0004_board_scoped_ids.sql
|
|
322
|
+
var _0004_board_scoped_ids_default = `ALTER TABLE tasks ADD COLUMN board_task_id INTEGER;
|
|
323
|
+
|
|
324
|
+
--> statement-breakpoint
|
|
325
|
+
|
|
326
|
+
ALTER TABLE boards ADD COLUMN max_board_task_id INTEGER NOT NULL DEFAULT 0;
|
|
327
|
+
|
|
328
|
+
--> statement-breakpoint
|
|
329
|
+
|
|
330
|
+
WITH ranked AS (
|
|
331
|
+
SELECT
|
|
332
|
+
t.id,
|
|
333
|
+
ROW_NUMBER() OVER (
|
|
334
|
+
PARTITION BY c.board_id
|
|
335
|
+
ORDER BY t.created_at, t.id
|
|
336
|
+
) as rn
|
|
337
|
+
FROM tasks t
|
|
338
|
+
JOIN columns c ON t.column_id = c.id
|
|
339
|
+
)
|
|
340
|
+
UPDATE tasks
|
|
341
|
+
SET board_task_id = (SELECT rn FROM ranked WHERE ranked.id = tasks.id);
|
|
342
|
+
|
|
343
|
+
--> statement-breakpoint
|
|
344
|
+
|
|
345
|
+
UPDATE boards SET max_board_task_id = (
|
|
346
|
+
SELECT COALESCE(MAX(t.board_task_id), 0)
|
|
347
|
+
FROM tasks t
|
|
348
|
+
JOIN columns c ON t.column_id = c.id
|
|
349
|
+
WHERE c.board_id = boards.id
|
|
350
|
+
);
|
|
351
|
+
`;
|
|
352
|
+
var init_0004_board_scoped_ids = () => {};
|
|
353
|
+
|
|
354
|
+
// drizzle/0005_audit_log.sql
|
|
355
|
+
var _0005_audit_log_default = `-- Add updated_by column to tasks for actor tracking
|
|
356
|
+
ALTER TABLE tasks ADD COLUMN updated_by TEXT;
|
|
357
|
+
|
|
358
|
+
--> statement-breakpoint
|
|
359
|
+
|
|
360
|
+
-- Create audits table
|
|
361
|
+
CREATE TABLE audits (
|
|
362
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
363
|
+
timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
364
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('CREATE', 'UPDATE', 'DELETE')),
|
|
365
|
+
object_type TEXT NOT NULL CHECK (object_type IN ('task', 'column', 'board')),
|
|
366
|
+
object_id TEXT NOT NULL,
|
|
367
|
+
field_name TEXT,
|
|
368
|
+
old_value TEXT,
|
|
369
|
+
new_value TEXT,
|
|
370
|
+
actor TEXT
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
--> statement-breakpoint
|
|
374
|
+
|
|
375
|
+
CREATE INDEX idx_audits_object ON audits(object_type, object_id);
|
|
376
|
+
|
|
377
|
+
--> statement-breakpoint
|
|
378
|
+
|
|
379
|
+
CREATE INDEX idx_audits_timestamp ON audits(timestamp);
|
|
380
|
+
|
|
381
|
+
--> statement-breakpoint
|
|
382
|
+
|
|
383
|
+
-- Trigger: task INSERT
|
|
384
|
+
CREATE TRIGGER audit_task_insert
|
|
385
|
+
AFTER INSERT ON tasks
|
|
386
|
+
BEGIN
|
|
387
|
+
INSERT INTO audits (event_type, object_type, object_id, new_value, actor)
|
|
388
|
+
VALUES ('CREATE', 'task', NEW.id,
|
|
389
|
+
json_object('title', NEW.title, 'columnId', NEW.column_id),
|
|
390
|
+
NEW.created_by);
|
|
391
|
+
END;
|
|
392
|
+
|
|
393
|
+
--> statement-breakpoint
|
|
394
|
+
|
|
395
|
+
-- Trigger: task UPDATE (single trigger with conditional inserts for each field)
|
|
396
|
+
CREATE TRIGGER audit_task_update
|
|
397
|
+
AFTER UPDATE ON tasks
|
|
398
|
+
BEGIN
|
|
399
|
+
-- Title changed
|
|
400
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
401
|
+
SELECT 'UPDATE', 'task', OLD.id, 'title', OLD.title, NEW.title, NEW.updated_by
|
|
402
|
+
WHERE (OLD.title IS NULL AND NEW.title IS NOT NULL)
|
|
403
|
+
OR (OLD.title IS NOT NULL AND NEW.title IS NULL)
|
|
404
|
+
OR (OLD.title <> NEW.title);
|
|
405
|
+
|
|
406
|
+
-- Column changed (task moved)
|
|
407
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
408
|
+
SELECT 'UPDATE', 'task', OLD.id, 'columnId', OLD.column_id, NEW.column_id, NEW.updated_by
|
|
409
|
+
WHERE (OLD.column_id IS NULL AND NEW.column_id IS NOT NULL)
|
|
410
|
+
OR (OLD.column_id IS NOT NULL AND NEW.column_id IS NULL)
|
|
411
|
+
OR (OLD.column_id <> NEW.column_id);
|
|
412
|
+
|
|
413
|
+
-- Assigned to changed
|
|
414
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
415
|
+
SELECT 'UPDATE', 'task', OLD.id, 'assignedTo', OLD.assigned_to, NEW.assigned_to, NEW.updated_by
|
|
416
|
+
WHERE (OLD.assigned_to IS NULL AND NEW.assigned_to IS NOT NULL)
|
|
417
|
+
OR (OLD.assigned_to IS NOT NULL AND NEW.assigned_to IS NULL)
|
|
418
|
+
OR (OLD.assigned_to <> NEW.assigned_to);
|
|
419
|
+
|
|
420
|
+
-- Description changed
|
|
421
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
422
|
+
SELECT 'UPDATE', 'task', OLD.id, 'description', OLD.description, NEW.description, NEW.updated_by
|
|
423
|
+
WHERE (OLD.description IS NULL AND NEW.description IS NOT NULL)
|
|
424
|
+
OR (OLD.description IS NOT NULL AND NEW.description IS NULL)
|
|
425
|
+
OR (OLD.description <> NEW.description);
|
|
426
|
+
|
|
427
|
+
-- Archived status changed
|
|
428
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
429
|
+
SELECT 'UPDATE', 'task', OLD.id, 'archived', OLD.archived, NEW.archived, NEW.updated_by
|
|
430
|
+
WHERE (OLD.archived IS NULL AND NEW.archived IS NOT NULL)
|
|
431
|
+
OR (OLD.archived IS NOT NULL AND NEW.archived IS NULL)
|
|
432
|
+
OR (OLD.archived <> NEW.archived);
|
|
433
|
+
|
|
434
|
+
-- Labels changed
|
|
435
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
436
|
+
SELECT 'UPDATE', 'task', OLD.id, 'labels', OLD.labels, NEW.labels, NEW.updated_by
|
|
437
|
+
WHERE (OLD.labels IS NULL AND NEW.labels IS NOT NULL)
|
|
438
|
+
OR (OLD.labels IS NOT NULL AND NEW.labels IS NULL)
|
|
439
|
+
OR (OLD.labels <> NEW.labels);
|
|
440
|
+
END;
|
|
441
|
+
|
|
442
|
+
--> statement-breakpoint
|
|
443
|
+
|
|
444
|
+
-- Trigger: task DELETE
|
|
445
|
+
CREATE TRIGGER audit_task_delete
|
|
446
|
+
AFTER DELETE ON tasks
|
|
447
|
+
BEGIN
|
|
448
|
+
INSERT INTO audits (event_type, object_type, object_id, old_value)
|
|
449
|
+
VALUES ('DELETE', 'task', OLD.id,
|
|
450
|
+
json_object('title', OLD.title, 'columnId', OLD.column_id));
|
|
451
|
+
END;
|
|
452
|
+
`;
|
|
453
|
+
var init_0005_audit_log = () => {};
|
|
454
|
+
|
|
455
|
+
// drizzle/0006_due_date.sql
|
|
456
|
+
var _0006_due_date_default = `ALTER TABLE tasks ADD COLUMN due_date INTEGER;
|
|
457
|
+
`;
|
|
458
|
+
var init_0006_due_date = () => {};
|
|
459
|
+
|
|
460
|
+
// drizzle/0007_task_links.sql
|
|
461
|
+
var _0007_task_links_default = `-- Create task_links table
|
|
462
|
+
CREATE TABLE task_links (
|
|
463
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
464
|
+
from_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
465
|
+
to_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
466
|
+
link_type TEXT NOT NULL CHECK (link_type IN ('blocks', 'blocked_by', 'related')),
|
|
467
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
468
|
+
UNIQUE(from_task_id, to_task_id, link_type)
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
--> statement-breakpoint
|
|
472
|
+
|
|
473
|
+
CREATE INDEX idx_task_links_from ON task_links(from_task_id);
|
|
474
|
+
|
|
475
|
+
--> statement-breakpoint
|
|
476
|
+
|
|
477
|
+
CREATE INDEX idx_task_links_to ON task_links(to_task_id);
|
|
478
|
+
|
|
479
|
+
--> statement-breakpoint
|
|
480
|
+
|
|
481
|
+
CREATE INDEX idx_task_links_type ON task_links(link_type);
|
|
482
|
+
`;
|
|
483
|
+
var init_0007_task_links = () => {};
|
|
484
|
+
|
|
214
485
|
// src/db/migrator.ts
|
|
215
486
|
var exports_migrator = {};
|
|
216
487
|
__export(exports_migrator, {
|
|
@@ -220,7 +491,7 @@ import { readFileSync } from "node:fs";
|
|
|
220
491
|
import { dirname as dirname2, isAbsolute, join } from "node:path";
|
|
221
492
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
222
493
|
function resolveSqlContent(sqlOrPath) {
|
|
223
|
-
if (sqlOrPath.includes("CREATE") || sqlOrPath.includes("INSERT") || sqlOrPath.includes("--")) {
|
|
494
|
+
if (sqlOrPath.includes("CREATE") || sqlOrPath.includes("INSERT") || sqlOrPath.includes("ALTER") || sqlOrPath.includes("--")) {
|
|
224
495
|
return sqlOrPath;
|
|
225
496
|
}
|
|
226
497
|
let filePath = sqlOrPath;
|
|
@@ -327,10 +598,20 @@ var init_migrator = __esm(() => {
|
|
|
327
598
|
init_0000_init();
|
|
328
599
|
init_0001_add_archived();
|
|
329
600
|
init_0002_add_fts5();
|
|
601
|
+
init_0003_fix_fts_compatibility();
|
|
602
|
+
init_0004_board_scoped_ids();
|
|
603
|
+
init_0005_audit_log();
|
|
604
|
+
init_0006_due_date();
|
|
605
|
+
init_0007_task_links();
|
|
330
606
|
migrationSql = {
|
|
331
607
|
"0000_init": _0000_init_default,
|
|
332
608
|
"0001_add_archived": _0001_add_archived_default,
|
|
333
|
-
"0002_add_fts5": _0002_add_fts5_default
|
|
609
|
+
"0002_add_fts5": _0002_add_fts5_default,
|
|
610
|
+
"0003_fix_fts_compatibility": _0003_fix_fts_compatibility_default,
|
|
611
|
+
"0004_board_scoped_ids": _0004_board_scoped_ids_default,
|
|
612
|
+
"0005_audit_log": _0005_audit_log_default,
|
|
613
|
+
"0006_due_date": _0006_due_date_default,
|
|
614
|
+
"0007_task_links": _0007_task_links_default
|
|
334
615
|
};
|
|
335
616
|
__dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
336
617
|
drizzleDir = join(__dirname2, "..", "..", "drizzle");
|
|
@@ -386,17 +667,174 @@ var init_libsql_adapter = __esm(() => {
|
|
|
386
667
|
init_utils();
|
|
387
668
|
});
|
|
388
669
|
|
|
670
|
+
// src/db/recovery.ts
|
|
671
|
+
import { existsSync as existsSync2, renameSync, unlinkSync } from "node:fs";
|
|
672
|
+
import { execSync } from "node:child_process";
|
|
673
|
+
function checkIntegrity(db) {
|
|
674
|
+
try {
|
|
675
|
+
const result = db.query("PRAGMA integrity_check").get();
|
|
676
|
+
return result?.integrity_check === "ok";
|
|
677
|
+
} catch {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function attemptWalRecovery(db) {
|
|
682
|
+
try {
|
|
683
|
+
db.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
684
|
+
return checkIntegrity(db);
|
|
685
|
+
} catch {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function dumpAndRestore(dbPath) {
|
|
690
|
+
const backupPath = `${dbPath}.corrupted.${Date.now()}`;
|
|
691
|
+
const dumpPath = `${dbPath}.dump.sql`;
|
|
692
|
+
try {
|
|
693
|
+
execSync(`sqlite3 "${dbPath}" ".dump" > "${dumpPath}" 2>/dev/null`, {
|
|
694
|
+
encoding: "utf-8",
|
|
695
|
+
timeout: 30000
|
|
696
|
+
});
|
|
697
|
+
renameSync(dbPath, backupPath);
|
|
698
|
+
const walPath = `${dbPath}-wal`;
|
|
699
|
+
const shmPath = `${dbPath}-shm`;
|
|
700
|
+
if (existsSync2(walPath))
|
|
701
|
+
unlinkSync(walPath);
|
|
702
|
+
if (existsSync2(shmPath))
|
|
703
|
+
unlinkSync(shmPath);
|
|
704
|
+
execSync(`sqlite3 "${dbPath}" < "${dumpPath}"`, {
|
|
705
|
+
encoding: "utf-8",
|
|
706
|
+
timeout: 30000
|
|
707
|
+
});
|
|
708
|
+
unlinkSync(dumpPath);
|
|
709
|
+
return { success: true, backupPath };
|
|
710
|
+
} catch {
|
|
711
|
+
try {
|
|
712
|
+
if (existsSync2(dumpPath))
|
|
713
|
+
unlinkSync(dumpPath);
|
|
714
|
+
if (existsSync2(backupPath) && !existsSync2(dbPath)) {
|
|
715
|
+
renameSync(backupPath, dbPath);
|
|
716
|
+
}
|
|
717
|
+
} catch {}
|
|
718
|
+
return { success: false, backupPath };
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function rebuildFts(db) {
|
|
722
|
+
try {
|
|
723
|
+
db.run("DROP TABLE IF EXISTS tasks_fts");
|
|
724
|
+
db.run("DROP TRIGGER IF EXISTS tasks_fts_insert");
|
|
725
|
+
db.run("DROP TRIGGER IF EXISTS tasks_fts_delete");
|
|
726
|
+
db.run("DROP TRIGGER IF EXISTS tasks_fts_update");
|
|
727
|
+
db.run(`
|
|
728
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
|
|
729
|
+
id, title, description,
|
|
730
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
731
|
+
)
|
|
732
|
+
`);
|
|
733
|
+
db.run(`
|
|
734
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_insert AFTER INSERT ON tasks BEGIN
|
|
735
|
+
INSERT INTO tasks_fts (id, title, description)
|
|
736
|
+
VALUES (NEW.id, NEW.title, COALESCE(NEW.description, ''));
|
|
737
|
+
END
|
|
738
|
+
`);
|
|
739
|
+
db.run(`
|
|
740
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_delete AFTER DELETE ON tasks BEGIN
|
|
741
|
+
DELETE FROM tasks_fts WHERE id = OLD.id;
|
|
742
|
+
END
|
|
743
|
+
`);
|
|
744
|
+
db.run(`
|
|
745
|
+
CREATE TRIGGER IF NOT EXISTS tasks_fts_update AFTER UPDATE ON tasks BEGIN
|
|
746
|
+
UPDATE tasks_fts
|
|
747
|
+
SET title = NEW.title, description = COALESCE(NEW.description, '')
|
|
748
|
+
WHERE id = OLD.id;
|
|
749
|
+
END
|
|
750
|
+
`);
|
|
751
|
+
db.run(`
|
|
752
|
+
INSERT INTO tasks_fts (id, title, description)
|
|
753
|
+
SELECT id, title, COALESCE(description, '') FROM tasks
|
|
754
|
+
`);
|
|
755
|
+
return true;
|
|
756
|
+
} catch {
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function checkFtsHealth(db) {
|
|
761
|
+
try {
|
|
762
|
+
db.query("SELECT COUNT(*) FROM tasks_fts").get();
|
|
763
|
+
return true;
|
|
764
|
+
} catch {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async function attemptRecovery(dbPath, openDb) {
|
|
769
|
+
let db = null;
|
|
770
|
+
try {
|
|
771
|
+
db = openDb();
|
|
772
|
+
const integrityOk = checkIntegrity(db);
|
|
773
|
+
const ftsOk = checkFtsHealth(db);
|
|
774
|
+
if (integrityOk && ftsOk) {
|
|
775
|
+
return { recovered: false };
|
|
776
|
+
}
|
|
777
|
+
if (integrityOk && !ftsOk) {
|
|
778
|
+
console.warn("[kaban] FTS corruption detected, rebuilding...");
|
|
779
|
+
if (rebuildFts(db)) {
|
|
780
|
+
console.warn("[kaban] FTS rebuilt successfully");
|
|
781
|
+
return { recovered: true, method: "fts_rebuild" };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (integrityOk) {
|
|
785
|
+
return { recovered: false };
|
|
786
|
+
}
|
|
787
|
+
console.warn("[kaban] Database corruption detected, attempting recovery...");
|
|
788
|
+
if (attemptWalRecovery(db)) {
|
|
789
|
+
console.warn("[kaban] Recovery successful via WAL checkpoint");
|
|
790
|
+
return { recovered: true, method: "wal_checkpoint" };
|
|
791
|
+
}
|
|
792
|
+
db.close();
|
|
793
|
+
db = null;
|
|
794
|
+
const { success, backupPath } = dumpAndRestore(dbPath);
|
|
795
|
+
if (success) {
|
|
796
|
+
console.warn(`[kaban] Recovery successful via dump/restore. Backup: ${backupPath}`);
|
|
797
|
+
return { recovered: true, method: "dump_restore", backupPath };
|
|
798
|
+
}
|
|
799
|
+
throw new KabanError(`Database is corrupted and automatic recovery failed. Backup at: ${backupPath}`, ExitCode.GENERAL_ERROR);
|
|
800
|
+
} finally {
|
|
801
|
+
try {
|
|
802
|
+
db?.close();
|
|
803
|
+
} catch {}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
var init_recovery = __esm(() => {
|
|
807
|
+
init_types();
|
|
808
|
+
});
|
|
809
|
+
|
|
389
810
|
// src/db/bun-adapter.ts
|
|
390
811
|
var exports_bun_adapter = {};
|
|
391
812
|
__export(exports_bun_adapter, {
|
|
392
813
|
createBunDb: () => createBunDb
|
|
393
814
|
});
|
|
815
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
394
816
|
async function createBunDb(filePath) {
|
|
395
817
|
let sqlite;
|
|
396
818
|
try {
|
|
397
819
|
const { Database } = await import("bun:sqlite");
|
|
398
820
|
const { drizzle } = await import("drizzle-orm/bun-sqlite");
|
|
399
821
|
ensureDbDir(filePath);
|
|
822
|
+
if (existsSync3(filePath)) {
|
|
823
|
+
const recovery = await attemptRecovery(filePath, () => {
|
|
824
|
+
const tempDb = new Database(filePath);
|
|
825
|
+
return {
|
|
826
|
+
query: (sql) => ({
|
|
827
|
+
get: () => tempDb.query(sql).get(),
|
|
828
|
+
all: () => tempDb.query(sql).all()
|
|
829
|
+
}),
|
|
830
|
+
run: (sql) => tempDb.run(sql),
|
|
831
|
+
close: () => tempDb.close()
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
if (recovery.recovered) {
|
|
835
|
+
console.warn(`[kaban] Database recovered using ${recovery.method}`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
400
838
|
sqlite = new Database(filePath);
|
|
401
839
|
const db = drizzle({ client: sqlite, schema: exports_schema });
|
|
402
840
|
let closed = false;
|
|
@@ -437,6 +875,7 @@ async function createBunDb(filePath) {
|
|
|
437
875
|
}
|
|
438
876
|
var init_bun_adapter = __esm(() => {
|
|
439
877
|
init_types();
|
|
878
|
+
init_recovery();
|
|
440
879
|
init_schema();
|
|
441
880
|
init_utils();
|
|
442
881
|
});
|
|
@@ -510,6 +949,7 @@ PRAGMA journal_mode = WAL;
|
|
|
510
949
|
CREATE TABLE IF NOT EXISTS boards (
|
|
511
950
|
id TEXT PRIMARY KEY,
|
|
512
951
|
name TEXT NOT NULL,
|
|
952
|
+
max_board_task_id INTEGER NOT NULL DEFAULT 0,
|
|
513
953
|
created_at INTEGER NOT NULL,
|
|
514
954
|
updated_at INTEGER NOT NULL
|
|
515
955
|
);
|
|
@@ -525,6 +965,7 @@ CREATE TABLE IF NOT EXISTS columns (
|
|
|
525
965
|
|
|
526
966
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
527
967
|
id TEXT PRIMARY KEY,
|
|
968
|
+
board_task_id INTEGER,
|
|
528
969
|
title TEXT NOT NULL,
|
|
529
970
|
description TEXT,
|
|
530
971
|
column_id TEXT NOT NULL REFERENCES columns(id),
|
|
@@ -541,8 +982,10 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
541
982
|
updated_at INTEGER NOT NULL,
|
|
542
983
|
started_at INTEGER,
|
|
543
984
|
completed_at INTEGER,
|
|
985
|
+
due_date INTEGER,
|
|
544
986
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
545
|
-
archived_at INTEGER
|
|
987
|
+
archived_at INTEGER,
|
|
988
|
+
updated_by TEXT
|
|
546
989
|
);
|
|
547
990
|
|
|
548
991
|
CREATE TABLE IF NOT EXISTS undo_log (
|
|
@@ -552,8 +995,95 @@ CREATE TABLE IF NOT EXISTS undo_log (
|
|
|
552
995
|
created_at INTEGER NOT NULL
|
|
553
996
|
);
|
|
554
997
|
|
|
998
|
+
CREATE TABLE IF NOT EXISTS audits (
|
|
999
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1000
|
+
timestamp INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
1001
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('CREATE', 'UPDATE', 'DELETE')),
|
|
1002
|
+
object_type TEXT NOT NULL CHECK (object_type IN ('task', 'column', 'board')),
|
|
1003
|
+
object_id TEXT NOT NULL,
|
|
1004
|
+
field_name TEXT,
|
|
1005
|
+
old_value TEXT,
|
|
1006
|
+
new_value TEXT,
|
|
1007
|
+
actor TEXT
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
CREATE TABLE IF NOT EXISTS task_links (
|
|
1011
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1012
|
+
from_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1013
|
+
to_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1014
|
+
link_type TEXT NOT NULL CHECK (link_type IN ('blocks', 'blocked_by', 'related')),
|
|
1015
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
1016
|
+
UNIQUE(from_task_id, to_task_id, link_type)
|
|
1017
|
+
);
|
|
1018
|
+
|
|
555
1019
|
CREATE INDEX IF NOT EXISTS idx_tasks_column ON tasks(column_id);
|
|
556
1020
|
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id);
|
|
1021
|
+
CREATE INDEX IF NOT EXISTS idx_audits_object ON audits(object_type, object_id);
|
|
1022
|
+
CREATE INDEX IF NOT EXISTS idx_audits_timestamp ON audits(timestamp);
|
|
1023
|
+
CREATE INDEX IF NOT EXISTS idx_audits_actor ON audits(actor);
|
|
1024
|
+
CREATE INDEX IF NOT EXISTS idx_task_links_from ON task_links(from_task_id);
|
|
1025
|
+
CREATE INDEX IF NOT EXISTS idx_task_links_to ON task_links(to_task_id);
|
|
1026
|
+
CREATE INDEX IF NOT EXISTS idx_task_links_type ON task_links(link_type);
|
|
1027
|
+
|
|
1028
|
+
-- Trigger: task INSERT
|
|
1029
|
+
CREATE TRIGGER IF NOT EXISTS audit_task_insert
|
|
1030
|
+
AFTER INSERT ON tasks
|
|
1031
|
+
BEGIN
|
|
1032
|
+
INSERT INTO audits (event_type, object_type, object_id, new_value, actor)
|
|
1033
|
+
VALUES ('CREATE', 'task', NEW.id,
|
|
1034
|
+
json_object('title', NEW.title, 'columnId', NEW.column_id),
|
|
1035
|
+
NEW.created_by);
|
|
1036
|
+
END;
|
|
1037
|
+
|
|
1038
|
+
-- Trigger: task UPDATE
|
|
1039
|
+
CREATE TRIGGER IF NOT EXISTS audit_task_update
|
|
1040
|
+
AFTER UPDATE ON tasks
|
|
1041
|
+
BEGIN
|
|
1042
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
1043
|
+
SELECT 'UPDATE', 'task', OLD.id, 'title', OLD.title, NEW.title, NEW.updated_by
|
|
1044
|
+
WHERE (OLD.title IS NULL AND NEW.title IS NOT NULL)
|
|
1045
|
+
OR (OLD.title IS NOT NULL AND NEW.title IS NULL)
|
|
1046
|
+
OR (OLD.title <> NEW.title);
|
|
1047
|
+
|
|
1048
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
1049
|
+
SELECT 'UPDATE', 'task', OLD.id, 'columnId', OLD.column_id, NEW.column_id, NEW.updated_by
|
|
1050
|
+
WHERE (OLD.column_id IS NULL AND NEW.column_id IS NOT NULL)
|
|
1051
|
+
OR (OLD.column_id IS NOT NULL AND NEW.column_id IS NULL)
|
|
1052
|
+
OR (OLD.column_id <> NEW.column_id);
|
|
1053
|
+
|
|
1054
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
1055
|
+
SELECT 'UPDATE', 'task', OLD.id, 'assignedTo', OLD.assigned_to, NEW.assigned_to, NEW.updated_by
|
|
1056
|
+
WHERE (OLD.assigned_to IS NULL AND NEW.assigned_to IS NOT NULL)
|
|
1057
|
+
OR (OLD.assigned_to IS NOT NULL AND NEW.assigned_to IS NULL)
|
|
1058
|
+
OR (OLD.assigned_to <> NEW.assigned_to);
|
|
1059
|
+
|
|
1060
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
1061
|
+
SELECT 'UPDATE', 'task', OLD.id, 'description', OLD.description, NEW.description, NEW.updated_by
|
|
1062
|
+
WHERE (OLD.description IS NULL AND NEW.description IS NOT NULL)
|
|
1063
|
+
OR (OLD.description IS NOT NULL AND NEW.description IS NULL)
|
|
1064
|
+
OR (OLD.description <> NEW.description);
|
|
1065
|
+
|
|
1066
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
1067
|
+
SELECT 'UPDATE', 'task', OLD.id, 'archived', OLD.archived, NEW.archived, NEW.updated_by
|
|
1068
|
+
WHERE (OLD.archived IS NULL AND NEW.archived IS NOT NULL)
|
|
1069
|
+
OR (OLD.archived IS NOT NULL AND NEW.archived IS NULL)
|
|
1070
|
+
OR (OLD.archived <> NEW.archived);
|
|
1071
|
+
|
|
1072
|
+
INSERT INTO audits (event_type, object_type, object_id, field_name, old_value, new_value, actor)
|
|
1073
|
+
SELECT 'UPDATE', 'task', OLD.id, 'labels', OLD.labels, NEW.labels, NEW.updated_by
|
|
1074
|
+
WHERE (OLD.labels IS NULL AND NEW.labels IS NOT NULL)
|
|
1075
|
+
OR (OLD.labels IS NOT NULL AND NEW.labels IS NULL)
|
|
1076
|
+
OR (OLD.labels <> NEW.labels);
|
|
1077
|
+
END;
|
|
1078
|
+
|
|
1079
|
+
-- Trigger: task DELETE
|
|
1080
|
+
CREATE TRIGGER IF NOT EXISTS audit_task_delete
|
|
1081
|
+
AFTER DELETE ON tasks
|
|
1082
|
+
BEGIN
|
|
1083
|
+
INSERT INTO audits (event_type, object_type, object_id, old_value)
|
|
1084
|
+
VALUES ('DELETE', 'task', OLD.id,
|
|
1085
|
+
json_object('title', OLD.title, 'columnId', OLD.column_id));
|
|
1086
|
+
END;
|
|
557
1087
|
`;
|
|
558
1088
|
async function initializeSchema(db) {
|
|
559
1089
|
await db.$runRaw(SCHEMA_SQL);
|
|
@@ -569,6 +1099,7 @@ var AgentNameSchema = AgentNameBaseSchema.transform((s) => s.toLowerCase());
|
|
|
569
1099
|
var ColumnIdSchema = z.string().min(1, "Column ID cannot be empty").max(50, "Column ID cannot exceed 50 characters").regex(/^[a-z0-9_]+$/, "Column ID must be lowercase alphanumeric with underscores");
|
|
570
1100
|
var TaskSchema = z.object({
|
|
571
1101
|
id: UlidSchema,
|
|
1102
|
+
boardTaskId: z.number().int().positive().nullable(),
|
|
572
1103
|
title: z.string(),
|
|
573
1104
|
description: z.string().nullable(),
|
|
574
1105
|
columnId: z.string(),
|
|
@@ -585,8 +1116,29 @@ var TaskSchema = z.object({
|
|
|
585
1116
|
updatedAt: z.date(),
|
|
586
1117
|
startedAt: z.date().nullable(),
|
|
587
1118
|
completedAt: z.date().nullable(),
|
|
1119
|
+
dueDate: z.date().nullable(),
|
|
588
1120
|
archived: z.boolean().default(false),
|
|
589
|
-
archivedAt: z.date().nullable().optional()
|
|
1121
|
+
archivedAt: z.date().nullable().optional(),
|
|
1122
|
+
updatedBy: z.string().nullable()
|
|
1123
|
+
});
|
|
1124
|
+
var AuditSchema = z.object({
|
|
1125
|
+
id: z.number().int().positive(),
|
|
1126
|
+
timestamp: z.date(),
|
|
1127
|
+
eventType: z.enum(["CREATE", "UPDATE", "DELETE"]),
|
|
1128
|
+
objectType: z.enum(["task", "column", "board"]),
|
|
1129
|
+
objectId: z.string(),
|
|
1130
|
+
fieldName: z.string().nullable(),
|
|
1131
|
+
oldValue: z.string().nullable(),
|
|
1132
|
+
newValue: z.string().nullable(),
|
|
1133
|
+
actor: z.string().nullable()
|
|
1134
|
+
});
|
|
1135
|
+
var LinkTypeSchema = z.enum(["blocks", "blocked_by", "related"]);
|
|
1136
|
+
var TaskLinkSchema = z.object({
|
|
1137
|
+
id: z.number().int().positive(),
|
|
1138
|
+
fromTaskId: UlidSchema,
|
|
1139
|
+
toTaskId: UlidSchema,
|
|
1140
|
+
linkType: LinkTypeSchema,
|
|
1141
|
+
createdAt: z.date()
|
|
590
1142
|
});
|
|
591
1143
|
var ColumnSchema = z.object({
|
|
592
1144
|
id: z.string(),
|
|
@@ -598,6 +1150,7 @@ var ColumnSchema = z.object({
|
|
|
598
1150
|
var BoardSchema = z.object({
|
|
599
1151
|
id: UlidSchema,
|
|
600
1152
|
name: z.string(),
|
|
1153
|
+
maxBoardTaskId: z.number().int().nonnegative(),
|
|
601
1154
|
createdAt: z.date(),
|
|
602
1155
|
updatedAt: z.date()
|
|
603
1156
|
});
|
|
@@ -632,14 +1185,16 @@ var AddTaskInputSchema = z.object({
|
|
|
632
1185
|
assignedTo: AgentNameSchema.optional(),
|
|
633
1186
|
dependsOn: z.array(UlidSchema).optional(),
|
|
634
1187
|
files: z.array(z.string()).optional(),
|
|
635
|
-
labels: z.array(z.string().max(50)).optional()
|
|
1188
|
+
labels: z.array(z.string().max(50)).optional(),
|
|
1189
|
+
dueDate: z.string().optional()
|
|
636
1190
|
});
|
|
637
1191
|
var UpdateTaskInputSchema = z.object({
|
|
638
1192
|
title: TitleSchema.optional(),
|
|
639
1193
|
description: z.string().max(5000).nullable().optional(),
|
|
640
1194
|
assignedTo: AgentNameSchema.nullable().optional(),
|
|
641
1195
|
files: z.array(z.string()).optional(),
|
|
642
|
-
labels: z.array(z.string().max(50)).optional()
|
|
1196
|
+
labels: z.array(z.string().max(50)).optional(),
|
|
1197
|
+
dueDate: z.string().nullable().optional()
|
|
643
1198
|
});
|
|
644
1199
|
var MoveTaskInputSchema = z.object({
|
|
645
1200
|
id: UlidSchema,
|
|
@@ -665,6 +1220,7 @@ var TaskResponseSchema = TaskSchema.extend({
|
|
|
665
1220
|
updatedAt: z.string().datetime(),
|
|
666
1221
|
startedAt: z.string().datetime().nullable(),
|
|
667
1222
|
completedAt: z.string().datetime().nullable(),
|
|
1223
|
+
dueDate: z.string().datetime().nullable(),
|
|
668
1224
|
archivedAt: z.string().datetime().nullable().optional()
|
|
669
1225
|
});
|
|
670
1226
|
var BoardStatusSchema = z.object({
|
|
@@ -691,14 +1247,16 @@ var AddTaskInputJsonSchema = z.object({
|
|
|
691
1247
|
assignedTo: AgentNameBaseSchema.optional(),
|
|
692
1248
|
dependsOn: z.array(UlidSchema).optional(),
|
|
693
1249
|
files: z.array(z.string()).optional(),
|
|
694
|
-
labels: z.array(z.string().max(50)).optional()
|
|
1250
|
+
labels: z.array(z.string().max(50)).optional(),
|
|
1251
|
+
dueDate: z.string().optional()
|
|
695
1252
|
});
|
|
696
1253
|
var UpdateTaskInputJsonSchema = z.object({
|
|
697
1254
|
title: TitleBaseSchema.optional(),
|
|
698
1255
|
description: z.string().max(5000).nullable().optional(),
|
|
699
1256
|
assignedTo: AgentNameBaseSchema.nullable().optional(),
|
|
700
1257
|
files: z.array(z.string()).optional(),
|
|
701
|
-
labels: z.array(z.string().max(50)).optional()
|
|
1258
|
+
labels: z.array(z.string().max(50)).optional(),
|
|
1259
|
+
dueDate: z.string().nullable().optional()
|
|
702
1260
|
});
|
|
703
1261
|
var ListTasksFilterJsonSchema = z.object({
|
|
704
1262
|
columnId: ColumnIdSchema.optional(),
|
|
@@ -738,9 +1296,103 @@ var jsonSchemas = {
|
|
|
738
1296
|
function getJsonSchema(name) {
|
|
739
1297
|
return jsonSchemas[name];
|
|
740
1298
|
}
|
|
1299
|
+
// src/services/audit.ts
|
|
1300
|
+
init_schema();
|
|
1301
|
+
import { and, desc, eq, gte, isNotNull, lte, sql } from "drizzle-orm";
|
|
1302
|
+
|
|
1303
|
+
class AuditService {
|
|
1304
|
+
db;
|
|
1305
|
+
constructor(db) {
|
|
1306
|
+
this.db = db;
|
|
1307
|
+
}
|
|
1308
|
+
async getHistory(filter = {}) {
|
|
1309
|
+
const conditions = [];
|
|
1310
|
+
if (filter.objectType) {
|
|
1311
|
+
conditions.push(eq(audits.objectType, filter.objectType));
|
|
1312
|
+
}
|
|
1313
|
+
if (filter.objectId) {
|
|
1314
|
+
conditions.push(eq(audits.objectId, filter.objectId));
|
|
1315
|
+
}
|
|
1316
|
+
if (filter.eventType) {
|
|
1317
|
+
conditions.push(eq(audits.eventType, filter.eventType));
|
|
1318
|
+
}
|
|
1319
|
+
if (filter.actor) {
|
|
1320
|
+
conditions.push(eq(audits.actor, filter.actor));
|
|
1321
|
+
}
|
|
1322
|
+
if (filter.since) {
|
|
1323
|
+
conditions.push(gte(audits.timestamp, filter.since));
|
|
1324
|
+
}
|
|
1325
|
+
if (filter.until) {
|
|
1326
|
+
conditions.push(lte(audits.timestamp, filter.until));
|
|
1327
|
+
}
|
|
1328
|
+
const limit = Math.min(filter.limit ?? 50, 1000);
|
|
1329
|
+
const offset = filter.offset ?? 0;
|
|
1330
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
|
1331
|
+
const entries = await this.db.select().from(audits).where(whereClause).orderBy(desc(audits.timestamp)).limit(limit + 1).offset(offset);
|
|
1332
|
+
const hasMore = entries.length > limit;
|
|
1333
|
+
if (hasMore)
|
|
1334
|
+
entries.pop();
|
|
1335
|
+
const countResult = await this.db.select({ count: sql`COUNT(*)` }).from(audits).where(whereClause);
|
|
1336
|
+
return {
|
|
1337
|
+
entries,
|
|
1338
|
+
total: countResult[0]?.count ?? 0,
|
|
1339
|
+
hasMore
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
async getTaskHistory(taskId, limit = 50) {
|
|
1343
|
+
const result = await this.getHistory({
|
|
1344
|
+
objectType: "task",
|
|
1345
|
+
objectId: taskId,
|
|
1346
|
+
limit
|
|
1347
|
+
});
|
|
1348
|
+
return result.entries;
|
|
1349
|
+
}
|
|
1350
|
+
async getRecentChanges(limit = 50) {
|
|
1351
|
+
const result = await this.getHistory({ limit });
|
|
1352
|
+
return result.entries;
|
|
1353
|
+
}
|
|
1354
|
+
async getChangesByActor(actor, limit = 50) {
|
|
1355
|
+
const result = await this.getHistory({ actor, limit });
|
|
1356
|
+
return result.entries;
|
|
1357
|
+
}
|
|
1358
|
+
async getStats() {
|
|
1359
|
+
const total = await this.db.select({ count: sql`COUNT(*)` }).from(audits);
|
|
1360
|
+
const byEvent = await this.db.select({
|
|
1361
|
+
eventType: audits.eventType,
|
|
1362
|
+
count: sql`COUNT(*)`
|
|
1363
|
+
}).from(audits).groupBy(audits.eventType);
|
|
1364
|
+
const byObject = await this.db.select({
|
|
1365
|
+
objectType: audits.objectType,
|
|
1366
|
+
count: sql`COUNT(*)`
|
|
1367
|
+
}).from(audits).groupBy(audits.objectType);
|
|
1368
|
+
const recentActors = await this.db.selectDistinct({ actor: audits.actor }).from(audits).where(isNotNull(audits.actor)).orderBy(desc(audits.timestamp)).limit(10);
|
|
1369
|
+
const byEventType = {
|
|
1370
|
+
CREATE: 0,
|
|
1371
|
+
UPDATE: 0,
|
|
1372
|
+
DELETE: 0
|
|
1373
|
+
};
|
|
1374
|
+
for (const e of byEvent) {
|
|
1375
|
+
byEventType[e.eventType] = e.count;
|
|
1376
|
+
}
|
|
1377
|
+
const byObjectType = {
|
|
1378
|
+
task: 0,
|
|
1379
|
+
column: 0,
|
|
1380
|
+
board: 0
|
|
1381
|
+
};
|
|
1382
|
+
for (const o of byObject) {
|
|
1383
|
+
byObjectType[o.objectType] = o.count;
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
totalEntries: total[0]?.count ?? 0,
|
|
1387
|
+
byEventType,
|
|
1388
|
+
byObjectType,
|
|
1389
|
+
recentActors: recentActors.map((a) => a.actor).filter((a) => a !== null)
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
741
1393
|
// src/services/board.ts
|
|
742
1394
|
init_schema();
|
|
743
|
-
import { eq } from "drizzle-orm";
|
|
1395
|
+
import { eq as eq2 } from "drizzle-orm";
|
|
744
1396
|
import { ulid } from "ulid";
|
|
745
1397
|
|
|
746
1398
|
class BoardService {
|
|
@@ -771,6 +1423,7 @@ class BoardService {
|
|
|
771
1423
|
return {
|
|
772
1424
|
id: boardId,
|
|
773
1425
|
name: config.board.name,
|
|
1426
|
+
maxBoardTaskId: 0,
|
|
774
1427
|
createdAt: now,
|
|
775
1428
|
updatedAt: now
|
|
776
1429
|
};
|
|
@@ -783,23 +1436,493 @@ class BoardService {
|
|
|
783
1436
|
return this.db.select().from(columns).orderBy(columns.position);
|
|
784
1437
|
}
|
|
785
1438
|
async getColumn(id) {
|
|
786
|
-
const rows = await this.db.select().from(columns).where(
|
|
1439
|
+
const rows = await this.db.select().from(columns).where(eq2(columns.id, id));
|
|
787
1440
|
return rows[0] ?? null;
|
|
788
1441
|
}
|
|
789
1442
|
async getTerminalColumn() {
|
|
790
|
-
const rows = await this.db.select().from(columns).where(
|
|
1443
|
+
const rows = await this.db.select().from(columns).where(eq2(columns.isTerminal, true));
|
|
791
1444
|
return rows[0] ?? null;
|
|
792
1445
|
}
|
|
793
1446
|
async getTerminalColumns() {
|
|
794
|
-
return this.db.select().from(columns).where(
|
|
1447
|
+
return this.db.select().from(columns).where(eq2(columns.isTerminal, true)).orderBy(columns.position);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
// src/services/dependency.ts
|
|
1451
|
+
class DependencyService {
|
|
1452
|
+
getTask;
|
|
1453
|
+
constructor(getTask) {
|
|
1454
|
+
this.getTask = getTask;
|
|
1455
|
+
}
|
|
1456
|
+
async wouldCreateCycle(taskId, dependsOnId) {
|
|
1457
|
+
if (taskId === dependsOnId) {
|
|
1458
|
+
return { hasCycle: true, cyclePath: [taskId, taskId] };
|
|
1459
|
+
}
|
|
1460
|
+
const visited = new Set;
|
|
1461
|
+
const path = [];
|
|
1462
|
+
const dfs = async (currentId) => {
|
|
1463
|
+
if (currentId === taskId) {
|
|
1464
|
+
path.push(currentId);
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
if (visited.has(currentId)) {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
visited.add(currentId);
|
|
1471
|
+
path.push(currentId);
|
|
1472
|
+
const task = await this.getTask(currentId);
|
|
1473
|
+
if (!task?.dependsOn?.length) {
|
|
1474
|
+
path.pop();
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
for (const depId of task.dependsOn) {
|
|
1478
|
+
if (await dfs(depId)) {
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
path.pop();
|
|
1483
|
+
return false;
|
|
1484
|
+
};
|
|
1485
|
+
const hasCycle = await dfs(dependsOnId);
|
|
1486
|
+
if (hasCycle) {
|
|
1487
|
+
return { hasCycle: true, cyclePath: [taskId, ...path] };
|
|
1488
|
+
}
|
|
1489
|
+
return { hasCycle: false };
|
|
1490
|
+
}
|
|
1491
|
+
formatCyclePath(path, getShortId) {
|
|
1492
|
+
return path.map((id) => {
|
|
1493
|
+
const shortId = getShortId?.(id);
|
|
1494
|
+
return shortId ? `#${shortId}` : id.slice(0, 8);
|
|
1495
|
+
}).join(" → ");
|
|
795
1496
|
}
|
|
796
1497
|
}
|
|
1498
|
+
// src/services/link.ts
|
|
1499
|
+
init_schema();
|
|
1500
|
+
init_types();
|
|
1501
|
+
import { and as and2, eq as eq3, or } from "drizzle-orm";
|
|
1502
|
+
|
|
1503
|
+
class LinkService {
|
|
1504
|
+
db;
|
|
1505
|
+
constructor(db) {
|
|
1506
|
+
this.db = db;
|
|
1507
|
+
}
|
|
1508
|
+
async addLink(fromTaskId, toTaskId, type) {
|
|
1509
|
+
if (fromTaskId === toTaskId) {
|
|
1510
|
+
throw new KabanError("Task cannot link to itself", ExitCode.VALIDATION);
|
|
1511
|
+
}
|
|
1512
|
+
const now = new Date;
|
|
1513
|
+
await this.db.insert(taskLinks).values({
|
|
1514
|
+
fromTaskId,
|
|
1515
|
+
toTaskId,
|
|
1516
|
+
linkType: type,
|
|
1517
|
+
createdAt: now
|
|
1518
|
+
}).onConflictDoNothing();
|
|
1519
|
+
if (type === "blocks") {
|
|
1520
|
+
await this.db.insert(taskLinks).values({
|
|
1521
|
+
fromTaskId: toTaskId,
|
|
1522
|
+
toTaskId: fromTaskId,
|
|
1523
|
+
linkType: "blocked_by",
|
|
1524
|
+
createdAt: now
|
|
1525
|
+
}).onConflictDoNothing();
|
|
1526
|
+
} else if (type === "blocked_by") {
|
|
1527
|
+
await this.db.insert(taskLinks).values({
|
|
1528
|
+
fromTaskId: toTaskId,
|
|
1529
|
+
toTaskId: fromTaskId,
|
|
1530
|
+
linkType: "blocks",
|
|
1531
|
+
createdAt: now
|
|
1532
|
+
}).onConflictDoNothing();
|
|
1533
|
+
}
|
|
1534
|
+
const result = await this.db.select().from(taskLinks).where(and2(eq3(taskLinks.fromTaskId, fromTaskId), eq3(taskLinks.toTaskId, toTaskId), eq3(taskLinks.linkType, type))).limit(1);
|
|
1535
|
+
return result[0];
|
|
1536
|
+
}
|
|
1537
|
+
async removeLink(fromTaskId, toTaskId, type) {
|
|
1538
|
+
await this.db.delete(taskLinks).where(and2(eq3(taskLinks.fromTaskId, fromTaskId), eq3(taskLinks.toTaskId, toTaskId), eq3(taskLinks.linkType, type)));
|
|
1539
|
+
if (type === "blocks") {
|
|
1540
|
+
await this.db.delete(taskLinks).where(and2(eq3(taskLinks.fromTaskId, toTaskId), eq3(taskLinks.toTaskId, fromTaskId), eq3(taskLinks.linkType, "blocked_by")));
|
|
1541
|
+
} else if (type === "blocked_by") {
|
|
1542
|
+
await this.db.delete(taskLinks).where(and2(eq3(taskLinks.fromTaskId, toTaskId), eq3(taskLinks.toTaskId, fromTaskId), eq3(taskLinks.linkType, "blocks")));
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
async getLinksFrom(taskId, type) {
|
|
1546
|
+
if (type) {
|
|
1547
|
+
return this.db.select().from(taskLinks).where(and2(eq3(taskLinks.fromTaskId, taskId), eq3(taskLinks.linkType, type)));
|
|
1548
|
+
}
|
|
1549
|
+
return this.db.select().from(taskLinks).where(eq3(taskLinks.fromTaskId, taskId));
|
|
1550
|
+
}
|
|
1551
|
+
async getLinksTo(taskId, type) {
|
|
1552
|
+
if (type) {
|
|
1553
|
+
return this.db.select().from(taskLinks).where(and2(eq3(taskLinks.toTaskId, taskId), eq3(taskLinks.linkType, type)));
|
|
1554
|
+
}
|
|
1555
|
+
return this.db.select().from(taskLinks).where(eq3(taskLinks.toTaskId, taskId));
|
|
1556
|
+
}
|
|
1557
|
+
async getAllLinks(taskId) {
|
|
1558
|
+
return this.db.select().from(taskLinks).where(or(eq3(taskLinks.fromTaskId, taskId), eq3(taskLinks.toTaskId, taskId)));
|
|
1559
|
+
}
|
|
1560
|
+
async getBlockers(taskId) {
|
|
1561
|
+
const links = await this.getLinksFrom(taskId, "blocked_by");
|
|
1562
|
+
return links.map((l) => l.toTaskId);
|
|
1563
|
+
}
|
|
1564
|
+
async getBlocking(taskId) {
|
|
1565
|
+
const links = await this.getLinksFrom(taskId, "blocks");
|
|
1566
|
+
return links.map((l) => l.toTaskId);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
// src/services/markdown.ts
|
|
1570
|
+
class MarkdownService {
|
|
1571
|
+
exportBoard(board, columns2, tasksByColumn, options) {
|
|
1572
|
+
const lines = [];
|
|
1573
|
+
lines.push(`# ${escapeMarkdown(board.name)}`);
|
|
1574
|
+
lines.push("");
|
|
1575
|
+
const sortedColumns = [...columns2].sort((a, b) => a.position - b.position);
|
|
1576
|
+
for (const column of sortedColumns) {
|
|
1577
|
+
lines.push(`## ${escapeMarkdown(column.name)}`);
|
|
1578
|
+
if (column.wipLimit !== null) {
|
|
1579
|
+
lines.push(`<!-- WIP Limit: ${column.wipLimit} -->`);
|
|
1580
|
+
}
|
|
1581
|
+
if (column.isTerminal) {
|
|
1582
|
+
lines.push(`<!-- Terminal column -->`);
|
|
1583
|
+
}
|
|
1584
|
+
lines.push("");
|
|
1585
|
+
const tasks2 = tasksByColumn.get(column.id) || [];
|
|
1586
|
+
const sortedTasks = [...tasks2].sort((a, b) => a.position - b.position);
|
|
1587
|
+
for (const task of sortedTasks) {
|
|
1588
|
+
if (!options?.includeArchived && task.archived)
|
|
1589
|
+
continue;
|
|
1590
|
+
if (options?.includeMetadata) {
|
|
1591
|
+
lines.push(`- ${escapeMarkdown(task.title)} <!-- id:${task.id} -->`);
|
|
1592
|
+
} else {
|
|
1593
|
+
lines.push(`- ${escapeMarkdown(task.title)}`);
|
|
1594
|
+
}
|
|
1595
|
+
if (task.dueDate) {
|
|
1596
|
+
const checkmark = task.completedAt ? " ✓" : "";
|
|
1597
|
+
lines.push(` @ ${formatDate(task.dueDate)}${checkmark}`);
|
|
1598
|
+
}
|
|
1599
|
+
if (task.labels && task.labels.length > 0) {
|
|
1600
|
+
lines.push(` # ${task.labels.join(", ")}`);
|
|
1601
|
+
}
|
|
1602
|
+
if (task.assignedTo) {
|
|
1603
|
+
lines.push(` @ assigned: ${task.assignedTo}`);
|
|
1604
|
+
}
|
|
1605
|
+
if (task.description) {
|
|
1606
|
+
const descLines = task.description.split(`
|
|
1607
|
+
`);
|
|
1608
|
+
for (const line of descLines) {
|
|
1609
|
+
lines.push(` > ${escapeMarkdown(line)}`);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
lines.push("");
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return lines.join(`
|
|
1616
|
+
`).trimEnd() + `
|
|
1617
|
+
`;
|
|
1618
|
+
}
|
|
1619
|
+
parseMarkdown(content) {
|
|
1620
|
+
const lines = content.split(`
|
|
1621
|
+
`);
|
|
1622
|
+
const errors = [];
|
|
1623
|
+
let boardName = "Imported Board";
|
|
1624
|
+
const columns2 = [];
|
|
1625
|
+
let currentColumn = null;
|
|
1626
|
+
let currentTask = null;
|
|
1627
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1628
|
+
const line = lines[i];
|
|
1629
|
+
const lineNum = i + 1;
|
|
1630
|
+
if (line.startsWith("# ") && !line.startsWith("## ")) {
|
|
1631
|
+
boardName = unescapeMarkdown(line.slice(2).trim());
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
if (line.startsWith("## ")) {
|
|
1635
|
+
if (currentTask && currentColumn) {
|
|
1636
|
+
currentColumn.tasks.push(currentTask);
|
|
1637
|
+
currentTask = null;
|
|
1638
|
+
}
|
|
1639
|
+
if (currentColumn) {
|
|
1640
|
+
columns2.push(currentColumn);
|
|
1641
|
+
}
|
|
1642
|
+
currentColumn = {
|
|
1643
|
+
name: unescapeMarkdown(line.slice(3).trim()),
|
|
1644
|
+
tasks: []
|
|
1645
|
+
};
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
if (line.includes("WIP Limit:") && currentColumn) {
|
|
1649
|
+
const match = line.match(/WIP Limit:\s*(\d+|none)/i);
|
|
1650
|
+
if (match) {
|
|
1651
|
+
currentColumn.wipLimit = match[1].toLowerCase() === "none" ? undefined : parseInt(match[1], 10);
|
|
1652
|
+
}
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
if (line.includes("Terminal column") && currentColumn) {
|
|
1656
|
+
currentColumn.isTerminal = true;
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (line.startsWith("- ")) {
|
|
1660
|
+
if (currentTask && currentColumn) {
|
|
1661
|
+
currentColumn.tasks.push(currentTask);
|
|
1662
|
+
}
|
|
1663
|
+
let title = line.slice(2);
|
|
1664
|
+
let id;
|
|
1665
|
+
const idMatch = title.match(/<!--\s*id:([^\s]+)\s*-->/);
|
|
1666
|
+
if (idMatch) {
|
|
1667
|
+
id = idMatch[1];
|
|
1668
|
+
title = title.replace(idMatch[0], "").trim();
|
|
1669
|
+
}
|
|
1670
|
+
currentTask = {
|
|
1671
|
+
title: unescapeMarkdown(title),
|
|
1672
|
+
id,
|
|
1673
|
+
description: null,
|
|
1674
|
+
dueDate: null,
|
|
1675
|
+
labels: []
|
|
1676
|
+
};
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
const indentMatch = line.match(/^(\s{4}|\t)/);
|
|
1680
|
+
if (indentMatch && currentTask) {
|
|
1681
|
+
const content2 = line.slice(indentMatch[0].length);
|
|
1682
|
+
if (content2.startsWith("@ ") && !content2.includes("assigned:")) {
|
|
1683
|
+
const dueLine = content2.slice(2).trim();
|
|
1684
|
+
const dateStr = dueLine.replace("✓", "").trim();
|
|
1685
|
+
const parsed = parseISODate(dateStr);
|
|
1686
|
+
if (parsed) {
|
|
1687
|
+
currentTask.dueDate = parsed;
|
|
1688
|
+
} else {
|
|
1689
|
+
errors.push(`Line ${lineNum}: Invalid date format "${dateStr}"`);
|
|
1690
|
+
}
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
if (content2.includes("assigned:")) {
|
|
1694
|
+
const match = content2.match(/assigned:\s*(\S+)/);
|
|
1695
|
+
if (match) {
|
|
1696
|
+
currentTask.assignedTo = match[1];
|
|
1697
|
+
}
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
if (content2.startsWith("# ")) {
|
|
1701
|
+
const labelStr = content2.slice(2).trim();
|
|
1702
|
+
currentTask.labels = labelStr.split(",").map((l) => l.trim()).filter(Boolean);
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1705
|
+
if (content2.startsWith("> ")) {
|
|
1706
|
+
const descLine = unescapeMarkdown(content2.slice(2));
|
|
1707
|
+
if (currentTask.description === null) {
|
|
1708
|
+
currentTask.description = descLine;
|
|
1709
|
+
} else {
|
|
1710
|
+
currentTask.description += `
|
|
1711
|
+
` + descLine;
|
|
1712
|
+
}
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (currentTask && currentColumn) {
|
|
1718
|
+
currentColumn.tasks.push(currentTask);
|
|
1719
|
+
}
|
|
1720
|
+
if (currentColumn) {
|
|
1721
|
+
columns2.push(currentColumn);
|
|
1722
|
+
}
|
|
1723
|
+
return { boardName, columns: columns2, errors };
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function escapeMarkdown(text2) {
|
|
1727
|
+
return text2.replace(/\\/g, "\\\\").replace(/\n/g, " ").replace(/<!--/g, "\\<!--");
|
|
1728
|
+
}
|
|
1729
|
+
function unescapeMarkdown(text2) {
|
|
1730
|
+
return text2.replace(/\\<!--/g, "<!--").replace(/\\\\/g, "\\");
|
|
1731
|
+
}
|
|
1732
|
+
function formatDate(date) {
|
|
1733
|
+
return date.toISOString().split("T")[0];
|
|
1734
|
+
}
|
|
1735
|
+
function parseISODate(str) {
|
|
1736
|
+
const match = str.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
1737
|
+
if (!match)
|
|
1738
|
+
return null;
|
|
1739
|
+
const date = new Date(str + "T00:00:00.000Z");
|
|
1740
|
+
if (isNaN(date.getTime()))
|
|
1741
|
+
return null;
|
|
1742
|
+
return date;
|
|
1743
|
+
}
|
|
1744
|
+
// src/services/scoring/scorers/fifo.ts
|
|
1745
|
+
var fifoScorer = {
|
|
1746
|
+
name: "fifo",
|
|
1747
|
+
description: "First In, First Out - oldest tasks scored higher",
|
|
1748
|
+
units: "age",
|
|
1749
|
+
async score(task) {
|
|
1750
|
+
const ageMs = Date.now() - task.createdAt.getTime();
|
|
1751
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
1752
|
+
return ageDays;
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
// src/services/scoring/scorers/priority.ts
|
|
1756
|
+
var PRIORITY_WEIGHTS = {
|
|
1757
|
+
critical: 1000,
|
|
1758
|
+
urgent: 500,
|
|
1759
|
+
high: 100,
|
|
1760
|
+
medium: 50,
|
|
1761
|
+
low: 10,
|
|
1762
|
+
p0: 1000,
|
|
1763
|
+
p1: 500,
|
|
1764
|
+
p2: 100,
|
|
1765
|
+
p3: 50,
|
|
1766
|
+
p4: 10
|
|
1767
|
+
};
|
|
1768
|
+
var priorityScorer = {
|
|
1769
|
+
name: "priority",
|
|
1770
|
+
description: "Score based on priority labels (critical, urgent, high, p0-p4)",
|
|
1771
|
+
units: "priority",
|
|
1772
|
+
async score(task) {
|
|
1773
|
+
let maxPriority = 0;
|
|
1774
|
+
for (const label of task.labels) {
|
|
1775
|
+
const weight = PRIORITY_WEIGHTS[label.toLowerCase()];
|
|
1776
|
+
if (weight && weight > maxPriority) {
|
|
1777
|
+
maxPriority = weight;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return maxPriority;
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
// src/services/scoring/scorers/due-date.ts
|
|
1784
|
+
var dueDateScorer = {
|
|
1785
|
+
name: "due-date",
|
|
1786
|
+
description: "Score based on due date urgency (closer/overdue = higher)",
|
|
1787
|
+
units: "urgency",
|
|
1788
|
+
async score(task) {
|
|
1789
|
+
if (!task.dueDate) {
|
|
1790
|
+
return 0;
|
|
1791
|
+
}
|
|
1792
|
+
const now = Date.now();
|
|
1793
|
+
const dueTime = task.dueDate.getTime();
|
|
1794
|
+
const daysUntilDue = (dueTime - now) / (1000 * 60 * 60 * 24);
|
|
1795
|
+
if (daysUntilDue <= 0) {
|
|
1796
|
+
return 1000 + Math.abs(daysUntilDue) * 10;
|
|
1797
|
+
}
|
|
1798
|
+
if (daysUntilDue <= 1) {
|
|
1799
|
+
return 500;
|
|
1800
|
+
}
|
|
1801
|
+
if (daysUntilDue <= 7) {
|
|
1802
|
+
return 100 + (7 - daysUntilDue) * 10;
|
|
1803
|
+
}
|
|
1804
|
+
return Math.max(0, 50 - daysUntilDue);
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
// src/services/scoring/scorers/blocking.ts
|
|
1808
|
+
function createBlockingScorer(getBlockingCount) {
|
|
1809
|
+
return {
|
|
1810
|
+
name: "blocking",
|
|
1811
|
+
description: "Score based on how many tasks this blocks",
|
|
1812
|
+
units: "blocked",
|
|
1813
|
+
async score(task) {
|
|
1814
|
+
const blockingCount = await getBlockingCount(task.id);
|
|
1815
|
+
return blockingCount * 50;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// src/services/scoring/index.ts
|
|
1821
|
+
class ScoringService {
|
|
1822
|
+
scorers = [];
|
|
1823
|
+
addScorer(scorer) {
|
|
1824
|
+
this.scorers.push(scorer);
|
|
1825
|
+
}
|
|
1826
|
+
removeScorer(name) {
|
|
1827
|
+
this.scorers = this.scorers.filter((s) => s.name !== name);
|
|
1828
|
+
}
|
|
1829
|
+
getScorers() {
|
|
1830
|
+
return [...this.scorers];
|
|
1831
|
+
}
|
|
1832
|
+
async scoreTask(task) {
|
|
1833
|
+
const breakdown = {};
|
|
1834
|
+
let totalScore = 0;
|
|
1835
|
+
for (const scorer of this.scorers) {
|
|
1836
|
+
const score = await scorer.score(task);
|
|
1837
|
+
breakdown[scorer.name] = score;
|
|
1838
|
+
totalScore += score;
|
|
1839
|
+
}
|
|
1840
|
+
return {
|
|
1841
|
+
task,
|
|
1842
|
+
score: totalScore,
|
|
1843
|
+
breakdown
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
async rankTasks(tasks2) {
|
|
1847
|
+
const scored = await Promise.all(tasks2.map((task) => this.scoreTask(task)));
|
|
1848
|
+
return scored.sort((a, b) => b.score - a.score);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function createDefaultScoringService() {
|
|
1852
|
+
return new ScoringService;
|
|
1853
|
+
}
|
|
797
1854
|
// src/services/task.ts
|
|
798
1855
|
init_schema();
|
|
799
1856
|
init_types();
|
|
800
|
-
import { and, eq as
|
|
1857
|
+
import { and as and3, eq as eq4, inArray, lt, sql as sql2 } from "drizzle-orm";
|
|
801
1858
|
import { ulid as ulid2 } from "ulid";
|
|
802
1859
|
|
|
1860
|
+
// src/utils/date-parser.ts
|
|
1861
|
+
import * as chrono from "chrono-node";
|
|
1862
|
+
|
|
1863
|
+
class DateParseError extends Error {
|
|
1864
|
+
input;
|
|
1865
|
+
suggestion;
|
|
1866
|
+
constructor(input, suggestion) {
|
|
1867
|
+
super(`Unable to parse date: "${input}"${suggestion ? `. Did you mean: ${suggestion}?` : ""}`);
|
|
1868
|
+
this.input = input;
|
|
1869
|
+
this.suggestion = suggestion;
|
|
1870
|
+
this.name = "DateParseError";
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
var DURATION_REGEX = /^(\d+w)?\s*(\d+d)?\s*(\d+h)?\s*(\d+m)?$/i;
|
|
1874
|
+
function parseDate2(input, referenceDate) {
|
|
1875
|
+
const trimmed = input.trim().toLowerCase();
|
|
1876
|
+
if (!trimmed) {
|
|
1877
|
+
throw new DateParseError(input);
|
|
1878
|
+
}
|
|
1879
|
+
if (/^-\d|in\s+-\d/i.test(trimmed)) {
|
|
1880
|
+
throw new DateParseError(input, "Negative values not supported. Use positive durations.");
|
|
1881
|
+
}
|
|
1882
|
+
const durationMatch = trimmed.match(DURATION_REGEX);
|
|
1883
|
+
if (durationMatch && (durationMatch[1] || durationMatch[2] || durationMatch[3] || durationMatch[4])) {
|
|
1884
|
+
const weeks = parseInt(durationMatch[1] || "0", 10);
|
|
1885
|
+
const days = parseInt(durationMatch[2] || "0", 10);
|
|
1886
|
+
const hours = parseInt(durationMatch[3] || "0", 10);
|
|
1887
|
+
const minutes = parseInt(durationMatch[4] || "0", 10);
|
|
1888
|
+
const ref2 = referenceDate ?? new Date;
|
|
1889
|
+
const result = new Date(ref2);
|
|
1890
|
+
result.setDate(result.getDate() + weeks * 7 + days);
|
|
1891
|
+
result.setHours(result.getHours() + hours);
|
|
1892
|
+
result.setMinutes(result.getMinutes() + minutes);
|
|
1893
|
+
return { date: result, hasTime: hours > 0 || minutes > 0 };
|
|
1894
|
+
}
|
|
1895
|
+
const ref = referenceDate ?? new Date;
|
|
1896
|
+
const parsed = chrono.parseDate(input, ref, { forwardDate: true });
|
|
1897
|
+
if (!parsed) {
|
|
1898
|
+
const suggestions = {
|
|
1899
|
+
tmrw: "tomorrow",
|
|
1900
|
+
tom: "tomorrow",
|
|
1901
|
+
tomorow: "tomorrow",
|
|
1902
|
+
nxt: "next",
|
|
1903
|
+
wk: "week"
|
|
1904
|
+
};
|
|
1905
|
+
const words = trimmed.split(/\s+/);
|
|
1906
|
+
for (const word of words) {
|
|
1907
|
+
if (suggestions[word]) {
|
|
1908
|
+
throw new DateParseError(input, input.replace(word, suggestions[word]));
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
throw new DateParseError(input);
|
|
1912
|
+
}
|
|
1913
|
+
const hasTime = /\d{1,2}:\d{2}|(\d{1,2})\s*(am|pm)/i.test(input) || /at\s+\d/i.test(input);
|
|
1914
|
+
return { date: parsed, hasTime };
|
|
1915
|
+
}
|
|
1916
|
+
function parseDateOrNull(input, referenceDate) {
|
|
1917
|
+
if (!input?.trim())
|
|
1918
|
+
return null;
|
|
1919
|
+
try {
|
|
1920
|
+
return parseDate2(input, referenceDate).date;
|
|
1921
|
+
} catch {
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
803
1926
|
// src/utils/similarity.ts
|
|
804
1927
|
function tokenize(text2) {
|
|
805
1928
|
const words = text2.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((word) => word.length > 0);
|
|
@@ -855,9 +1978,11 @@ class TaskService {
|
|
|
855
1978
|
threshold: 0.5,
|
|
856
1979
|
warnThreshold: 0.5
|
|
857
1980
|
};
|
|
1981
|
+
depService;
|
|
858
1982
|
constructor(db, boardService) {
|
|
859
1983
|
this.db = db;
|
|
860
1984
|
this.boardService = boardService;
|
|
1985
|
+
this.depService = new DependencyService(this.getTask.bind(this));
|
|
861
1986
|
}
|
|
862
1987
|
async getTaskOrThrow(id) {
|
|
863
1988
|
const task = await this.getTask(id);
|
|
@@ -870,61 +1995,80 @@ class TaskService {
|
|
|
870
1995
|
const title = validateTitle(input.title);
|
|
871
1996
|
const createdBy = input.createdBy ? validateAgentName(input.createdBy) : input.agent ? validateAgentName(input.agent) : "user";
|
|
872
1997
|
const columnId = input.columnId ? validateColumnId(input.columnId) : "todo";
|
|
1998
|
+
const dueDate = parseDateOrNull(input.dueDate);
|
|
873
1999
|
const column = await this.boardService.getColumn(columnId);
|
|
874
2000
|
if (!column) {
|
|
875
2001
|
throw new KabanError(`Column '${columnId}' does not exist`, ExitCode.VALIDATION);
|
|
876
2002
|
}
|
|
2003
|
+
const board = await this.boardService.getBoard();
|
|
2004
|
+
if (!board) {
|
|
2005
|
+
throw new KabanError("No board found", ExitCode.NOT_FOUND);
|
|
2006
|
+
}
|
|
877
2007
|
const now = new Date;
|
|
878
2008
|
const id = ulid2();
|
|
879
|
-
const maxPositionResult = await this.db.select({ max:
|
|
2009
|
+
const maxPositionResult = await this.db.select({ max: sql2`COALESCE(MAX(position), -1)` }).from(tasks).where(eq4(tasks.columnId, columnId));
|
|
880
2010
|
const position = (maxPositionResult[0]?.max ?? -1) + 1;
|
|
881
|
-
await this.db.
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
2011
|
+
await this.db.run(sql2`
|
|
2012
|
+
UPDATE boards
|
|
2013
|
+
SET max_board_task_id = max_board_task_id + 1
|
|
2014
|
+
WHERE id = ${board.id}
|
|
2015
|
+
`);
|
|
2016
|
+
await this.db.run(sql2`
|
|
2017
|
+
INSERT INTO tasks (
|
|
2018
|
+
id, board_task_id, title, description, column_id, position,
|
|
2019
|
+
created_by, assigned_to, parent_id, depends_on, files, labels,
|
|
2020
|
+
blocked_reason, version, created_at, updated_at, started_at,
|
|
2021
|
+
completed_at, due_date, archived, archived_at
|
|
2022
|
+
) VALUES (
|
|
2023
|
+
${id},
|
|
2024
|
+
(SELECT max_board_task_id FROM boards WHERE id = ${board.id}),
|
|
2025
|
+
${title},
|
|
2026
|
+
${input.description ?? null},
|
|
2027
|
+
${columnId},
|
|
2028
|
+
${position},
|
|
2029
|
+
${createdBy},
|
|
2030
|
+
${null},
|
|
2031
|
+
${null},
|
|
2032
|
+
${JSON.stringify(input.dependsOn ?? [])},
|
|
2033
|
+
${JSON.stringify(input.files ?? [])},
|
|
2034
|
+
${JSON.stringify(input.labels ?? [])},
|
|
2035
|
+
${null},
|
|
2036
|
+
${1},
|
|
2037
|
+
${Math.floor(now.getTime() / 1000)},
|
|
2038
|
+
${Math.floor(now.getTime() / 1000)},
|
|
2039
|
+
${null},
|
|
2040
|
+
${null},
|
|
2041
|
+
${dueDate ? Math.floor(dueDate.getTime() / 1000) : null},
|
|
2042
|
+
${0},
|
|
2043
|
+
${null}
|
|
2044
|
+
)
|
|
2045
|
+
`);
|
|
902
2046
|
return this.getTaskOrThrow(id);
|
|
903
2047
|
}
|
|
904
2048
|
async getTask(id) {
|
|
905
|
-
const rows = await this.db.select().from(tasks).where(
|
|
2049
|
+
const rows = await this.db.select().from(tasks).where(eq4(tasks.id, id));
|
|
906
2050
|
return rows[0] ?? null;
|
|
907
2051
|
}
|
|
908
2052
|
async listTasks(filter) {
|
|
909
2053
|
const conditions = [];
|
|
910
2054
|
if (!filter?.includeArchived) {
|
|
911
|
-
conditions.push(
|
|
2055
|
+
conditions.push(eq4(tasks.archived, false));
|
|
912
2056
|
}
|
|
913
2057
|
if (filter?.columnId) {
|
|
914
|
-
conditions.push(
|
|
2058
|
+
conditions.push(eq4(tasks.columnId, filter.columnId));
|
|
915
2059
|
}
|
|
916
2060
|
const creatorFilter = filter?.createdBy ?? filter?.agent;
|
|
917
2061
|
if (creatorFilter) {
|
|
918
|
-
conditions.push(
|
|
2062
|
+
conditions.push(eq4(tasks.createdBy, creatorFilter));
|
|
919
2063
|
}
|
|
920
2064
|
if (filter?.assignee) {
|
|
921
|
-
conditions.push(
|
|
2065
|
+
conditions.push(eq4(tasks.assignedTo, filter.assignee));
|
|
922
2066
|
}
|
|
923
2067
|
if (filter?.blocked === true) {
|
|
924
|
-
conditions.push(
|
|
2068
|
+
conditions.push(sql2`${tasks.blockedReason} IS NOT NULL`);
|
|
925
2069
|
}
|
|
926
2070
|
if (conditions.length > 0) {
|
|
927
|
-
return this.db.select().from(tasks).where(
|
|
2071
|
+
return this.db.select().from(tasks).where(and3(...conditions)).orderBy(tasks.columnId, tasks.position);
|
|
928
2072
|
}
|
|
929
2073
|
return this.db.select().from(tasks).orderBy(tasks.columnId, tasks.position);
|
|
930
2074
|
}
|
|
@@ -933,7 +2077,7 @@ class TaskService {
|
|
|
933
2077
|
if (!task) {
|
|
934
2078
|
throw new KabanError(`Task '${id}' not found`, ExitCode.NOT_FOUND);
|
|
935
2079
|
}
|
|
936
|
-
await this.db.delete(tasks).where(
|
|
2080
|
+
await this.db.delete(tasks).where(eq4(tasks.id, id));
|
|
937
2081
|
}
|
|
938
2082
|
async moveTask(id, columnId, options) {
|
|
939
2083
|
const task = await this.getTask(id);
|
|
@@ -959,7 +2103,7 @@ class TaskService {
|
|
|
959
2103
|
}
|
|
960
2104
|
const now = new Date;
|
|
961
2105
|
const isTerminal = column.isTerminal;
|
|
962
|
-
const maxPositionResult = await this.db.select({ max:
|
|
2106
|
+
const maxPositionResult = await this.db.select({ max: sql2`COALESCE(MAX(position), -1)` }).from(tasks).where(eq4(tasks.columnId, columnId));
|
|
963
2107
|
const newPosition = (maxPositionResult[0]?.max ?? -1) + 1;
|
|
964
2108
|
await this.db.update(tasks).set({
|
|
965
2109
|
columnId,
|
|
@@ -967,11 +2111,12 @@ class TaskService {
|
|
|
967
2111
|
version: task.version + 1,
|
|
968
2112
|
updatedAt: now,
|
|
969
2113
|
completedAt: isTerminal ? now : task.completedAt,
|
|
970
|
-
startedAt: columnId === "in_progress" && !task.startedAt ? now : task.startedAt
|
|
971
|
-
|
|
2114
|
+
startedAt: columnId === "in_progress" && !task.startedAt ? now : task.startedAt,
|
|
2115
|
+
updatedBy: options?.actor ?? null
|
|
2116
|
+
}).where(eq4(tasks.id, id));
|
|
972
2117
|
return this.getTaskOrThrow(id);
|
|
973
2118
|
}
|
|
974
|
-
async updateTask(id, input, expectedVersion) {
|
|
2119
|
+
async updateTask(id, input, expectedVersion, actor) {
|
|
975
2120
|
const task = await this.getTask(id);
|
|
976
2121
|
if (!task) {
|
|
977
2122
|
throw new KabanError(`Task '${id}' not found`, ExitCode.NOT_FOUND);
|
|
@@ -981,7 +2126,8 @@ class TaskService {
|
|
|
981
2126
|
}
|
|
982
2127
|
const updates = {
|
|
983
2128
|
version: task.version + 1,
|
|
984
|
-
updatedAt: new Date
|
|
2129
|
+
updatedAt: new Date,
|
|
2130
|
+
updatedBy: actor ?? null
|
|
985
2131
|
};
|
|
986
2132
|
if (input.title !== undefined) {
|
|
987
2133
|
updates.title = validateTitle(input.title);
|
|
@@ -998,11 +2144,14 @@ class TaskService {
|
|
|
998
2144
|
if (input.labels !== undefined) {
|
|
999
2145
|
updates.labels = input.labels;
|
|
1000
2146
|
}
|
|
1001
|
-
|
|
2147
|
+
if (input.dueDate !== undefined) {
|
|
2148
|
+
updates.dueDate = input.dueDate ? parseDateOrNull(input.dueDate) : null;
|
|
2149
|
+
}
|
|
2150
|
+
await this.db.update(tasks).set(updates).where(expectedVersion !== undefined ? and3(eq4(tasks.id, id), eq4(tasks.version, expectedVersion)) : eq4(tasks.id, id));
|
|
1002
2151
|
return this.getTaskOrThrow(id);
|
|
1003
2152
|
}
|
|
1004
2153
|
async getTaskCountInColumn(columnId) {
|
|
1005
|
-
const result = await this.db.select({ count:
|
|
2154
|
+
const result = await this.db.select({ count: sql2`COUNT(*)` }).from(tasks).where(eq4(tasks.columnId, columnId));
|
|
1006
2155
|
return result[0]?.count ?? 0;
|
|
1007
2156
|
}
|
|
1008
2157
|
async archiveTasks(criteria) {
|
|
@@ -1010,9 +2159,9 @@ class TaskService {
|
|
|
1010
2159
|
if (!hasCriteria) {
|
|
1011
2160
|
throw new KabanError("At least one criteria must be provided", ExitCode.VALIDATION);
|
|
1012
2161
|
}
|
|
1013
|
-
const conditions = [
|
|
2162
|
+
const conditions = [eq4(tasks.archived, false)];
|
|
1014
2163
|
if (criteria.status) {
|
|
1015
|
-
conditions.push(
|
|
2164
|
+
conditions.push(eq4(tasks.columnId, criteria.status));
|
|
1016
2165
|
}
|
|
1017
2166
|
if (criteria.olderThan) {
|
|
1018
2167
|
conditions.push(lt(tasks.createdAt, criteria.olderThan));
|
|
@@ -1020,7 +2169,7 @@ class TaskService {
|
|
|
1020
2169
|
if (criteria.taskIds?.length) {
|
|
1021
2170
|
conditions.push(inArray(tasks.id, criteria.taskIds));
|
|
1022
2171
|
}
|
|
1023
|
-
const matchingTasks = await this.db.select({ id: tasks.id }).from(tasks).where(
|
|
2172
|
+
const matchingTasks = await this.db.select({ id: tasks.id }).from(tasks).where(and3(...conditions));
|
|
1024
2173
|
if (matchingTasks.length === 0) {
|
|
1025
2174
|
return { archivedCount: 0, taskIds: [] };
|
|
1026
2175
|
}
|
|
@@ -1059,16 +2208,16 @@ class TaskService {
|
|
|
1059
2208
|
columnId,
|
|
1060
2209
|
version: task.version + 1,
|
|
1061
2210
|
updatedAt: now
|
|
1062
|
-
}).where(
|
|
2211
|
+
}).where(eq4(tasks.id, taskId));
|
|
1063
2212
|
return this.getTaskOrThrow(taskId);
|
|
1064
2213
|
}
|
|
1065
2214
|
async searchArchive(query, options) {
|
|
1066
2215
|
const limit = options?.limit ?? 50;
|
|
1067
2216
|
const offset = options?.offset ?? 0;
|
|
1068
2217
|
if (!query.trim()) {
|
|
1069
|
-
const countResult = await this.db.select({ count:
|
|
2218
|
+
const countResult = await this.db.select({ count: sql2`COUNT(*)` }).from(tasks).where(eq4(tasks.archived, true));
|
|
1070
2219
|
const total2 = countResult[0]?.count ?? 0;
|
|
1071
|
-
const archivedTasks = await this.db.select().from(tasks).where(
|
|
2220
|
+
const archivedTasks = await this.db.select().from(tasks).where(eq4(tasks.archived, true)).orderBy(tasks.archivedAt).limit(limit).offset(offset);
|
|
1072
2221
|
return { tasks: archivedTasks, total: total2 };
|
|
1073
2222
|
}
|
|
1074
2223
|
const ftsQuery = query.trim().split(/\s+/).map((term) => `"${term}"`).join(" ");
|
|
@@ -1086,6 +2235,7 @@ class TaskService {
|
|
|
1086
2235
|
LIMIT ? OFFSET ?`).all(ftsQuery, limit, offset);
|
|
1087
2236
|
const searchTasks = rows.map((row) => ({
|
|
1088
2237
|
id: row.id,
|
|
2238
|
+
boardTaskId: row.board_task_id,
|
|
1089
2239
|
title: row.title,
|
|
1090
2240
|
description: row.description,
|
|
1091
2241
|
columnId: row.column_id,
|
|
@@ -1102,17 +2252,19 @@ class TaskService {
|
|
|
1102
2252
|
updatedAt: new Date(row.updated_at),
|
|
1103
2253
|
startedAt: row.started_at ? new Date(row.started_at) : null,
|
|
1104
2254
|
completedAt: row.completed_at ? new Date(row.completed_at) : null,
|
|
2255
|
+
dueDate: row.due_date ? new Date(row.due_date * 1000) : null,
|
|
1105
2256
|
archived: Boolean(row.archived),
|
|
1106
|
-
archivedAt: row.archived_at ? new Date(row.archived_at) : null
|
|
2257
|
+
archivedAt: row.archived_at ? new Date(row.archived_at) : null,
|
|
2258
|
+
updatedBy: row.updated_by
|
|
1107
2259
|
}));
|
|
1108
2260
|
return { tasks: searchTasks, total };
|
|
1109
2261
|
}
|
|
1110
2262
|
async purgeArchive(criteria) {
|
|
1111
|
-
const conditions = [
|
|
2263
|
+
const conditions = [eq4(tasks.archived, true)];
|
|
1112
2264
|
if (criteria?.olderThan) {
|
|
1113
2265
|
conditions.push(lt(tasks.archivedAt, criteria.olderThan));
|
|
1114
2266
|
}
|
|
1115
|
-
const matchingTasks = await this.db.select({ id: tasks.id }).from(tasks).where(
|
|
2267
|
+
const matchingTasks = await this.db.select({ id: tasks.id }).from(tasks).where(and3(...conditions));
|
|
1116
2268
|
if (matchingTasks.length === 0) {
|
|
1117
2269
|
return { deletedCount: 0 };
|
|
1118
2270
|
}
|
|
@@ -1125,7 +2277,7 @@ class TaskService {
|
|
|
1125
2277
|
if (allTasks.length === 0) {
|
|
1126
2278
|
return { deletedCount: 0 };
|
|
1127
2279
|
}
|
|
1128
|
-
await this.db.delete(tasks)
|
|
2280
|
+
await this.db.delete(tasks);
|
|
1129
2281
|
return { deletedCount: allTasks.length };
|
|
1130
2282
|
}
|
|
1131
2283
|
async addDependency(taskId, dependsOnId) {
|
|
@@ -1137,13 +2289,18 @@ class TaskService {
|
|
|
1137
2289
|
if (task.dependsOn.includes(dependsOnId)) {
|
|
1138
2290
|
return task;
|
|
1139
2291
|
}
|
|
2292
|
+
const cycleCheck = await this.depService.wouldCreateCycle(taskId, dependsOnId);
|
|
2293
|
+
if (cycleCheck.hasCycle && cycleCheck.cyclePath) {
|
|
2294
|
+
const cyclePath = this.depService.formatCyclePath(cycleCheck.cyclePath);
|
|
2295
|
+
throw new KabanError(`Cannot add dependency - would create cycle: ${cyclePath}`, ExitCode.VALIDATION);
|
|
2296
|
+
}
|
|
1140
2297
|
const updatedDependsOn = [...task.dependsOn, dependsOnId];
|
|
1141
2298
|
const now = new Date;
|
|
1142
2299
|
await this.db.update(tasks).set({
|
|
1143
2300
|
dependsOn: updatedDependsOn,
|
|
1144
2301
|
version: task.version + 1,
|
|
1145
2302
|
updatedAt: now
|
|
1146
|
-
}).where(
|
|
2303
|
+
}).where(eq4(tasks.id, taskId));
|
|
1147
2304
|
return this.getTaskOrThrow(taskId);
|
|
1148
2305
|
}
|
|
1149
2306
|
async removeDependency(taskId, dependsOnId) {
|
|
@@ -1157,7 +2314,7 @@ class TaskService {
|
|
|
1157
2314
|
dependsOn: updatedDependsOn,
|
|
1158
2315
|
version: task.version + 1,
|
|
1159
2316
|
updatedAt: now
|
|
1160
|
-
}).where(
|
|
2317
|
+
}).where(eq4(tasks.id, taskId));
|
|
1161
2318
|
return this.getTaskOrThrow(taskId);
|
|
1162
2319
|
}
|
|
1163
2320
|
async validateDependencies(taskId) {
|
|
@@ -1213,7 +2370,7 @@ class TaskService {
|
|
|
1213
2370
|
};
|
|
1214
2371
|
}
|
|
1215
2372
|
async getArchiveStats() {
|
|
1216
|
-
const archived = await this.db.select().from(tasks).where(
|
|
2373
|
+
const archived = await this.db.select().from(tasks).where(eq4(tasks.archived, true));
|
|
1217
2374
|
const byColumn = {};
|
|
1218
2375
|
let oldestArchivedAt = null;
|
|
1219
2376
|
for (const task of archived) {
|
|
@@ -1228,6 +2385,37 @@ class TaskService {
|
|
|
1228
2385
|
oldestArchivedAt
|
|
1229
2386
|
};
|
|
1230
2387
|
}
|
|
2388
|
+
async resolveTask(idOrShortId, boardId) {
|
|
2389
|
+
const cleanId = idOrShortId.replace(/^#/, "").trim();
|
|
2390
|
+
if (/^\d+$/.test(cleanId)) {
|
|
2391
|
+
const shortId = parseInt(cleanId, 10);
|
|
2392
|
+
const board = boardId ? { id: boardId } : await this.boardService.getBoard();
|
|
2393
|
+
if (board) {
|
|
2394
|
+
return this.getTaskByBoardTaskId(board.id, shortId);
|
|
2395
|
+
}
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
if (/^[0-9A-Z]{26}$/i.test(cleanId)) {
|
|
2399
|
+
return this.getTask(cleanId);
|
|
2400
|
+
}
|
|
2401
|
+
if (cleanId.length >= 4 && /^[0-9A-Z]+$/i.test(cleanId)) {
|
|
2402
|
+
return this.getTaskByIdPrefix(cleanId);
|
|
2403
|
+
}
|
|
2404
|
+
return null;
|
|
2405
|
+
}
|
|
2406
|
+
async getTaskByBoardTaskId(boardId, boardTaskId) {
|
|
2407
|
+
const result = await this.db.select().from(tasks).innerJoin(columns, eq4(tasks.columnId, columns.id)).where(and3(eq4(columns.boardId, boardId), eq4(tasks.boardTaskId, boardTaskId))).limit(1);
|
|
2408
|
+
return result[0]?.tasks ?? null;
|
|
2409
|
+
}
|
|
2410
|
+
async getTaskByIdPrefix(prefix) {
|
|
2411
|
+
const result = await this.db.select().from(tasks).where(sql2`${tasks.id} LIKE ${prefix.toUpperCase() + "%"}`).limit(2);
|
|
2412
|
+
if (result.length === 0)
|
|
2413
|
+
return null;
|
|
2414
|
+
if (result.length > 1) {
|
|
2415
|
+
throw new KabanError(`Ambiguous task ID prefix: ${prefix}. Multiple matches found.`, ExitCode.VALIDATION);
|
|
2416
|
+
}
|
|
2417
|
+
return result[0];
|
|
2418
|
+
}
|
|
1231
2419
|
}
|
|
1232
2420
|
|
|
1233
2421
|
// src/index.ts
|
|
@@ -1240,26 +2428,38 @@ export {
|
|
|
1240
2428
|
undoLog,
|
|
1241
2429
|
tokenize,
|
|
1242
2430
|
tasks,
|
|
2431
|
+
taskLinks,
|
|
1243
2432
|
runMigrations,
|
|
2433
|
+
priorityScorer,
|
|
2434
|
+
linkTypes,
|
|
1244
2435
|
jsonSchemas,
|
|
1245
2436
|
jaccardSimilarity,
|
|
1246
2437
|
isValidUlid,
|
|
1247
2438
|
initializeSchema,
|
|
1248
2439
|
getJsonSchema,
|
|
2440
|
+
fifoScorer,
|
|
2441
|
+
dueDateScorer,
|
|
2442
|
+
createDefaultScoringService,
|
|
1249
2443
|
createDb,
|
|
2444
|
+
createBlockingScorer,
|
|
1250
2445
|
columns,
|
|
1251
2446
|
boards,
|
|
2447
|
+
audits,
|
|
1252
2448
|
UpdateTaskInputSchema,
|
|
1253
2449
|
UlidSchema,
|
|
1254
2450
|
TitleSchema,
|
|
1255
2451
|
TaskService,
|
|
1256
2452
|
TaskSchema,
|
|
1257
2453
|
TaskResponseSchema,
|
|
2454
|
+
ScoringService,
|
|
1258
2455
|
MoveTaskInputSchema,
|
|
2456
|
+
MarkdownService,
|
|
1259
2457
|
ListTasksFilterSchema,
|
|
2458
|
+
LinkService,
|
|
1260
2459
|
KabanError,
|
|
1261
2460
|
GetTaskInputSchema,
|
|
1262
2461
|
ExitCode,
|
|
2462
|
+
DependencyService,
|
|
1263
2463
|
DeleteTaskInputSchema,
|
|
1264
2464
|
DEFAULT_CONFIG,
|
|
1265
2465
|
ConfigSchema,
|
|
@@ -1269,6 +2469,7 @@ export {
|
|
|
1269
2469
|
BoardStatusSchema,
|
|
1270
2470
|
BoardService,
|
|
1271
2471
|
BoardSchema,
|
|
2472
|
+
AuditService,
|
|
1272
2473
|
ApiResponseSchema,
|
|
1273
2474
|
AgentNameSchema,
|
|
1274
2475
|
AddTaskInputSchema
|