@sascha384/tic 5.16.0 → 5.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/backends/ado/index.js +1 -1
- package/dist/backends/ado/index.js.map +1 -1
- package/dist/backends/ado/mappers.d.ts +2 -2
- package/dist/backends/ado/mappers.js +7 -2
- package/dist/backends/ado/mappers.js.map +1 -1
- package/dist/backends/files/index.d.ts +6 -0
- package/dist/backends/files/index.js +36 -0
- package/dist/backends/files/index.js.map +1 -1
- package/dist/backends/files/sync.d.ts +2 -2
- package/dist/backends/files/sync.js +9 -7
- package/dist/backends/files/sync.js.map +1 -1
- package/dist/backends/github/index.js +3 -3
- package/dist/backends/github/index.js.map +1 -1
- package/dist/backends/github/mappers.js +2 -1
- package/dist/backends/github/mappers.js.map +1 -1
- package/dist/backends/github/pr-mappers.d.ts +1 -1
- package/dist/backends/github/pr-mappers.js +1 -1
- package/dist/backends/github/pr-mappers.js.map +1 -1
- package/dist/backends/gitlab/index.js +13 -6
- package/dist/backends/gitlab/index.js.map +1 -1
- package/dist/backends/gitlab/mappers.js +2 -1
- package/dist/backends/gitlab/mappers.js.map +1 -1
- package/dist/backends/jira/index.js +2 -2
- package/dist/backends/jira/index.js.map +1 -1
- package/dist/backends/jira/mappers.d.ts +1 -1
- package/dist/backends/jira/mappers.js +5 -4
- package/dist/backends/jira/mappers.js.map +1 -1
- package/dist/backends/local/items.js +9 -2
- package/dist/backends/local/items.js.map +1 -1
- package/dist/backends/types.d.ts +2 -3
- package/dist/backends/types.js +8 -2
- package/dist/backends/types.js.map +1 -1
- package/dist/cli/commands/config.js +0 -1
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/item.js +45 -17
- package/dist/cli/commands/item.js.map +1 -1
- package/dist/cli/commands/mcp.js +24 -9
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/pr.js +12 -6
- package/dist/cli/commands/pr.js.map +1 -1
- package/dist/cli/index.js +13 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/components/BranchList.js +4 -2
- package/dist/components/BranchList.js.map +1 -1
- package/dist/components/CommandBar.js +8 -7
- package/dist/components/CommandBar.js.map +1 -1
- package/dist/components/DetailPanel.js +2 -2
- package/dist/components/DetailPanel.js.map +1 -1
- package/dist/components/Settings.js +1 -1
- package/dist/components/Settings.js.map +1 -1
- package/dist/components/StatusScreen.js +2 -2
- package/dist/components/StatusScreen.js.map +1 -1
- package/dist/components/WorkItemForm.js +107 -73
- package/dist/components/WorkItemForm.js.map +1 -1
- package/dist/components/WorkItemList.d.ts +5 -3
- package/dist/components/WorkItemList.js +175 -124
- package/dist/components/WorkItemList.js.map +1 -1
- package/dist/components/buildTree.js +15 -13
- package/dist/components/buildTree.js.map +1 -1
- package/dist/components/fuzzyMatch.js +1 -1
- package/dist/components/fuzzyMatch.js.map +1 -1
- package/dist/components/getMarkedDistribution.d.ts +2 -2
- package/dist/components/getMarkedDistribution.js +1 -1
- package/dist/components/getMarkedDistribution.js.map +1 -1
- package/dist/git.d.ts +2 -1
- package/dist/hooks/useFormValidation.js +31 -21
- package/dist/hooks/useFormValidation.js.map +1 -1
- package/dist/implement.js +3 -3
- package/dist/implement.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/storage/config.d.ts +0 -1
- package/dist/storage/config.js +0 -6
- package/dist/storage/config.js.map +1 -1
- package/dist/storage/index.d.ts +25 -6
- package/dist/storage/index.js +304 -183
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/mappers.d.ts +1 -1
- package/dist/storage/mappers.js +5 -3
- package/dist/storage/mappers.js.map +1 -1
- package/dist/storage/schema.d.ts +83 -380
- package/dist/storage/schema.js +27 -55
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/syncQueue.d.ts +2 -3
- package/dist/storage/syncQueue.js +10 -17
- package/dist/storage/syncQueue.js.map +1 -1
- package/dist/storage/undo.js +7 -6
- package/dist/storage/undo.js.map +1 -1
- package/dist/stores/backendDataStore.js +3 -1
- package/dist/stores/backendDataStore.js.map +1 -1
- package/dist/stores/formStackStore.d.ts +1 -1
- package/dist/stores/listViewStore.d.ts +6 -6
- package/dist/stores/navigationStore.d.ts +7 -7
- package/dist/stores/undoStore.d.ts +2 -2
- package/dist/sync/SyncManager.d.ts +6 -1
- package/dist/sync/SyncManager.js +80 -76
- package/dist/sync/SyncManager.js.map +1 -1
- package/dist/sync/types.d.ts +6 -7
- package/dist/test-helpers.d.ts +1 -1
- package/dist/test-helpers.js +11 -8
- package/dist/test-helpers.js.map +1 -1
- package/dist/types.d.ts +6 -5
- package/drizzle/0006_dual_id.sql +173 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
package/dist/storage/index.js
CHANGED
|
@@ -12,7 +12,6 @@ const DEFAULT_STATUSES = ['backlog', 'todo', 'in-progress', 'review', 'done'];
|
|
|
12
12
|
const DEFAULT_TYPES = ['epic', 'issue', 'task'];
|
|
13
13
|
const DEFAULT_ITERATIONS = ['default'];
|
|
14
14
|
const DEFAULT_CURRENT_ITERATION = 'default';
|
|
15
|
-
const DEFAULT_NEXT_ID = 1;
|
|
16
15
|
const DEFAULT_BRANCH_MODE = 'worktree';
|
|
17
16
|
const DEFAULT_AUTO_UPDATE = true;
|
|
18
17
|
const DEFAULT_BRANCH_COMMAND = `claude "Brainstorm the implementation of issue #$TIC_ITEM_ID: $TIC_ITEM_TITLE. $TIC_ITEM_DESCRIPTION"`;
|
|
@@ -20,19 +19,24 @@ const DEFAULT_COPY_TO_CLIPBOARD = true;
|
|
|
20
19
|
export class Storage extends BaseBackend {
|
|
21
20
|
db;
|
|
22
21
|
root;
|
|
23
|
-
|
|
24
|
-
constructor(db, root
|
|
22
|
+
_hasRemoteBackend = false;
|
|
23
|
+
constructor(db, root) {
|
|
25
24
|
super(0); // No TTL — DB is always fresh
|
|
26
25
|
this.db = db;
|
|
27
26
|
this.root = root;
|
|
28
|
-
|
|
27
|
+
}
|
|
28
|
+
get hasRemoteBackend() {
|
|
29
|
+
return this._hasRemoteBackend;
|
|
30
|
+
}
|
|
31
|
+
setHasRemoteBackend(value) {
|
|
32
|
+
this._hasRemoteBackend = value;
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* Create a Storage instance, initializing the database and seeding defaults.
|
|
32
36
|
*/
|
|
33
|
-
static create(root
|
|
37
|
+
static create(root) {
|
|
34
38
|
const db = createDatabase(root);
|
|
35
|
-
const backend = new Storage(db, root
|
|
39
|
+
const backend = new Storage(db, root);
|
|
36
40
|
backend.seedDefaults();
|
|
37
41
|
backend.migrateFromYaml();
|
|
38
42
|
return backend;
|
|
@@ -40,8 +44,8 @@ export class Storage extends BaseBackend {
|
|
|
40
44
|
/**
|
|
41
45
|
* Create a Storage instance from an existing database instance (for testing).
|
|
42
46
|
*/
|
|
43
|
-
static createFromDb(db
|
|
44
|
-
const backend = new Storage(db, ':memory:'
|
|
47
|
+
static createFromDb(db) {
|
|
48
|
+
const backend = new Storage(db, ':memory:');
|
|
45
49
|
backend.seedDefaults();
|
|
46
50
|
return backend;
|
|
47
51
|
}
|
|
@@ -62,7 +66,6 @@ export class Storage extends BaseBackend {
|
|
|
62
66
|
id: 1,
|
|
63
67
|
backend: 'drizzle',
|
|
64
68
|
currentIteration: DEFAULT_CURRENT_ITERATION,
|
|
65
|
-
nextId: DEFAULT_NEXT_ID,
|
|
66
69
|
branchMode: DEFAULT_BRANCH_MODE,
|
|
67
70
|
branchCommand: DEFAULT_BRANCH_COMMAND,
|
|
68
71
|
copyToClipboard: DEFAULT_COPY_TO_CLIPBOARD,
|
|
@@ -116,15 +119,6 @@ export class Storage extends BaseBackend {
|
|
|
116
119
|
try {
|
|
117
120
|
const raw = fs.readFileSync(yamlPath, 'utf-8');
|
|
118
121
|
const config = yaml.parse(raw);
|
|
119
|
-
// Recalculate nextId from actual max item ID to prevent collisions
|
|
120
|
-
// (the YAML next_id may be stale if items were synced from a remote)
|
|
121
|
-
const maxRow = this.db.raw
|
|
122
|
-
.prepare('SELECT MAX(CAST(id AS INTEGER)) AS maxId FROM work_items')
|
|
123
|
-
.get();
|
|
124
|
-
const maxId = maxRow?.maxId ?? 0;
|
|
125
|
-
if (maxId >= config.next_id) {
|
|
126
|
-
config.next_id = maxId + 1;
|
|
127
|
-
}
|
|
128
122
|
this.db.transaction((tx) => {
|
|
129
123
|
insertConfigTx(tx, config);
|
|
130
124
|
});
|
|
@@ -229,7 +223,7 @@ export class Storage extends BaseBackend {
|
|
|
229
223
|
const rows = this.db
|
|
230
224
|
.selectDistinct({ label: schema.workItemLabels.label })
|
|
231
225
|
.from(schema.workItemLabels)
|
|
232
|
-
.innerJoin(schema.workItems, eq(schema.workItemLabels.
|
|
226
|
+
.innerJoin(schema.workItems, eq(schema.workItemLabels.workItemRowId, schema.workItems.rowId))
|
|
233
227
|
.where(isNull(schema.workItems.deletedAt))
|
|
234
228
|
.all();
|
|
235
229
|
return rows.map((r) => r.label).sort();
|
|
@@ -285,30 +279,63 @@ export class Storage extends BaseBackend {
|
|
|
285
279
|
if (!row) {
|
|
286
280
|
throw new Error(`Work item #${id} not found`);
|
|
287
281
|
}
|
|
282
|
+
return this.assembleWorkItemByRowId(row);
|
|
283
|
+
}
|
|
284
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
285
|
+
async getWorkItemByRowId(rowId) {
|
|
286
|
+
const row = this.db
|
|
287
|
+
.select()
|
|
288
|
+
.from(schema.workItems)
|
|
289
|
+
.where(and(eq(schema.workItems.rowId, rowId), isNull(schema.workItems.deletedAt)))
|
|
290
|
+
.get();
|
|
291
|
+
if (!row) {
|
|
292
|
+
throw new Error(`Work item with rowId ${rowId} not found`);
|
|
293
|
+
}
|
|
294
|
+
return this.assembleWorkItemByRowId(row);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get the display ID for a rowId, including soft-deleted items.
|
|
298
|
+
* Used by SyncManager to resolve display IDs for delete pushes.
|
|
299
|
+
*/
|
|
300
|
+
getDisplayIdByRowId(rowId) {
|
|
301
|
+
const row = this.db
|
|
302
|
+
.select({ id: schema.workItems.id })
|
|
303
|
+
.from(schema.workItems)
|
|
304
|
+
.where(eq(schema.workItems.rowId, rowId))
|
|
305
|
+
.get();
|
|
306
|
+
if (!row)
|
|
307
|
+
return null;
|
|
308
|
+
return row.id;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Assemble a single work item from its row, loading labels/deps/comments by rowId.
|
|
312
|
+
*/
|
|
313
|
+
assembleWorkItemByRowId(row) {
|
|
288
314
|
const labels = this.db
|
|
289
315
|
.select()
|
|
290
316
|
.from(schema.workItemLabels)
|
|
291
|
-
.where(eq(schema.workItemLabels.
|
|
317
|
+
.where(eq(schema.workItemLabels.workItemRowId, row.rowId))
|
|
292
318
|
.all();
|
|
293
319
|
const deps = this.db
|
|
294
320
|
.select()
|
|
295
321
|
.from(schema.workItemDeps)
|
|
296
|
-
.where(eq(schema.workItemDeps.
|
|
322
|
+
.where(eq(schema.workItemDeps.workItemRowId, row.rowId))
|
|
297
323
|
.all();
|
|
298
324
|
const itemComments = this.db
|
|
299
325
|
.select()
|
|
300
326
|
.from(schema.comments)
|
|
301
|
-
.where(eq(schema.comments.
|
|
327
|
+
.where(eq(schema.comments.workItemRowId, row.rowId))
|
|
302
328
|
.all();
|
|
303
329
|
return rowToWorkItem(row, labels, deps, itemComments);
|
|
304
330
|
}
|
|
305
331
|
// ─── Read: relationships (SQL-optimized overrides) ─────────────────
|
|
306
332
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
307
333
|
async getChildren(id) {
|
|
334
|
+
const parentRowId = this.resolveRowId(id);
|
|
308
335
|
const childRows = this.db
|
|
309
336
|
.select()
|
|
310
337
|
.from(schema.workItems)
|
|
311
|
-
.where(and(eq(schema.workItems.parent,
|
|
338
|
+
.where(and(eq(schema.workItems.parent, parentRowId), isNull(schema.workItems.deletedAt)))
|
|
312
339
|
.all();
|
|
313
340
|
if (childRows.length === 0)
|
|
314
341
|
return [];
|
|
@@ -316,19 +343,20 @@ export class Storage extends BaseBackend {
|
|
|
316
343
|
}
|
|
317
344
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
318
345
|
async getDependents(id) {
|
|
319
|
-
|
|
346
|
+
const targetRowId = this.resolveRowId(id);
|
|
347
|
+
// Find items that depend on `targetRowId`
|
|
320
348
|
const depRows = this.db
|
|
321
|
-
.select({
|
|
349
|
+
.select({ workItemRowId: schema.workItemDeps.workItemRowId })
|
|
322
350
|
.from(schema.workItemDeps)
|
|
323
|
-
.where(eq(schema.workItemDeps.
|
|
351
|
+
.where(eq(schema.workItemDeps.dependsOnRowId, targetRowId))
|
|
324
352
|
.all();
|
|
325
353
|
if (depRows.length === 0)
|
|
326
354
|
return [];
|
|
327
|
-
const
|
|
355
|
+
const dependentRowIds = depRows.map((r) => r.workItemRowId);
|
|
328
356
|
const itemRows = this.db
|
|
329
357
|
.select()
|
|
330
358
|
.from(schema.workItems)
|
|
331
|
-
.where(and(inArray(schema.workItems.
|
|
359
|
+
.where(and(inArray(schema.workItems.rowId, dependentRowIds), isNull(schema.workItems.deletedAt)))
|
|
332
360
|
.all();
|
|
333
361
|
if (itemRows.length === 0)
|
|
334
362
|
return [];
|
|
@@ -338,75 +366,89 @@ export class Storage extends BaseBackend {
|
|
|
338
366
|
* Helper: given a set of work item rows, fetch their labels/deps/comments and assemble.
|
|
339
367
|
*/
|
|
340
368
|
assembleWorkItems(itemRows, options) {
|
|
341
|
-
const
|
|
369
|
+
const rowIds = itemRows.map((r) => r.rowId);
|
|
342
370
|
const labelRows = this.db
|
|
343
371
|
.select()
|
|
344
372
|
.from(schema.workItemLabels)
|
|
345
|
-
.where(inArray(schema.workItemLabels.
|
|
373
|
+
.where(inArray(schema.workItemLabels.workItemRowId, rowIds))
|
|
346
374
|
.all();
|
|
347
375
|
const depRows = this.db
|
|
348
376
|
.select()
|
|
349
377
|
.from(schema.workItemDeps)
|
|
350
|
-
.where(inArray(schema.workItemDeps.
|
|
378
|
+
.where(inArray(schema.workItemDeps.workItemRowId, rowIds))
|
|
351
379
|
.all();
|
|
352
380
|
const commentRows = options?.includeComments
|
|
353
381
|
? this.db
|
|
354
382
|
.select()
|
|
355
383
|
.from(schema.comments)
|
|
356
|
-
.where(inArray(schema.comments.
|
|
384
|
+
.where(inArray(schema.comments.workItemRowId, rowIds))
|
|
357
385
|
.all()
|
|
358
386
|
: [];
|
|
359
387
|
const labelsByItem = new Map();
|
|
360
388
|
for (const l of labelRows) {
|
|
361
|
-
const arr = labelsByItem.get(l.
|
|
389
|
+
const arr = labelsByItem.get(l.workItemRowId);
|
|
362
390
|
if (arr)
|
|
363
391
|
arr.push(l);
|
|
364
392
|
else
|
|
365
|
-
labelsByItem.set(l.
|
|
393
|
+
labelsByItem.set(l.workItemRowId, [l]);
|
|
366
394
|
}
|
|
367
395
|
const depsByItem = new Map();
|
|
368
396
|
for (const d of depRows) {
|
|
369
|
-
const arr = depsByItem.get(d.
|
|
397
|
+
const arr = depsByItem.get(d.workItemRowId);
|
|
370
398
|
if (arr)
|
|
371
399
|
arr.push(d);
|
|
372
400
|
else
|
|
373
|
-
depsByItem.set(d.
|
|
401
|
+
depsByItem.set(d.workItemRowId, [d]);
|
|
374
402
|
}
|
|
375
403
|
const commentsByItem = new Map();
|
|
376
404
|
for (const c of commentRows) {
|
|
377
|
-
const arr = commentsByItem.get(c.
|
|
405
|
+
const arr = commentsByItem.get(c.workItemRowId);
|
|
378
406
|
if (arr)
|
|
379
407
|
arr.push(c);
|
|
380
408
|
else
|
|
381
|
-
commentsByItem.set(c.
|
|
409
|
+
commentsByItem.set(c.workItemRowId, [c]);
|
|
382
410
|
}
|
|
383
|
-
return itemRows.map((row) => rowToWorkItem(row, labelsByItem.get(row.
|
|
411
|
+
return itemRows.map((row) => rowToWorkItem(row, labelsByItem.get(row.rowId) ?? [], depsByItem.get(row.rowId) ?? [], commentsByItem.get(row.rowId) ?? []));
|
|
384
412
|
}
|
|
385
413
|
// ─── Read: item URL ────────────────────────────────────────────────
|
|
386
414
|
getItemUrl(id) {
|
|
387
415
|
return `${this.root}/.tic/items/${id}.md`;
|
|
388
416
|
}
|
|
417
|
+
// ─── Row ID resolution ─────────────────────────────────────────
|
|
418
|
+
/**
|
|
419
|
+
* Resolve a display ID to a rowId. Throws if not found.
|
|
420
|
+
*/
|
|
421
|
+
resolveRowId(displayId) {
|
|
422
|
+
const row = this.db
|
|
423
|
+
.select({ rowId: schema.workItems.rowId })
|
|
424
|
+
.from(schema.workItems)
|
|
425
|
+
.where(and(eq(schema.workItems.id, displayId), isNull(schema.workItems.deletedAt)))
|
|
426
|
+
.get();
|
|
427
|
+
if (!row)
|
|
428
|
+
throw new Error(`Work item "${displayId}" not found`);
|
|
429
|
+
return row.rowId;
|
|
430
|
+
}
|
|
389
431
|
// ─── Relationship validation ─────────────────────────────────────
|
|
390
|
-
validateRelationships(
|
|
432
|
+
validateRelationships(itemRowId, parent, dependsOn) {
|
|
391
433
|
// Validate parent
|
|
392
434
|
if (parent !== null && parent !== undefined) {
|
|
393
|
-
if (parent ===
|
|
394
|
-
throw new Error(`Work item #${
|
|
435
|
+
if (parent === itemRowId) {
|
|
436
|
+
throw new Error(`Work item #${this.displayIdForRowId(itemRowId)} cannot be its own parent`);
|
|
395
437
|
}
|
|
396
438
|
const parentRow = this.db
|
|
397
|
-
.select({
|
|
439
|
+
.select({ rowId: schema.workItems.rowId })
|
|
398
440
|
.from(schema.workItems)
|
|
399
|
-
.where(and(eq(schema.workItems.
|
|
441
|
+
.where(and(eq(schema.workItems.rowId, parent), isNull(schema.workItems.deletedAt)))
|
|
400
442
|
.get();
|
|
401
443
|
if (!parentRow) {
|
|
402
|
-
throw new Error(`Parent #${parent} does not exist`);
|
|
444
|
+
throw new Error(`Parent #${this.displayIdForRowId(parent)} does not exist`);
|
|
403
445
|
}
|
|
404
446
|
// Walk up the parent chain to detect circular references
|
|
405
447
|
let current = parent;
|
|
406
448
|
const visited = new Set();
|
|
407
449
|
while (current !== null) {
|
|
408
|
-
if (current ===
|
|
409
|
-
throw new Error(`Circular parent chain detected for #${
|
|
450
|
+
if (current === itemRowId) {
|
|
451
|
+
throw new Error(`Circular parent chain detected for #${this.displayIdForRowId(itemRowId)}`);
|
|
410
452
|
}
|
|
411
453
|
if (visited.has(current))
|
|
412
454
|
break;
|
|
@@ -414,83 +456,86 @@ export class Storage extends BaseBackend {
|
|
|
414
456
|
const row = this.db
|
|
415
457
|
.select({ parent: schema.workItems.parent })
|
|
416
458
|
.from(schema.workItems)
|
|
417
|
-
.where(and(eq(schema.workItems.
|
|
459
|
+
.where(and(eq(schema.workItems.rowId, current), isNull(schema.workItems.deletedAt)))
|
|
418
460
|
.get();
|
|
419
461
|
current = row?.parent ?? null;
|
|
420
462
|
}
|
|
421
463
|
}
|
|
422
464
|
// Validate dependencies
|
|
423
465
|
if (dependsOn !== undefined && dependsOn.length > 0) {
|
|
424
|
-
for (const
|
|
425
|
-
if (
|
|
426
|
-
throw new Error(`Work item #${
|
|
466
|
+
for (const depRowId of dependsOn) {
|
|
467
|
+
if (depRowId === itemRowId) {
|
|
468
|
+
throw new Error(`Work item #${this.displayIdForRowId(itemRowId)} cannot depend on itself`);
|
|
427
469
|
}
|
|
428
470
|
}
|
|
429
471
|
// Check all deps exist in one query
|
|
430
472
|
const existingRows = this.db
|
|
431
|
-
.select({
|
|
473
|
+
.select({ rowId: schema.workItems.rowId })
|
|
432
474
|
.from(schema.workItems)
|
|
433
|
-
.where(and(inArray(schema.workItems.
|
|
475
|
+
.where(and(inArray(schema.workItems.rowId, dependsOn), isNull(schema.workItems.deletedAt)))
|
|
434
476
|
.all();
|
|
435
|
-
const
|
|
436
|
-
for (const
|
|
437
|
-
if (!
|
|
438
|
-
throw new Error(`Dependency #${
|
|
477
|
+
const existingRowIds = new Set(existingRows.map((r) => r.rowId));
|
|
478
|
+
for (const depRowId of dependsOn) {
|
|
479
|
+
if (!existingRowIds.has(depRowId)) {
|
|
480
|
+
throw new Error(`Dependency #${this.displayIdForRowId(depRowId)} does not exist`);
|
|
439
481
|
}
|
|
440
482
|
}
|
|
441
483
|
// Check for circular dependency chains
|
|
442
|
-
const hasCycle = (
|
|
484
|
+
const hasCycle = (startRowId, targetRowId) => {
|
|
443
485
|
const visited = new Set();
|
|
444
|
-
const stack = [
|
|
486
|
+
const stack = [startRowId];
|
|
445
487
|
while (stack.length > 0) {
|
|
446
488
|
const current = stack.pop();
|
|
447
|
-
if (current ===
|
|
489
|
+
if (current === targetRowId)
|
|
448
490
|
return true;
|
|
449
491
|
if (visited.has(current))
|
|
450
492
|
continue;
|
|
451
493
|
visited.add(current);
|
|
452
494
|
const deps = this.db
|
|
453
|
-
.select({
|
|
495
|
+
.select({
|
|
496
|
+
dependsOnRowId: schema.workItemDeps.dependsOnRowId,
|
|
497
|
+
})
|
|
454
498
|
.from(schema.workItemDeps)
|
|
455
|
-
.where(eq(schema.workItemDeps.
|
|
499
|
+
.where(eq(schema.workItemDeps.workItemRowId, current))
|
|
456
500
|
.all();
|
|
457
501
|
for (const dep of deps) {
|
|
458
|
-
stack.push(dep.
|
|
502
|
+
stack.push(dep.dependsOnRowId);
|
|
459
503
|
}
|
|
460
504
|
}
|
|
461
505
|
return false;
|
|
462
506
|
};
|
|
463
|
-
for (const
|
|
464
|
-
if (hasCycle(
|
|
465
|
-
throw new Error(`Circular dependency chain detected for #${
|
|
507
|
+
for (const depRowId of dependsOn) {
|
|
508
|
+
if (hasCycle(depRowId, itemRowId)) {
|
|
509
|
+
throw new Error(`Circular dependency chain detected for #${this.displayIdForRowId(itemRowId)}`);
|
|
466
510
|
}
|
|
467
511
|
}
|
|
468
512
|
}
|
|
469
513
|
}
|
|
514
|
+
/**
|
|
515
|
+
* Get the display id for a rowId, for error messages.
|
|
516
|
+
* Returns the display id if set, otherwise the rowId as string.
|
|
517
|
+
*/
|
|
518
|
+
displayIdForRowId(rowId) {
|
|
519
|
+
if (rowId === null)
|
|
520
|
+
return 'null';
|
|
521
|
+
const row = this.db
|
|
522
|
+
.select({ id: schema.workItems.id })
|
|
523
|
+
.from(schema.workItems)
|
|
524
|
+
.where(eq(schema.workItems.rowId, rowId))
|
|
525
|
+
.get();
|
|
526
|
+
return row?.id ?? String(rowId);
|
|
527
|
+
}
|
|
470
528
|
// ─── Write: createWorkItem ────────────────────────────────────────
|
|
471
529
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
472
530
|
async createWorkItem(data) {
|
|
473
531
|
this.validateFields(data);
|
|
474
532
|
const now = new Date().toISOString();
|
|
475
|
-
// Single IMMEDIATE transaction:
|
|
476
|
-
// IMMEDIATE acquires a write lock upfront, preventing
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const config = tx
|
|
482
|
-
.select()
|
|
483
|
-
.from(schema.projectConfig)
|
|
484
|
-
.where(eq(schema.projectConfig.id, 1))
|
|
485
|
-
.get();
|
|
486
|
-
const nid = config?.nextId ?? 1;
|
|
487
|
-
const itemId = this.tempIds ? `local-${nid}` : String(nid);
|
|
488
|
-
tx.update(schema.projectConfig)
|
|
489
|
-
.set({ nextId: nid + 1 })
|
|
490
|
-
.where(eq(schema.projectConfig.id, 1))
|
|
491
|
-
.run();
|
|
492
|
-
// Validate relationships
|
|
493
|
-
this.validateRelationships(itemId, data.parent, data.dependsOn);
|
|
533
|
+
// Single IMMEDIATE transaction: validate + insert atomically.
|
|
534
|
+
// IMMEDIATE acquires a write lock upfront, preventing race conditions
|
|
535
|
+
// with parallel MCP calls (#37).
|
|
536
|
+
const result = this.db.transaction((tx) => {
|
|
537
|
+
// Validate relationships (these are rowIds now)
|
|
538
|
+
this.validateRelationships(null, data.parent, data.dependsOn);
|
|
494
539
|
// Ensure iteration exists
|
|
495
540
|
if (data.iteration) {
|
|
496
541
|
tx.insert(schema.iterations)
|
|
@@ -498,10 +543,10 @@ export class Storage extends BaseBackend {
|
|
|
498
543
|
.onConflictDoNothing()
|
|
499
544
|
.run();
|
|
500
545
|
}
|
|
501
|
-
// Insert work item
|
|
502
|
-
tx
|
|
546
|
+
// Insert work item — let SQLite assign rowId via AUTOINCREMENT
|
|
547
|
+
const insertResult = tx
|
|
548
|
+
.insert(schema.workItems)
|
|
503
549
|
.values({
|
|
504
|
-
id: itemId,
|
|
505
550
|
title: data.title,
|
|
506
551
|
type: data.type,
|
|
507
552
|
status: data.status,
|
|
@@ -514,26 +559,36 @@ export class Storage extends BaseBackend {
|
|
|
514
559
|
updated: now,
|
|
515
560
|
})
|
|
516
561
|
.run();
|
|
562
|
+
const rowId = Number(insertResult.lastInsertRowid);
|
|
563
|
+
// Assign display ID: if no remote backend, use rowId as display ID
|
|
564
|
+
const displayId = this._hasRemoteBackend ? null : String(rowId);
|
|
565
|
+
if (displayId !== null) {
|
|
566
|
+
tx.update(schema.workItems)
|
|
567
|
+
.set({ id: displayId })
|
|
568
|
+
.where(eq(schema.workItems.rowId, rowId))
|
|
569
|
+
.run();
|
|
570
|
+
}
|
|
517
571
|
// Insert labels
|
|
518
572
|
if (data.labels.length > 0) {
|
|
519
573
|
tx.insert(schema.workItemLabels)
|
|
520
|
-
.values(data.labels.map((label) => ({
|
|
574
|
+
.values(data.labels.map((label) => ({ workItemRowId: rowId, label })))
|
|
521
575
|
.run();
|
|
522
576
|
}
|
|
523
577
|
// Insert deps
|
|
524
578
|
if (data.dependsOn.length > 0) {
|
|
525
579
|
tx.insert(schema.workItemDeps)
|
|
526
|
-
.values(data.dependsOn.map((
|
|
527
|
-
|
|
528
|
-
|
|
580
|
+
.values(data.dependsOn.map((dependsOnRowId) => ({
|
|
581
|
+
workItemRowId: rowId,
|
|
582
|
+
dependsOnRowId,
|
|
529
583
|
})))
|
|
530
584
|
.run();
|
|
531
585
|
}
|
|
532
|
-
return
|
|
586
|
+
return { rowId, displayId };
|
|
533
587
|
}, { behavior: 'immediate' });
|
|
534
588
|
this.invalidateCache();
|
|
535
589
|
return {
|
|
536
|
-
|
|
590
|
+
rowId: result.rowId,
|
|
591
|
+
id: result.displayId,
|
|
537
592
|
title: data.title,
|
|
538
593
|
type: data.type,
|
|
539
594
|
status: data.status,
|
|
@@ -552,23 +607,7 @@ export class Storage extends BaseBackend {
|
|
|
552
607
|
// ─── Write: importWorkItem (for sync) ───────────────────────────────
|
|
553
608
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
554
609
|
async importWorkItem(item) {
|
|
555
|
-
this.db.transaction((tx) => {
|
|
556
|
-
// Bump nextId past imported numeric IDs to prevent collisions
|
|
557
|
-
const numericId = Number(item.id);
|
|
558
|
-
if (!Number.isNaN(numericId) && numericId > 0) {
|
|
559
|
-
const config = tx
|
|
560
|
-
.select({ nextId: schema.projectConfig.nextId })
|
|
561
|
-
.from(schema.projectConfig)
|
|
562
|
-
.where(eq(schema.projectConfig.id, 1))
|
|
563
|
-
.get();
|
|
564
|
-
const currentNext = config?.nextId ?? 1;
|
|
565
|
-
if (numericId >= currentNext) {
|
|
566
|
-
tx.update(schema.projectConfig)
|
|
567
|
-
.set({ nextId: numericId + 1 })
|
|
568
|
-
.where(eq(schema.projectConfig.id, 1))
|
|
569
|
-
.run();
|
|
570
|
-
}
|
|
571
|
-
}
|
|
610
|
+
const resultRowId = this.db.transaction((tx) => {
|
|
572
611
|
// Ensure iteration exists
|
|
573
612
|
if (item.iteration) {
|
|
574
613
|
tx.insert(schema.iterations)
|
|
@@ -576,24 +615,43 @@ export class Storage extends BaseBackend {
|
|
|
576
615
|
.onConflictDoNothing()
|
|
577
616
|
.run();
|
|
578
617
|
}
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
618
|
+
// Resolve parent: incoming item.parent is a remote numeric ID (as number).
|
|
619
|
+
// Look up the local rowId by display ID.
|
|
620
|
+
let parentRowId = null;
|
|
621
|
+
if (item.parent !== null) {
|
|
622
|
+
const parentRow = tx
|
|
623
|
+
.select({ rowId: schema.workItems.rowId })
|
|
624
|
+
.from(schema.workItems)
|
|
625
|
+
.where(eq(schema.workItems.id, String(item.parent)))
|
|
626
|
+
.get();
|
|
627
|
+
parentRowId = parentRow?.rowId ?? null;
|
|
628
|
+
}
|
|
629
|
+
// Resolve dependsOn: same logic
|
|
630
|
+
const depRowIds = [];
|
|
631
|
+
for (const depId of item.dependsOn) {
|
|
632
|
+
const depRow = tx
|
|
633
|
+
.select({ rowId: schema.workItems.rowId })
|
|
634
|
+
.from(schema.workItems)
|
|
635
|
+
.where(eq(schema.workItems.id, String(depId)))
|
|
636
|
+
.get();
|
|
637
|
+
if (depRow) {
|
|
638
|
+
depRowIds.push(depRow.rowId);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Look up existing row by display id
|
|
642
|
+
const existing = item.id
|
|
643
|
+
? tx
|
|
644
|
+
.select({ rowId: schema.workItems.rowId })
|
|
645
|
+
.from(schema.workItems)
|
|
646
|
+
.where(eq(schema.workItems.id, item.id))
|
|
647
|
+
.get()
|
|
648
|
+
: null;
|
|
649
|
+
let rowId;
|
|
650
|
+
if (existing) {
|
|
651
|
+
// Update existing row
|
|
652
|
+
rowId = existing.rowId;
|
|
653
|
+
tx.update(schema.workItems)
|
|
654
|
+
.set({
|
|
597
655
|
title: item.title,
|
|
598
656
|
type: item.type,
|
|
599
657
|
status: item.status,
|
|
@@ -601,42 +659,63 @@ export class Storage extends BaseBackend {
|
|
|
601
659
|
priority: item.priority,
|
|
602
660
|
assignee: item.assignee,
|
|
603
661
|
description: item.description,
|
|
604
|
-
parent:
|
|
662
|
+
parent: parentRowId,
|
|
605
663
|
created: item.created,
|
|
606
664
|
updated: item.updated,
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
|
|
665
|
+
})
|
|
666
|
+
.where(eq(schema.workItems.rowId, rowId))
|
|
667
|
+
.run();
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
// Insert new row
|
|
671
|
+
const insertResult = tx
|
|
672
|
+
.insert(schema.workItems)
|
|
673
|
+
.values({
|
|
674
|
+
id: item.id,
|
|
675
|
+
title: item.title,
|
|
676
|
+
type: item.type,
|
|
677
|
+
status: item.status,
|
|
678
|
+
iteration: item.iteration,
|
|
679
|
+
priority: item.priority,
|
|
680
|
+
assignee: item.assignee,
|
|
681
|
+
description: item.description,
|
|
682
|
+
parent: parentRowId,
|
|
683
|
+
created: item.created,
|
|
684
|
+
updated: item.updated,
|
|
685
|
+
})
|
|
686
|
+
.run();
|
|
687
|
+
rowId = Number(insertResult.lastInsertRowid);
|
|
688
|
+
}
|
|
610
689
|
// Replace labels
|
|
611
690
|
tx.delete(schema.workItemLabels)
|
|
612
|
-
.where(eq(schema.workItemLabels.
|
|
691
|
+
.where(eq(schema.workItemLabels.workItemRowId, rowId))
|
|
613
692
|
.run();
|
|
614
693
|
if (item.labels.length > 0) {
|
|
615
694
|
tx.insert(schema.workItemLabels)
|
|
616
|
-
.values(item.labels.map((label) => ({
|
|
695
|
+
.values(item.labels.map((label) => ({ workItemRowId: rowId, label })))
|
|
617
696
|
.run();
|
|
618
697
|
}
|
|
619
698
|
// Replace deps
|
|
620
699
|
tx.delete(schema.workItemDeps)
|
|
621
|
-
.where(eq(schema.workItemDeps.
|
|
700
|
+
.where(eq(schema.workItemDeps.workItemRowId, rowId))
|
|
622
701
|
.run();
|
|
623
|
-
if (
|
|
702
|
+
if (depRowIds.length > 0) {
|
|
624
703
|
tx.insert(schema.workItemDeps)
|
|
625
|
-
.values(
|
|
626
|
-
|
|
627
|
-
|
|
704
|
+
.values(depRowIds.map((dependsOnRowId) => ({
|
|
705
|
+
workItemRowId: rowId,
|
|
706
|
+
dependsOnRowId,
|
|
628
707
|
})))
|
|
629
708
|
.run();
|
|
630
709
|
}
|
|
631
710
|
// Replace comments
|
|
632
711
|
tx.delete(schema.comments)
|
|
633
|
-
.where(eq(schema.comments.
|
|
712
|
+
.where(eq(schema.comments.workItemRowId, rowId))
|
|
634
713
|
.run();
|
|
635
714
|
if (item.comments.length > 0) {
|
|
636
715
|
for (const c of item.comments) {
|
|
637
716
|
tx.insert(schema.comments)
|
|
638
717
|
.values({
|
|
639
|
-
|
|
718
|
+
workItemRowId: rowId,
|
|
640
719
|
author: c.author,
|
|
641
720
|
body: c.body,
|
|
642
721
|
created: c.date,
|
|
@@ -644,9 +723,19 @@ export class Storage extends BaseBackend {
|
|
|
644
723
|
.run();
|
|
645
724
|
}
|
|
646
725
|
}
|
|
726
|
+
return rowId;
|
|
647
727
|
});
|
|
648
728
|
this.invalidateCache();
|
|
649
|
-
return item;
|
|
729
|
+
return { ...item, rowId: resultRowId };
|
|
730
|
+
}
|
|
731
|
+
// ─── Write: setDisplayId ───────────────────────────────────────────
|
|
732
|
+
setDisplayId(rowId, displayId) {
|
|
733
|
+
this.db
|
|
734
|
+
.update(schema.workItems)
|
|
735
|
+
.set({ id: displayId })
|
|
736
|
+
.where(eq(schema.workItems.rowId, rowId))
|
|
737
|
+
.run();
|
|
738
|
+
this.invalidateCache();
|
|
650
739
|
}
|
|
651
740
|
// ─── Write: updateWorkItem ────────────────────────────────────────
|
|
652
741
|
async updateWorkItem(id, data) {
|
|
@@ -660,11 +749,12 @@ export class Storage extends BaseBackend {
|
|
|
660
749
|
if (!existingRow) {
|
|
661
750
|
throw new Error(`Work item #${id} not found`);
|
|
662
751
|
}
|
|
752
|
+
const rowId = existingRow.rowId;
|
|
663
753
|
// 2. Validate relationships if parent/dependsOn changed
|
|
664
754
|
const newParent = 'parent' in data ? data.parent : undefined;
|
|
665
755
|
const newDepsOn = 'dependsOn' in data ? data.dependsOn : undefined;
|
|
666
756
|
if (newParent !== undefined || newDepsOn !== undefined) {
|
|
667
|
-
this.validateRelationships(
|
|
757
|
+
this.validateRelationships(rowId, newParent !== undefined ? (newParent ?? null) : undefined, newDepsOn);
|
|
668
758
|
}
|
|
669
759
|
const now = new Date().toISOString();
|
|
670
760
|
// 3. In a transaction: update workItems row, delete+re-insert labels/deps
|
|
@@ -689,29 +779,29 @@ export class Storage extends BaseBackend {
|
|
|
689
779
|
updateSet['parent'] = data.parent ?? null;
|
|
690
780
|
tx.update(schema.workItems)
|
|
691
781
|
.set(updateSet)
|
|
692
|
-
.where(eq(schema.workItems.
|
|
782
|
+
.where(eq(schema.workItems.rowId, rowId))
|
|
693
783
|
.run();
|
|
694
784
|
// Replace labels if changed
|
|
695
785
|
if ('labels' in data && data.labels !== undefined) {
|
|
696
786
|
tx.delete(schema.workItemLabels)
|
|
697
|
-
.where(eq(schema.workItemLabels.
|
|
787
|
+
.where(eq(schema.workItemLabels.workItemRowId, rowId))
|
|
698
788
|
.run();
|
|
699
789
|
if (data.labels.length > 0) {
|
|
700
790
|
tx.insert(schema.workItemLabels)
|
|
701
|
-
.values(data.labels.map((label) => ({
|
|
791
|
+
.values(data.labels.map((label) => ({ workItemRowId: rowId, label })))
|
|
702
792
|
.run();
|
|
703
793
|
}
|
|
704
794
|
}
|
|
705
795
|
// Replace deps if changed
|
|
706
796
|
if ('dependsOn' in data && data.dependsOn !== undefined) {
|
|
707
797
|
tx.delete(schema.workItemDeps)
|
|
708
|
-
.where(eq(schema.workItemDeps.
|
|
798
|
+
.where(eq(schema.workItemDeps.workItemRowId, rowId))
|
|
709
799
|
.run();
|
|
710
800
|
if (data.dependsOn.length > 0) {
|
|
711
801
|
tx.insert(schema.workItemDeps)
|
|
712
|
-
.values(data.dependsOn.map((
|
|
713
|
-
|
|
714
|
-
|
|
802
|
+
.values(data.dependsOn.map((dependsOnRowId) => ({
|
|
803
|
+
workItemRowId: rowId,
|
|
804
|
+
dependsOnRowId,
|
|
715
805
|
})))
|
|
716
806
|
.run();
|
|
717
807
|
}
|
|
@@ -734,23 +824,23 @@ export class Storage extends BaseBackend {
|
|
|
734
824
|
.all()
|
|
735
825
|
.slice(-1)
|
|
736
826
|
.map((r) => r.name));
|
|
737
|
-
const cascadeIteration = (
|
|
827
|
+
const cascadeIteration = (parentRowId) => {
|
|
738
828
|
const children = tx
|
|
739
829
|
.select()
|
|
740
830
|
.from(schema.workItems)
|
|
741
|
-
.where(and(eq(schema.workItems.parent,
|
|
831
|
+
.where(and(eq(schema.workItems.parent, parentRowId), isNull(schema.workItems.deletedAt)))
|
|
742
832
|
.all();
|
|
743
833
|
for (const child of children) {
|
|
744
834
|
if (closedStatuses.has(child.status))
|
|
745
835
|
continue;
|
|
746
836
|
tx.update(schema.workItems)
|
|
747
837
|
.set({ iteration: data.iteration, updated: now })
|
|
748
|
-
.where(eq(schema.workItems.
|
|
838
|
+
.where(eq(schema.workItems.rowId, child.rowId))
|
|
749
839
|
.run();
|
|
750
|
-
cascadeIteration(child.
|
|
840
|
+
cascadeIteration(child.rowId);
|
|
751
841
|
}
|
|
752
842
|
};
|
|
753
|
-
cascadeIteration(
|
|
843
|
+
cascadeIteration(rowId);
|
|
754
844
|
}
|
|
755
845
|
});
|
|
756
846
|
this.invalidateCache();
|
|
@@ -760,18 +850,21 @@ export class Storage extends BaseBackend {
|
|
|
760
850
|
// ─── Write: deleteWorkItem ────────────────────────────────────────
|
|
761
851
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
762
852
|
async deleteWorkItem(id) {
|
|
853
|
+
const rowId = this.resolveRowId(id);
|
|
763
854
|
this.db.transaction((tx) => {
|
|
764
855
|
// 1. Null out parent on children
|
|
765
856
|
tx.update(schema.workItems)
|
|
766
857
|
.set({ parent: null })
|
|
767
|
-
.where(eq(schema.workItems.parent,
|
|
858
|
+
.where(eq(schema.workItems.parent, rowId))
|
|
768
859
|
.run();
|
|
769
860
|
// 2. Remove deps referencing this item (other items depending on this one)
|
|
770
861
|
tx.delete(schema.workItemDeps)
|
|
771
|
-
.where(eq(schema.workItemDeps.
|
|
862
|
+
.where(eq(schema.workItemDeps.dependsOnRowId, rowId))
|
|
772
863
|
.run();
|
|
773
864
|
// 3. Delete the item (cascade handles labels, deps, comments of this item)
|
|
774
|
-
tx.delete(schema.workItems)
|
|
865
|
+
tx.delete(schema.workItems)
|
|
866
|
+
.where(eq(schema.workItems.rowId, rowId))
|
|
867
|
+
.run();
|
|
775
868
|
});
|
|
776
869
|
this.invalidateCache();
|
|
777
870
|
}
|
|
@@ -779,31 +872,32 @@ export class Storage extends BaseBackend {
|
|
|
779
872
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
780
873
|
async softDeleteWorkItem(id) {
|
|
781
874
|
const now = new Date().toISOString();
|
|
875
|
+
// Resolve without the deletedAt filter since this method may be called on any item
|
|
876
|
+
const row = this.db
|
|
877
|
+
.select({ rowId: schema.workItems.rowId })
|
|
878
|
+
.from(schema.workItems)
|
|
879
|
+
.where(eq(schema.workItems.id, id))
|
|
880
|
+
.get();
|
|
881
|
+
if (!row)
|
|
882
|
+
return;
|
|
782
883
|
this.db
|
|
783
884
|
.update(schema.workItems)
|
|
784
885
|
.set({ deletedAt: now })
|
|
785
|
-
.where(eq(schema.workItems.
|
|
886
|
+
.where(eq(schema.workItems.rowId, row.rowId))
|
|
786
887
|
.run();
|
|
787
888
|
this.invalidateCache();
|
|
788
889
|
}
|
|
789
890
|
// ─── Write: addComment ────────────────────────────────────────────
|
|
790
891
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
791
892
|
async addComment(workItemId, comment) {
|
|
792
|
-
// 1. Verify item exists
|
|
793
|
-
const
|
|
794
|
-
.select({ id: schema.workItems.id })
|
|
795
|
-
.from(schema.workItems)
|
|
796
|
-
.where(and(eq(schema.workItems.id, workItemId), isNull(schema.workItems.deletedAt)))
|
|
797
|
-
.get();
|
|
798
|
-
if (!row) {
|
|
799
|
-
throw new Error(`Work item #${workItemId} not found`);
|
|
800
|
-
}
|
|
893
|
+
// 1. Verify item exists and resolve rowId
|
|
894
|
+
const rowId = this.resolveRowId(workItemId);
|
|
801
895
|
// 2. Insert comment (do NOT update work item's updated timestamp)
|
|
802
896
|
const now = new Date().toISOString();
|
|
803
897
|
this.db
|
|
804
898
|
.insert(schema.comments)
|
|
805
899
|
.values({
|
|
806
|
-
|
|
900
|
+
workItemRowId: rowId,
|
|
807
901
|
author: comment.author,
|
|
808
902
|
body: comment.body,
|
|
809
903
|
created: now,
|
|
@@ -819,17 +913,35 @@ export class Storage extends BaseBackend {
|
|
|
819
913
|
// ─── Write: restore (undo soft delete) ──────────────────────────
|
|
820
914
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
821
915
|
async restoreWorkItem(id) {
|
|
916
|
+
// Look up by display id without deletedAt filter (item is soft-deleted)
|
|
917
|
+
const row = this.db
|
|
918
|
+
.select({ rowId: schema.workItems.rowId })
|
|
919
|
+
.from(schema.workItems)
|
|
920
|
+
.where(eq(schema.workItems.id, id))
|
|
921
|
+
.get();
|
|
922
|
+
if (!row)
|
|
923
|
+
return;
|
|
822
924
|
this.db
|
|
823
925
|
.update(schema.workItems)
|
|
824
926
|
.set({ deletedAt: null })
|
|
825
|
-
.where(eq(schema.workItems.
|
|
927
|
+
.where(eq(schema.workItems.rowId, row.rowId))
|
|
826
928
|
.run();
|
|
827
929
|
this.invalidateCache();
|
|
828
930
|
}
|
|
829
931
|
// ─── Write: permanent delete ───────────────────────────────────
|
|
830
932
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
831
933
|
async permanentlyDeleteWorkItem(id) {
|
|
832
|
-
this.db
|
|
934
|
+
const row = this.db
|
|
935
|
+
.select({ rowId: schema.workItems.rowId })
|
|
936
|
+
.from(schema.workItems)
|
|
937
|
+
.where(eq(schema.workItems.id, id))
|
|
938
|
+
.get();
|
|
939
|
+
if (!row)
|
|
940
|
+
return;
|
|
941
|
+
this.db
|
|
942
|
+
.delete(schema.workItems)
|
|
943
|
+
.where(eq(schema.workItems.rowId, row.rowId))
|
|
944
|
+
.run();
|
|
833
945
|
this.invalidateCache();
|
|
834
946
|
}
|
|
835
947
|
// ─── Write: cleanup all soft-deleted items ─────────────────────
|
|
@@ -1084,9 +1196,9 @@ export class Storage extends BaseBackend {
|
|
|
1084
1196
|
for (const link of linkRows) {
|
|
1085
1197
|
const arr = linksByPr.get(link.prId);
|
|
1086
1198
|
if (arr)
|
|
1087
|
-
arr.push(link.
|
|
1199
|
+
arr.push(link.itemRowId);
|
|
1088
1200
|
else
|
|
1089
|
-
linksByPr.set(link.prId, [link.
|
|
1201
|
+
linksByPr.set(link.prId, [link.itemRowId]);
|
|
1090
1202
|
}
|
|
1091
1203
|
return prRows.map((row) => rowToPullRequest(row, linksByPr.get(row.id) ?? []));
|
|
1092
1204
|
}
|
|
@@ -1104,7 +1216,7 @@ export class Storage extends BaseBackend {
|
|
|
1104
1216
|
.from(schema.prItemLinks)
|
|
1105
1217
|
.where(eq(schema.prItemLinks.prId, id))
|
|
1106
1218
|
.all();
|
|
1107
|
-
return rowToPullRequest(row, linkRows.map((l) => l.
|
|
1219
|
+
return rowToPullRequest(row, linkRows.map((l) => l.itemRowId));
|
|
1108
1220
|
}
|
|
1109
1221
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
1110
1222
|
async importPullRequest(pr) {
|
|
@@ -1146,17 +1258,18 @@ export class Storage extends BaseBackend {
|
|
|
1146
1258
|
.run();
|
|
1147
1259
|
if (pr.linkedItems.length > 0) {
|
|
1148
1260
|
tx.insert(schema.prItemLinks)
|
|
1149
|
-
.values(pr.linkedItems.map((
|
|
1261
|
+
.values(pr.linkedItems.map((itemRowId) => ({ prId: pr.id, itemRowId })))
|
|
1150
1262
|
.run();
|
|
1151
1263
|
}
|
|
1152
1264
|
});
|
|
1153
1265
|
}
|
|
1154
1266
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
1155
1267
|
async getLinkedPullRequests(itemId) {
|
|
1268
|
+
const itemRowId = this.resolveRowId(itemId);
|
|
1156
1269
|
const linkRows = this.db
|
|
1157
1270
|
.select({ prId: schema.prItemLinks.prId })
|
|
1158
1271
|
.from(schema.prItemLinks)
|
|
1159
|
-
.where(eq(schema.prItemLinks.
|
|
1272
|
+
.where(eq(schema.prItemLinks.itemRowId, itemRowId))
|
|
1160
1273
|
.all();
|
|
1161
1274
|
if (linkRows.length === 0)
|
|
1162
1275
|
return [];
|
|
@@ -1178,34 +1291,42 @@ export class Storage extends BaseBackend {
|
|
|
1178
1291
|
for (const link of allLinks) {
|
|
1179
1292
|
const arr = linksByPr.get(link.prId);
|
|
1180
1293
|
if (arr)
|
|
1181
|
-
arr.push(link.
|
|
1294
|
+
arr.push(link.itemRowId);
|
|
1182
1295
|
else
|
|
1183
|
-
linksByPr.set(link.prId, [link.
|
|
1296
|
+
linksByPr.set(link.prId, [link.itemRowId]);
|
|
1184
1297
|
}
|
|
1185
1298
|
return prRows.map((row) => rowToPullRequest(row, linksByPr.get(row.id) ?? []));
|
|
1186
1299
|
}
|
|
1187
1300
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
1188
1301
|
async getLinkedItems(prId) {
|
|
1189
1302
|
const rows = this.db
|
|
1190
|
-
.select({
|
|
1303
|
+
.select({
|
|
1304
|
+
itemRowId: schema.prItemLinks.itemRowId,
|
|
1305
|
+
itemId: schema.workItems.id,
|
|
1306
|
+
})
|
|
1191
1307
|
.from(schema.prItemLinks)
|
|
1308
|
+
.innerJoin(schema.workItems, eq(schema.prItemLinks.itemRowId, schema.workItems.rowId))
|
|
1192
1309
|
.where(eq(schema.prItemLinks.prId, prId))
|
|
1193
1310
|
.all();
|
|
1194
|
-
return rows
|
|
1311
|
+
return rows
|
|
1312
|
+
.map((r) => r.itemId ?? String(r.itemRowId))
|
|
1313
|
+
.filter((id) => id.length > 0);
|
|
1195
1314
|
}
|
|
1196
1315
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
1197
1316
|
async linkItem(prId, itemId) {
|
|
1317
|
+
const itemRowId = this.resolveRowId(itemId);
|
|
1198
1318
|
this.db
|
|
1199
1319
|
.insert(schema.prItemLinks)
|
|
1200
|
-
.values({ prId,
|
|
1320
|
+
.values({ prId, itemRowId })
|
|
1201
1321
|
.onConflictDoNothing()
|
|
1202
1322
|
.run();
|
|
1203
1323
|
}
|
|
1204
1324
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
1205
1325
|
async unlinkItem(prId, itemId) {
|
|
1326
|
+
const itemRowId = this.resolveRowId(itemId);
|
|
1206
1327
|
this.db
|
|
1207
1328
|
.delete(schema.prItemLinks)
|
|
1208
|
-
.where(and(eq(schema.prItemLinks.prId, prId), eq(schema.prItemLinks.
|
|
1329
|
+
.where(and(eq(schema.prItemLinks.prId, prId), eq(schema.prItemLinks.itemRowId, itemRowId)))
|
|
1209
1330
|
.run();
|
|
1210
1331
|
}
|
|
1211
1332
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/require-await
|