@kaban-board/core 0.3.0 → 0.3.2
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 -80
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1267 -81
- 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
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
2
|
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
-
for (let key of __getOwnPropNames(mod))
|
|
11
|
-
if (!__hasOwnProp.call(to, key))
|
|
12
|
-
__defProp(to, key, {
|
|
13
|
-
get: () => mod[key],
|
|
14
|
-
enumerable: true
|
|
15
|
-
});
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
3
|
var __export = (target, all) => {
|
|
19
4
|
for (var name in all)
|
|
20
5
|
__defProp(target, name, {
|
|
@@ -90,15 +75,19 @@ var exports_schema = {};
|
|
|
90
75
|
__export(exports_schema, {
|
|
91
76
|
undoLog: () => undoLog,
|
|
92
77
|
tasks: () => tasks,
|
|
78
|
+
taskLinks: () => taskLinks,
|
|
79
|
+
linkTypes: () => linkTypes,
|
|
93
80
|
columns: () => columns,
|
|
94
|
-
boards: () => boards
|
|
81
|
+
boards: () => boards,
|
|
82
|
+
audits: () => audits
|
|
95
83
|
});
|
|
96
84
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
97
|
-
var boards, columns, tasks, undoLog;
|
|
85
|
+
var boards, columns, tasks, undoLog, audits, linkTypes, taskLinks;
|
|
98
86
|
var init_schema = __esm(() => {
|
|
99
87
|
boards = sqliteTable("boards", {
|
|
100
88
|
id: text("id").primaryKey(),
|
|
101
89
|
name: text("name").notNull(),
|
|
90
|
+
maxBoardTaskId: integer("max_board_task_id").notNull().default(0),
|
|
102
91
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
103
92
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
|
|
104
93
|
});
|
|
@@ -112,6 +101,7 @@ var init_schema = __esm(() => {
|
|
|
112
101
|
});
|
|
113
102
|
tasks = sqliteTable("tasks", {
|
|
114
103
|
id: text("id").primaryKey(),
|
|
104
|
+
boardTaskId: integer("board_task_id"),
|
|
115
105
|
title: text("title").notNull(),
|
|
116
106
|
description: text("description"),
|
|
117
107
|
columnId: text("column_id").notNull().references(() => columns.id),
|
|
@@ -128,8 +118,10 @@ var init_schema = __esm(() => {
|
|
|
128
118
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
129
119
|
startedAt: integer("started_at", { mode: "timestamp" }),
|
|
130
120
|
completedAt: integer("completed_at", { mode: "timestamp" }),
|
|
121
|
+
dueDate: integer("due_date", { mode: "timestamp" }),
|
|
131
122
|
archived: integer("archived", { mode: "boolean" }).notNull().default(false),
|
|
132
|
-
archivedAt: integer("archived_at", { mode: "timestamp" })
|
|
123
|
+
archivedAt: integer("archived_at", { mode: "timestamp" }),
|
|
124
|
+
updatedBy: text("updated_by")
|
|
133
125
|
});
|
|
134
126
|
undoLog = sqliteTable("undo_log", {
|
|
135
127
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
@@ -137,6 +129,25 @@ var init_schema = __esm(() => {
|
|
|
137
129
|
data: text("data", { mode: "json" }).notNull(),
|
|
138
130
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull()
|
|
139
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
|
+
});
|
|
140
151
|
});
|
|
141
152
|
|
|
142
153
|
// drizzle/meta/_journal.json
|
|
@@ -166,6 +177,41 @@ var init__journal = __esm(() => {
|
|
|
166
177
|
when: 1769351200000,
|
|
167
178
|
tag: "0002_add_fts5",
|
|
168
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
|
|
169
215
|
}
|
|
170
216
|
]
|
|
171
217
|
};
|
|
@@ -226,6 +272,216 @@ WHERE NOT EXISTS (SELECT 1 FROM tasks_fts LIMIT 1);
|
|
|
226
272
|
`;
|
|
227
273
|
var init_0002_add_fts5 = () => {};
|
|
228
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
|
+
|
|
229
485
|
// src/db/migrator.ts
|
|
230
486
|
var exports_migrator = {};
|
|
231
487
|
__export(exports_migrator, {
|
|
@@ -235,7 +491,7 @@ import { readFileSync } from "node:fs";
|
|
|
235
491
|
import { dirname as dirname2, isAbsolute, join } from "node:path";
|
|
236
492
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
237
493
|
function resolveSqlContent(sqlOrPath) {
|
|
238
|
-
if (sqlOrPath.includes("CREATE") || sqlOrPath.includes("INSERT") || sqlOrPath.includes("--")) {
|
|
494
|
+
if (sqlOrPath.includes("CREATE") || sqlOrPath.includes("INSERT") || sqlOrPath.includes("ALTER") || sqlOrPath.includes("--")) {
|
|
239
495
|
return sqlOrPath;
|
|
240
496
|
}
|
|
241
497
|
let filePath = sqlOrPath;
|
|
@@ -342,10 +598,20 @@ var init_migrator = __esm(() => {
|
|
|
342
598
|
init_0000_init();
|
|
343
599
|
init_0001_add_archived();
|
|
344
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();
|
|
345
606
|
migrationSql = {
|
|
346
607
|
"0000_init": _0000_init_default,
|
|
347
608
|
"0001_add_archived": _0001_add_archived_default,
|
|
348
|
-
"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
|
|
349
615
|
};
|
|
350
616
|
__dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
351
617
|
drizzleDir = join(__dirname2, "..", "..", "drizzle");
|
|
@@ -401,17 +667,174 @@ var init_libsql_adapter = __esm(() => {
|
|
|
401
667
|
init_utils();
|
|
402
668
|
});
|
|
403
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
|
+
|
|
404
810
|
// src/db/bun-adapter.ts
|
|
405
811
|
var exports_bun_adapter = {};
|
|
406
812
|
__export(exports_bun_adapter, {
|
|
407
813
|
createBunDb: () => createBunDb
|
|
408
814
|
});
|
|
815
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
409
816
|
async function createBunDb(filePath) {
|
|
410
817
|
let sqlite;
|
|
411
818
|
try {
|
|
412
819
|
const { Database } = await import("bun:sqlite");
|
|
413
820
|
const { drizzle } = await import("drizzle-orm/bun-sqlite");
|
|
414
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
|
+
}
|
|
415
838
|
sqlite = new Database(filePath);
|
|
416
839
|
const db = drizzle({ client: sqlite, schema: exports_schema });
|
|
417
840
|
let closed = false;
|
|
@@ -452,6 +875,7 @@ async function createBunDb(filePath) {
|
|
|
452
875
|
}
|
|
453
876
|
var init_bun_adapter = __esm(() => {
|
|
454
877
|
init_types();
|
|
878
|
+
init_recovery();
|
|
455
879
|
init_schema();
|
|
456
880
|
init_utils();
|
|
457
881
|
});
|
|
@@ -525,6 +949,7 @@ PRAGMA journal_mode = WAL;
|
|
|
525
949
|
CREATE TABLE IF NOT EXISTS boards (
|
|
526
950
|
id TEXT PRIMARY KEY,
|
|
527
951
|
name TEXT NOT NULL,
|
|
952
|
+
max_board_task_id INTEGER NOT NULL DEFAULT 0,
|
|
528
953
|
created_at INTEGER NOT NULL,
|
|
529
954
|
updated_at INTEGER NOT NULL
|
|
530
955
|
);
|
|
@@ -540,6 +965,7 @@ CREATE TABLE IF NOT EXISTS columns (
|
|
|
540
965
|
|
|
541
966
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
542
967
|
id TEXT PRIMARY KEY,
|
|
968
|
+
board_task_id INTEGER,
|
|
543
969
|
title TEXT NOT NULL,
|
|
544
970
|
description TEXT,
|
|
545
971
|
column_id TEXT NOT NULL REFERENCES columns(id),
|
|
@@ -556,8 +982,10 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
556
982
|
updated_at INTEGER NOT NULL,
|
|
557
983
|
started_at INTEGER,
|
|
558
984
|
completed_at INTEGER,
|
|
985
|
+
due_date INTEGER,
|
|
559
986
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
560
|
-
archived_at INTEGER
|
|
987
|
+
archived_at INTEGER,
|
|
988
|
+
updated_by TEXT
|
|
561
989
|
);
|
|
562
990
|
|
|
563
991
|
CREATE TABLE IF NOT EXISTS undo_log (
|
|
@@ -567,8 +995,95 @@ CREATE TABLE IF NOT EXISTS undo_log (
|
|
|
567
995
|
created_at INTEGER NOT NULL
|
|
568
996
|
);
|
|
569
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
|
+
|
|
570
1019
|
CREATE INDEX IF NOT EXISTS idx_tasks_column ON tasks(column_id);
|
|
571
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;
|
|
572
1087
|
`;
|
|
573
1088
|
async function initializeSchema(db) {
|
|
574
1089
|
await db.$runRaw(SCHEMA_SQL);
|
|
@@ -584,6 +1099,7 @@ var AgentNameSchema = AgentNameBaseSchema.transform((s) => s.toLowerCase());
|
|
|
584
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");
|
|
585
1100
|
var TaskSchema = z.object({
|
|
586
1101
|
id: UlidSchema,
|
|
1102
|
+
boardTaskId: z.number().int().positive().nullable(),
|
|
587
1103
|
title: z.string(),
|
|
588
1104
|
description: z.string().nullable(),
|
|
589
1105
|
columnId: z.string(),
|
|
@@ -600,8 +1116,29 @@ var TaskSchema = z.object({
|
|
|
600
1116
|
updatedAt: z.date(),
|
|
601
1117
|
startedAt: z.date().nullable(),
|
|
602
1118
|
completedAt: z.date().nullable(),
|
|
1119
|
+
dueDate: z.date().nullable(),
|
|
603
1120
|
archived: z.boolean().default(false),
|
|
604
|
-
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()
|
|
605
1142
|
});
|
|
606
1143
|
var ColumnSchema = z.object({
|
|
607
1144
|
id: z.string(),
|
|
@@ -613,6 +1150,7 @@ var ColumnSchema = z.object({
|
|
|
613
1150
|
var BoardSchema = z.object({
|
|
614
1151
|
id: UlidSchema,
|
|
615
1152
|
name: z.string(),
|
|
1153
|
+
maxBoardTaskId: z.number().int().nonnegative(),
|
|
616
1154
|
createdAt: z.date(),
|
|
617
1155
|
updatedAt: z.date()
|
|
618
1156
|
});
|
|
@@ -647,14 +1185,16 @@ var AddTaskInputSchema = z.object({
|
|
|
647
1185
|
assignedTo: AgentNameSchema.optional(),
|
|
648
1186
|
dependsOn: z.array(UlidSchema).optional(),
|
|
649
1187
|
files: z.array(z.string()).optional(),
|
|
650
|
-
labels: z.array(z.string().max(50)).optional()
|
|
1188
|
+
labels: z.array(z.string().max(50)).optional(),
|
|
1189
|
+
dueDate: z.string().optional()
|
|
651
1190
|
});
|
|
652
1191
|
var UpdateTaskInputSchema = z.object({
|
|
653
1192
|
title: TitleSchema.optional(),
|
|
654
1193
|
description: z.string().max(5000).nullable().optional(),
|
|
655
1194
|
assignedTo: AgentNameSchema.nullable().optional(),
|
|
656
1195
|
files: z.array(z.string()).optional(),
|
|
657
|
-
labels: z.array(z.string().max(50)).optional()
|
|
1196
|
+
labels: z.array(z.string().max(50)).optional(),
|
|
1197
|
+
dueDate: z.string().nullable().optional()
|
|
658
1198
|
});
|
|
659
1199
|
var MoveTaskInputSchema = z.object({
|
|
660
1200
|
id: UlidSchema,
|
|
@@ -680,6 +1220,7 @@ var TaskResponseSchema = TaskSchema.extend({
|
|
|
680
1220
|
updatedAt: z.string().datetime(),
|
|
681
1221
|
startedAt: z.string().datetime().nullable(),
|
|
682
1222
|
completedAt: z.string().datetime().nullable(),
|
|
1223
|
+
dueDate: z.string().datetime().nullable(),
|
|
683
1224
|
archivedAt: z.string().datetime().nullable().optional()
|
|
684
1225
|
});
|
|
685
1226
|
var BoardStatusSchema = z.object({
|
|
@@ -706,14 +1247,16 @@ var AddTaskInputJsonSchema = z.object({
|
|
|
706
1247
|
assignedTo: AgentNameBaseSchema.optional(),
|
|
707
1248
|
dependsOn: z.array(UlidSchema).optional(),
|
|
708
1249
|
files: z.array(z.string()).optional(),
|
|
709
|
-
labels: z.array(z.string().max(50)).optional()
|
|
1250
|
+
labels: z.array(z.string().max(50)).optional(),
|
|
1251
|
+
dueDate: z.string().optional()
|
|
710
1252
|
});
|
|
711
1253
|
var UpdateTaskInputJsonSchema = z.object({
|
|
712
1254
|
title: TitleBaseSchema.optional(),
|
|
713
1255
|
description: z.string().max(5000).nullable().optional(),
|
|
714
1256
|
assignedTo: AgentNameBaseSchema.nullable().optional(),
|
|
715
1257
|
files: z.array(z.string()).optional(),
|
|
716
|
-
labels: z.array(z.string().max(50)).optional()
|
|
1258
|
+
labels: z.array(z.string().max(50)).optional(),
|
|
1259
|
+
dueDate: z.string().nullable().optional()
|
|
717
1260
|
});
|
|
718
1261
|
var ListTasksFilterJsonSchema = z.object({
|
|
719
1262
|
columnId: ColumnIdSchema.optional(),
|
|
@@ -753,9 +1296,103 @@ var jsonSchemas = {
|
|
|
753
1296
|
function getJsonSchema(name) {
|
|
754
1297
|
return jsonSchemas[name];
|
|
755
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
|
+
}
|
|
756
1393
|
// src/services/board.ts
|
|
757
1394
|
init_schema();
|
|
758
|
-
import { eq } from "drizzle-orm";
|
|
1395
|
+
import { eq as eq2 } from "drizzle-orm";
|
|
759
1396
|
import { ulid } from "ulid";
|
|
760
1397
|
|
|
761
1398
|
class BoardService {
|
|
@@ -786,6 +1423,7 @@ class BoardService {
|
|
|
786
1423
|
return {
|
|
787
1424
|
id: boardId,
|
|
788
1425
|
name: config.board.name,
|
|
1426
|
+
maxBoardTaskId: 0,
|
|
789
1427
|
createdAt: now,
|
|
790
1428
|
updatedAt: now
|
|
791
1429
|
};
|
|
@@ -798,23 +1436,493 @@ class BoardService {
|
|
|
798
1436
|
return this.db.select().from(columns).orderBy(columns.position);
|
|
799
1437
|
}
|
|
800
1438
|
async getColumn(id) {
|
|
801
|
-
const rows = await this.db.select().from(columns).where(
|
|
1439
|
+
const rows = await this.db.select().from(columns).where(eq2(columns.id, id));
|
|
802
1440
|
return rows[0] ?? null;
|
|
803
1441
|
}
|
|
804
1442
|
async getTerminalColumn() {
|
|
805
|
-
const rows = await this.db.select().from(columns).where(
|
|
1443
|
+
const rows = await this.db.select().from(columns).where(eq2(columns.isTerminal, true));
|
|
806
1444
|
return rows[0] ?? null;
|
|
807
1445
|
}
|
|
808
1446
|
async getTerminalColumns() {
|
|
809
|
-
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(" → ");
|
|
1496
|
+
}
|
|
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 };
|
|
810
1724
|
}
|
|
811
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
|
+
}
|
|
812
1854
|
// src/services/task.ts
|
|
813
1855
|
init_schema();
|
|
814
1856
|
init_types();
|
|
815
|
-
import { and, eq as
|
|
1857
|
+
import { and as and3, eq as eq4, inArray, lt, sql as sql2 } from "drizzle-orm";
|
|
816
1858
|
import { ulid as ulid2 } from "ulid";
|
|
817
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
|
+
|
|
818
1926
|
// src/utils/similarity.ts
|
|
819
1927
|
function tokenize(text2) {
|
|
820
1928
|
const words = text2.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((word) => word.length > 0);
|
|
@@ -870,9 +1978,11 @@ class TaskService {
|
|
|
870
1978
|
threshold: 0.5,
|
|
871
1979
|
warnThreshold: 0.5
|
|
872
1980
|
};
|
|
1981
|
+
depService;
|
|
873
1982
|
constructor(db, boardService) {
|
|
874
1983
|
this.db = db;
|
|
875
1984
|
this.boardService = boardService;
|
|
1985
|
+
this.depService = new DependencyService(this.getTask.bind(this));
|
|
876
1986
|
}
|
|
877
1987
|
async getTaskOrThrow(id) {
|
|
878
1988
|
const task = await this.getTask(id);
|
|
@@ -885,61 +1995,80 @@ class TaskService {
|
|
|
885
1995
|
const title = validateTitle(input.title);
|
|
886
1996
|
const createdBy = input.createdBy ? validateAgentName(input.createdBy) : input.agent ? validateAgentName(input.agent) : "user";
|
|
887
1997
|
const columnId = input.columnId ? validateColumnId(input.columnId) : "todo";
|
|
1998
|
+
const dueDate = parseDateOrNull(input.dueDate);
|
|
888
1999
|
const column = await this.boardService.getColumn(columnId);
|
|
889
2000
|
if (!column) {
|
|
890
2001
|
throw new KabanError(`Column '${columnId}' does not exist`, ExitCode.VALIDATION);
|
|
891
2002
|
}
|
|
2003
|
+
const board = await this.boardService.getBoard();
|
|
2004
|
+
if (!board) {
|
|
2005
|
+
throw new KabanError("No board found", ExitCode.NOT_FOUND);
|
|
2006
|
+
}
|
|
892
2007
|
const now = new Date;
|
|
893
2008
|
const id = ulid2();
|
|
894
|
-
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));
|
|
895
2010
|
const position = (maxPositionResult[0]?.max ?? -1) + 1;
|
|
896
|
-
await this.db.
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
+
`);
|
|
917
2046
|
return this.getTaskOrThrow(id);
|
|
918
2047
|
}
|
|
919
2048
|
async getTask(id) {
|
|
920
|
-
const rows = await this.db.select().from(tasks).where(
|
|
2049
|
+
const rows = await this.db.select().from(tasks).where(eq4(tasks.id, id));
|
|
921
2050
|
return rows[0] ?? null;
|
|
922
2051
|
}
|
|
923
2052
|
async listTasks(filter) {
|
|
924
2053
|
const conditions = [];
|
|
925
2054
|
if (!filter?.includeArchived) {
|
|
926
|
-
conditions.push(
|
|
2055
|
+
conditions.push(eq4(tasks.archived, false));
|
|
927
2056
|
}
|
|
928
2057
|
if (filter?.columnId) {
|
|
929
|
-
conditions.push(
|
|
2058
|
+
conditions.push(eq4(tasks.columnId, filter.columnId));
|
|
930
2059
|
}
|
|
931
2060
|
const creatorFilter = filter?.createdBy ?? filter?.agent;
|
|
932
2061
|
if (creatorFilter) {
|
|
933
|
-
conditions.push(
|
|
2062
|
+
conditions.push(eq4(tasks.createdBy, creatorFilter));
|
|
934
2063
|
}
|
|
935
2064
|
if (filter?.assignee) {
|
|
936
|
-
conditions.push(
|
|
2065
|
+
conditions.push(eq4(tasks.assignedTo, filter.assignee));
|
|
937
2066
|
}
|
|
938
2067
|
if (filter?.blocked === true) {
|
|
939
|
-
conditions.push(
|
|
2068
|
+
conditions.push(sql2`${tasks.blockedReason} IS NOT NULL`);
|
|
940
2069
|
}
|
|
941
2070
|
if (conditions.length > 0) {
|
|
942
|
-
return this.db.select().from(tasks).where(
|
|
2071
|
+
return this.db.select().from(tasks).where(and3(...conditions)).orderBy(tasks.columnId, tasks.position);
|
|
943
2072
|
}
|
|
944
2073
|
return this.db.select().from(tasks).orderBy(tasks.columnId, tasks.position);
|
|
945
2074
|
}
|
|
@@ -948,7 +2077,7 @@ class TaskService {
|
|
|
948
2077
|
if (!task) {
|
|
949
2078
|
throw new KabanError(`Task '${id}' not found`, ExitCode.NOT_FOUND);
|
|
950
2079
|
}
|
|
951
|
-
await this.db.delete(tasks).where(
|
|
2080
|
+
await this.db.delete(tasks).where(eq4(tasks.id, id));
|
|
952
2081
|
}
|
|
953
2082
|
async moveTask(id, columnId, options) {
|
|
954
2083
|
const task = await this.getTask(id);
|
|
@@ -974,7 +2103,7 @@ class TaskService {
|
|
|
974
2103
|
}
|
|
975
2104
|
const now = new Date;
|
|
976
2105
|
const isTerminal = column.isTerminal;
|
|
977
|
-
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));
|
|
978
2107
|
const newPosition = (maxPositionResult[0]?.max ?? -1) + 1;
|
|
979
2108
|
await this.db.update(tasks).set({
|
|
980
2109
|
columnId,
|
|
@@ -982,11 +2111,12 @@ class TaskService {
|
|
|
982
2111
|
version: task.version + 1,
|
|
983
2112
|
updatedAt: now,
|
|
984
2113
|
completedAt: isTerminal ? now : task.completedAt,
|
|
985
|
-
startedAt: columnId === "in_progress" && !task.startedAt ? now : task.startedAt
|
|
986
|
-
|
|
2114
|
+
startedAt: columnId === "in_progress" && !task.startedAt ? now : task.startedAt,
|
|
2115
|
+
updatedBy: options?.actor ?? null
|
|
2116
|
+
}).where(eq4(tasks.id, id));
|
|
987
2117
|
return this.getTaskOrThrow(id);
|
|
988
2118
|
}
|
|
989
|
-
async updateTask(id, input, expectedVersion) {
|
|
2119
|
+
async updateTask(id, input, expectedVersion, actor) {
|
|
990
2120
|
const task = await this.getTask(id);
|
|
991
2121
|
if (!task) {
|
|
992
2122
|
throw new KabanError(`Task '${id}' not found`, ExitCode.NOT_FOUND);
|
|
@@ -996,7 +2126,8 @@ class TaskService {
|
|
|
996
2126
|
}
|
|
997
2127
|
const updates = {
|
|
998
2128
|
version: task.version + 1,
|
|
999
|
-
updatedAt: new Date
|
|
2129
|
+
updatedAt: new Date,
|
|
2130
|
+
updatedBy: actor ?? null
|
|
1000
2131
|
};
|
|
1001
2132
|
if (input.title !== undefined) {
|
|
1002
2133
|
updates.title = validateTitle(input.title);
|
|
@@ -1013,11 +2144,14 @@ class TaskService {
|
|
|
1013
2144
|
if (input.labels !== undefined) {
|
|
1014
2145
|
updates.labels = input.labels;
|
|
1015
2146
|
}
|
|
1016
|
-
|
|
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));
|
|
1017
2151
|
return this.getTaskOrThrow(id);
|
|
1018
2152
|
}
|
|
1019
2153
|
async getTaskCountInColumn(columnId) {
|
|
1020
|
-
const result = await this.db.select({ count:
|
|
2154
|
+
const result = await this.db.select({ count: sql2`COUNT(*)` }).from(tasks).where(eq4(tasks.columnId, columnId));
|
|
1021
2155
|
return result[0]?.count ?? 0;
|
|
1022
2156
|
}
|
|
1023
2157
|
async archiveTasks(criteria) {
|
|
@@ -1025,9 +2159,9 @@ class TaskService {
|
|
|
1025
2159
|
if (!hasCriteria) {
|
|
1026
2160
|
throw new KabanError("At least one criteria must be provided", ExitCode.VALIDATION);
|
|
1027
2161
|
}
|
|
1028
|
-
const conditions = [
|
|
2162
|
+
const conditions = [eq4(tasks.archived, false)];
|
|
1029
2163
|
if (criteria.status) {
|
|
1030
|
-
conditions.push(
|
|
2164
|
+
conditions.push(eq4(tasks.columnId, criteria.status));
|
|
1031
2165
|
}
|
|
1032
2166
|
if (criteria.olderThan) {
|
|
1033
2167
|
conditions.push(lt(tasks.createdAt, criteria.olderThan));
|
|
@@ -1035,7 +2169,7 @@ class TaskService {
|
|
|
1035
2169
|
if (criteria.taskIds?.length) {
|
|
1036
2170
|
conditions.push(inArray(tasks.id, criteria.taskIds));
|
|
1037
2171
|
}
|
|
1038
|
-
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));
|
|
1039
2173
|
if (matchingTasks.length === 0) {
|
|
1040
2174
|
return { archivedCount: 0, taskIds: [] };
|
|
1041
2175
|
}
|
|
@@ -1074,16 +2208,16 @@ class TaskService {
|
|
|
1074
2208
|
columnId,
|
|
1075
2209
|
version: task.version + 1,
|
|
1076
2210
|
updatedAt: now
|
|
1077
|
-
}).where(
|
|
2211
|
+
}).where(eq4(tasks.id, taskId));
|
|
1078
2212
|
return this.getTaskOrThrow(taskId);
|
|
1079
2213
|
}
|
|
1080
2214
|
async searchArchive(query, options) {
|
|
1081
2215
|
const limit = options?.limit ?? 50;
|
|
1082
2216
|
const offset = options?.offset ?? 0;
|
|
1083
2217
|
if (!query.trim()) {
|
|
1084
|
-
const countResult = await this.db.select({ count:
|
|
2218
|
+
const countResult = await this.db.select({ count: sql2`COUNT(*)` }).from(tasks).where(eq4(tasks.archived, true));
|
|
1085
2219
|
const total2 = countResult[0]?.count ?? 0;
|
|
1086
|
-
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);
|
|
1087
2221
|
return { tasks: archivedTasks, total: total2 };
|
|
1088
2222
|
}
|
|
1089
2223
|
const ftsQuery = query.trim().split(/\s+/).map((term) => `"${term}"`).join(" ");
|
|
@@ -1101,6 +2235,7 @@ class TaskService {
|
|
|
1101
2235
|
LIMIT ? OFFSET ?`).all(ftsQuery, limit, offset);
|
|
1102
2236
|
const searchTasks = rows.map((row) => ({
|
|
1103
2237
|
id: row.id,
|
|
2238
|
+
boardTaskId: row.board_task_id,
|
|
1104
2239
|
title: row.title,
|
|
1105
2240
|
description: row.description,
|
|
1106
2241
|
columnId: row.column_id,
|
|
@@ -1117,17 +2252,19 @@ class TaskService {
|
|
|
1117
2252
|
updatedAt: new Date(row.updated_at),
|
|
1118
2253
|
startedAt: row.started_at ? new Date(row.started_at) : null,
|
|
1119
2254
|
completedAt: row.completed_at ? new Date(row.completed_at) : null,
|
|
2255
|
+
dueDate: row.due_date ? new Date(row.due_date * 1000) : null,
|
|
1120
2256
|
archived: Boolean(row.archived),
|
|
1121
|
-
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
|
|
1122
2259
|
}));
|
|
1123
2260
|
return { tasks: searchTasks, total };
|
|
1124
2261
|
}
|
|
1125
2262
|
async purgeArchive(criteria) {
|
|
1126
|
-
const conditions = [
|
|
2263
|
+
const conditions = [eq4(tasks.archived, true)];
|
|
1127
2264
|
if (criteria?.olderThan) {
|
|
1128
2265
|
conditions.push(lt(tasks.archivedAt, criteria.olderThan));
|
|
1129
2266
|
}
|
|
1130
|
-
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));
|
|
1131
2268
|
if (matchingTasks.length === 0) {
|
|
1132
2269
|
return { deletedCount: 0 };
|
|
1133
2270
|
}
|
|
@@ -1140,7 +2277,7 @@ class TaskService {
|
|
|
1140
2277
|
if (allTasks.length === 0) {
|
|
1141
2278
|
return { deletedCount: 0 };
|
|
1142
2279
|
}
|
|
1143
|
-
await this.db.delete(tasks)
|
|
2280
|
+
await this.db.delete(tasks);
|
|
1144
2281
|
return { deletedCount: allTasks.length };
|
|
1145
2282
|
}
|
|
1146
2283
|
async addDependency(taskId, dependsOnId) {
|
|
@@ -1152,13 +2289,18 @@ class TaskService {
|
|
|
1152
2289
|
if (task.dependsOn.includes(dependsOnId)) {
|
|
1153
2290
|
return task;
|
|
1154
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
|
+
}
|
|
1155
2297
|
const updatedDependsOn = [...task.dependsOn, dependsOnId];
|
|
1156
2298
|
const now = new Date;
|
|
1157
2299
|
await this.db.update(tasks).set({
|
|
1158
2300
|
dependsOn: updatedDependsOn,
|
|
1159
2301
|
version: task.version + 1,
|
|
1160
2302
|
updatedAt: now
|
|
1161
|
-
}).where(
|
|
2303
|
+
}).where(eq4(tasks.id, taskId));
|
|
1162
2304
|
return this.getTaskOrThrow(taskId);
|
|
1163
2305
|
}
|
|
1164
2306
|
async removeDependency(taskId, dependsOnId) {
|
|
@@ -1172,7 +2314,7 @@ class TaskService {
|
|
|
1172
2314
|
dependsOn: updatedDependsOn,
|
|
1173
2315
|
version: task.version + 1,
|
|
1174
2316
|
updatedAt: now
|
|
1175
|
-
}).where(
|
|
2317
|
+
}).where(eq4(tasks.id, taskId));
|
|
1176
2318
|
return this.getTaskOrThrow(taskId);
|
|
1177
2319
|
}
|
|
1178
2320
|
async validateDependencies(taskId) {
|
|
@@ -1228,7 +2370,7 @@ class TaskService {
|
|
|
1228
2370
|
};
|
|
1229
2371
|
}
|
|
1230
2372
|
async getArchiveStats() {
|
|
1231
|
-
const archived = await this.db.select().from(tasks).where(
|
|
2373
|
+
const archived = await this.db.select().from(tasks).where(eq4(tasks.archived, true));
|
|
1232
2374
|
const byColumn = {};
|
|
1233
2375
|
let oldestArchivedAt = null;
|
|
1234
2376
|
for (const task of archived) {
|
|
@@ -1243,6 +2385,37 @@ class TaskService {
|
|
|
1243
2385
|
oldestArchivedAt
|
|
1244
2386
|
};
|
|
1245
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
|
+
}
|
|
1246
2419
|
}
|
|
1247
2420
|
|
|
1248
2421
|
// src/index.ts
|
|
@@ -1255,26 +2428,38 @@ export {
|
|
|
1255
2428
|
undoLog,
|
|
1256
2429
|
tokenize,
|
|
1257
2430
|
tasks,
|
|
2431
|
+
taskLinks,
|
|
1258
2432
|
runMigrations,
|
|
2433
|
+
priorityScorer,
|
|
2434
|
+
linkTypes,
|
|
1259
2435
|
jsonSchemas,
|
|
1260
2436
|
jaccardSimilarity,
|
|
1261
2437
|
isValidUlid,
|
|
1262
2438
|
initializeSchema,
|
|
1263
2439
|
getJsonSchema,
|
|
2440
|
+
fifoScorer,
|
|
2441
|
+
dueDateScorer,
|
|
2442
|
+
createDefaultScoringService,
|
|
1264
2443
|
createDb,
|
|
2444
|
+
createBlockingScorer,
|
|
1265
2445
|
columns,
|
|
1266
2446
|
boards,
|
|
2447
|
+
audits,
|
|
1267
2448
|
UpdateTaskInputSchema,
|
|
1268
2449
|
UlidSchema,
|
|
1269
2450
|
TitleSchema,
|
|
1270
2451
|
TaskService,
|
|
1271
2452
|
TaskSchema,
|
|
1272
2453
|
TaskResponseSchema,
|
|
2454
|
+
ScoringService,
|
|
1273
2455
|
MoveTaskInputSchema,
|
|
2456
|
+
MarkdownService,
|
|
1274
2457
|
ListTasksFilterSchema,
|
|
2458
|
+
LinkService,
|
|
1275
2459
|
KabanError,
|
|
1276
2460
|
GetTaskInputSchema,
|
|
1277
2461
|
ExitCode,
|
|
2462
|
+
DependencyService,
|
|
1278
2463
|
DeleteTaskInputSchema,
|
|
1279
2464
|
DEFAULT_CONFIG,
|
|
1280
2465
|
ConfigSchema,
|
|
@@ -1284,6 +2469,7 @@ export {
|
|
|
1284
2469
|
BoardStatusSchema,
|
|
1285
2470
|
BoardService,
|
|
1286
2471
|
BoardSchema,
|
|
2472
|
+
AuditService,
|
|
1287
2473
|
ApiResponseSchema,
|
|
1288
2474
|
AgentNameSchema,
|
|
1289
2475
|
AddTaskInputSchema
|