trekoon 0.2.0 → 0.2.1
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/.agents/skills/trekoon/SKILL.md +174 -301
- package/README.md +215 -4
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +116 -0
- package/src/commands/dep.ts +197 -25
- package/src/commands/epic.ts +490 -28
- package/src/commands/error-utils.ts +111 -0
- package/src/commands/events.ts +23 -3
- package/src/commands/help.ts +36 -4
- package/src/commands/init.ts +11 -3
- package/src/commands/migrate.ts +11 -4
- package/src/commands/subtask.ts +224 -26
- package/src/commands/sync.ts +7 -1
- package/src/commands/task.ts +197 -26
- package/src/domain/mutation-service.ts +152 -0
- package/src/domain/tracker-domain.ts +503 -0
- package/src/domain/types.ts +80 -0
- package/src/sync/service.ts +42 -0
package/src/commands/task.ts
CHANGED
|
@@ -2,24 +2,29 @@ import {
|
|
|
2
2
|
SEARCH_REPLACE_FIELDS,
|
|
3
3
|
findUnknownOption,
|
|
4
4
|
hasFlag,
|
|
5
|
+
isValidCompactTempKey,
|
|
5
6
|
parseArgs,
|
|
7
|
+
parseCompactFields,
|
|
6
8
|
parseCsvEnumOption,
|
|
7
9
|
parseStrictNonNegativeInt,
|
|
8
10
|
parseStrictPositiveInt,
|
|
9
11
|
readEnumOption,
|
|
10
12
|
readMissingOptionValue,
|
|
11
13
|
readOption,
|
|
14
|
+
readOptions,
|
|
15
|
+
readUnexpectedPositionals,
|
|
12
16
|
resolvePreviewApplyMode,
|
|
13
17
|
suggestOptions,
|
|
14
18
|
} from "./arg-parser";
|
|
19
|
+
import { unexpectedFailureResult } from "./error-utils";
|
|
15
20
|
|
|
16
21
|
import { MutationService } from "../domain/mutation-service";
|
|
17
22
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
18
|
-
import {
|
|
23
|
+
import { type CompactBatchResultContract, type CompactTaskSpec, type SearchEntityMatch, type TaskRecord } from "../domain/types";
|
|
19
24
|
import { formatHumanTable } from "../io/human-table";
|
|
20
25
|
import { failResult, okResult } from "../io/output";
|
|
21
26
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
22
|
-
import { openTrekoonDatabase } from "../storage/database";
|
|
27
|
+
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
23
28
|
|
|
24
29
|
function formatTask(task: TaskRecord): string {
|
|
25
30
|
return `${task.id} | epic=${task.epicId} | ${task.title} | ${task.status}`;
|
|
@@ -33,6 +38,7 @@ const READY_REASON_READY = "all_dependencies_done";
|
|
|
33
38
|
const READY_REASON_BLOCKED = "blocked_by_dependencies";
|
|
34
39
|
const SEARCH_OPTIONS = ["fields", "preview"] as const;
|
|
35
40
|
const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
|
|
41
|
+
const CREATE_MANY_OPTIONS = ["epic", "e", "task"] as const;
|
|
36
42
|
|
|
37
43
|
interface DependencyBlocker {
|
|
38
44
|
readonly id: string;
|
|
@@ -383,29 +389,9 @@ function formatTaskShowTable(taskTree: {
|
|
|
383
389
|
}
|
|
384
390
|
|
|
385
391
|
function failFromError(error: unknown): CliResult {
|
|
386
|
-
|
|
387
|
-
return failResult({
|
|
388
|
-
command: "task",
|
|
389
|
-
human: error.message,
|
|
390
|
-
data: {
|
|
391
|
-
code: error.code,
|
|
392
|
-
...(error.details ?? {}),
|
|
393
|
-
},
|
|
394
|
-
error: {
|
|
395
|
-
code: error.code,
|
|
396
|
-
message: error.message,
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return failResult({
|
|
392
|
+
return unexpectedFailureResult(error, {
|
|
402
393
|
command: "task",
|
|
403
394
|
human: "Unexpected task command failure",
|
|
404
|
-
data: {},
|
|
405
|
-
error: {
|
|
406
|
-
code: "internal_error",
|
|
407
|
-
message: "Unexpected task command failure",
|
|
408
|
-
},
|
|
409
395
|
});
|
|
410
396
|
}
|
|
411
397
|
|
|
@@ -424,10 +410,145 @@ function failMissingOptionValue(command: string, option: string): CliResult {
|
|
|
424
410
|
});
|
|
425
411
|
}
|
|
426
412
|
|
|
413
|
+
function failBatchSpec(command: string, human: string, data: Record<string, unknown>): CliResult {
|
|
414
|
+
return failResult({
|
|
415
|
+
command,
|
|
416
|
+
human,
|
|
417
|
+
data,
|
|
418
|
+
error: {
|
|
419
|
+
code: "invalid_input",
|
|
420
|
+
message: human,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function failUnexpectedPositionals(command: string, unexpected: readonly string[]): CliResult {
|
|
426
|
+
return failBatchSpec(command, `Unexpected positional arguments: ${unexpected.join(", ")}.`, {
|
|
427
|
+
unexpectedPositionals: unexpected,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function failEmptyCompactField(command: string, option: string, index: number, rawSpec: string, field: string): CliResult {
|
|
432
|
+
return failBatchSpec(command, `${option === "task" ? "Task" : "Spec"} spec ${index + 1} is missing a ${field}.`, {
|
|
433
|
+
option,
|
|
434
|
+
index,
|
|
435
|
+
rawSpec,
|
|
436
|
+
field,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function parseTaskCreateManySpecs(rawSpecs: readonly string[]): { specs: CompactTaskSpec[]; error?: CliResult } {
|
|
441
|
+
const specs: CompactTaskSpec[] = [];
|
|
442
|
+
const seenTempKeys = new Set<string>();
|
|
443
|
+
|
|
444
|
+
for (const [index, rawSpec] of rawSpecs.entries()) {
|
|
445
|
+
const parsed = parseCompactFields(rawSpec);
|
|
446
|
+
if (parsed.invalidEscape !== null) {
|
|
447
|
+
return {
|
|
448
|
+
specs: [],
|
|
449
|
+
error: failBatchSpec("task.create-many", `Invalid escape sequence ${parsed.invalidEscape} in --task spec ${index + 1}.`, {
|
|
450
|
+
option: "task",
|
|
451
|
+
index,
|
|
452
|
+
rawSpec,
|
|
453
|
+
invalidEscape: parsed.invalidEscape,
|
|
454
|
+
}),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (parsed.hasDanglingEscape) {
|
|
459
|
+
return {
|
|
460
|
+
specs: [],
|
|
461
|
+
error: failBatchSpec("task.create-many", `Trailing escape in --task spec ${index + 1}.`, {
|
|
462
|
+
option: "task",
|
|
463
|
+
index,
|
|
464
|
+
rawSpec,
|
|
465
|
+
}),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (parsed.fields.length !== 4) {
|
|
470
|
+
return {
|
|
471
|
+
specs: [],
|
|
472
|
+
error: failBatchSpec("task.create-many", `Task specs must use <temp-key>|<title>|<description>|<status> in --task spec ${index + 1}.`, {
|
|
473
|
+
option: "task",
|
|
474
|
+
index,
|
|
475
|
+
rawSpec,
|
|
476
|
+
fields: parsed.fields,
|
|
477
|
+
}),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const tempKey = parsed.fields[0] ?? "";
|
|
482
|
+
const title = parsed.fields[1] ?? "";
|
|
483
|
+
const description = parsed.fields[2] ?? "";
|
|
484
|
+
const status = parsed.fields[3] ?? "";
|
|
485
|
+
if (!tempKey || !isValidCompactTempKey(tempKey)) {
|
|
486
|
+
return {
|
|
487
|
+
specs: [],
|
|
488
|
+
error: failBatchSpec("task.create-many", `Task spec ${index + 1} must start with a temp key like seed-1.`, {
|
|
489
|
+
option: "task",
|
|
490
|
+
index,
|
|
491
|
+
rawSpec,
|
|
492
|
+
tempKey,
|
|
493
|
+
}),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (seenTempKeys.has(tempKey)) {
|
|
498
|
+
return {
|
|
499
|
+
specs: [],
|
|
500
|
+
error: failBatchSpec("task.create-many", `Duplicate temp key '${tempKey}' in --task specs.`, {
|
|
501
|
+
option: "task",
|
|
502
|
+
index,
|
|
503
|
+
rawSpec,
|
|
504
|
+
tempKey,
|
|
505
|
+
}),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!title || title.trim().length === 0) {
|
|
510
|
+
return {
|
|
511
|
+
specs: [],
|
|
512
|
+
error: failBatchSpec("task.create-many", `Task spec ${index + 1} is missing a title.`, {
|
|
513
|
+
option: "task",
|
|
514
|
+
index,
|
|
515
|
+
rawSpec,
|
|
516
|
+
}),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (description.trim().length === 0) {
|
|
521
|
+
return {
|
|
522
|
+
specs: [],
|
|
523
|
+
error: failEmptyCompactField("task.create-many", "task", index, rawSpec, "description"),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
seenTempKeys.add(tempKey);
|
|
528
|
+
const spec: CompactTaskSpec = status.length > 0
|
|
529
|
+
? {
|
|
530
|
+
tempKey,
|
|
531
|
+
title,
|
|
532
|
+
description,
|
|
533
|
+
status,
|
|
534
|
+
}
|
|
535
|
+
: {
|
|
536
|
+
tempKey,
|
|
537
|
+
title,
|
|
538
|
+
description,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
specs.push(spec);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return { specs };
|
|
545
|
+
}
|
|
546
|
+
|
|
427
547
|
export async function runTask(context: CliContext): Promise<CliResult> {
|
|
428
|
-
|
|
548
|
+
let database: TrekoonDatabase | undefined;
|
|
429
549
|
|
|
430
550
|
try {
|
|
551
|
+
database = openTrekoonDatabase(context.cwd);
|
|
431
552
|
const parsed = parseArgs(context.args);
|
|
432
553
|
const subcommand: string | undefined = parsed.positional[0];
|
|
433
554
|
const domain = new TrackerDomain(database.db);
|
|
@@ -460,6 +581,56 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
460
581
|
data: { task },
|
|
461
582
|
});
|
|
462
583
|
}
|
|
584
|
+
case "create-many": {
|
|
585
|
+
const createManyUnknownOption = findUnknownOption(parsed, CREATE_MANY_OPTIONS);
|
|
586
|
+
if (createManyUnknownOption !== undefined) {
|
|
587
|
+
return unknownOption("task.create-many", createManyUnknownOption, CREATE_MANY_OPTIONS);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const missingCreateManyOption = readMissingOptionValue(parsed.missingOptionValues, "epic", "e", "task");
|
|
591
|
+
if (missingCreateManyOption !== undefined) {
|
|
592
|
+
return failMissingOptionValue("task.create-many", missingCreateManyOption);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const unexpectedPositionals = readUnexpectedPositionals(parsed, 1);
|
|
596
|
+
if (unexpectedPositionals.length > 0) {
|
|
597
|
+
return failUnexpectedPositionals("task.create-many", unexpectedPositionals);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const epicId = readOption(parsed.options, "epic", "e");
|
|
601
|
+
if (epicId === undefined || epicId.trim().length === 0) {
|
|
602
|
+
return failBatchSpec("task.create-many", "Provide --epic for task create-many.", {
|
|
603
|
+
option: "epic",
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const rawSpecs = readOptions(parsed.optionEntries, "task");
|
|
608
|
+
if (rawSpecs.length === 0) {
|
|
609
|
+
return failBatchSpec("task.create-many", "Provide at least one --task spec.", {
|
|
610
|
+
option: "task",
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const specResult = parseTaskCreateManySpecs(rawSpecs);
|
|
615
|
+
if (specResult.error !== undefined) {
|
|
616
|
+
return specResult.error;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const created = mutations.createTaskBatch({
|
|
620
|
+
epicId,
|
|
621
|
+
specs: specResult.specs,
|
|
622
|
+
});
|
|
623
|
+
const result: CompactBatchResultContract = created.result;
|
|
624
|
+
return okResult({
|
|
625
|
+
command: "task.create-many",
|
|
626
|
+
human: `Created ${created.tasks.length} task(s): ${created.tasks.map(formatTask).join("\n")}`,
|
|
627
|
+
data: {
|
|
628
|
+
epicId,
|
|
629
|
+
tasks: created.tasks,
|
|
630
|
+
result,
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
}
|
|
463
634
|
case "list": {
|
|
464
635
|
const missingListOption =
|
|
465
636
|
readMissingOptionValue(parsed.missingOptionValues, "view") ??
|
|
@@ -1084,7 +1255,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
1084
1255
|
default:
|
|
1085
1256
|
return failResult({
|
|
1086
1257
|
command: "task",
|
|
1087
|
-
human: "Usage: trekoon task <create|list|show|ready|next|search|replace|update|delete>",
|
|
1258
|
+
human: "Usage: trekoon task <create|create-many|list|show|ready|next|search|replace|update|delete>",
|
|
1088
1259
|
data: {
|
|
1089
1260
|
args: context.args,
|
|
1090
1261
|
},
|
|
@@ -1097,6 +1268,6 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
1097
1268
|
} catch (error: unknown) {
|
|
1098
1269
|
return failFromError(error);
|
|
1099
1270
|
} finally {
|
|
1100
|
-
database
|
|
1271
|
+
database?.close();
|
|
1101
1272
|
}
|
|
1102
1273
|
}
|
|
@@ -4,6 +4,14 @@ import { appendEventWithGitContext } from "../sync/event-writes";
|
|
|
4
4
|
import { ENTITY_OPERATIONS } from "./mutation-operations";
|
|
5
5
|
import { TrackerDomain } from "./tracker-domain";
|
|
6
6
|
import {
|
|
7
|
+
type CompactEpicCreateResult,
|
|
8
|
+
type CompactEpicExpandResult,
|
|
9
|
+
type CompactDependencyBatchAddResult,
|
|
10
|
+
type CompactDependencySpec,
|
|
11
|
+
type CompactSubtaskBatchCreateResult,
|
|
12
|
+
type CompactSubtaskSpec,
|
|
13
|
+
type CompactTaskBatchCreateResult,
|
|
14
|
+
type CompactTaskSpec,
|
|
7
15
|
type DependencyRecord,
|
|
8
16
|
type EpicRecord,
|
|
9
17
|
type SearchEntityMatch,
|
|
@@ -104,6 +112,66 @@ export class MutationService {
|
|
|
104
112
|
})();
|
|
105
113
|
}
|
|
106
114
|
|
|
115
|
+
createEpicGraph(input: {
|
|
116
|
+
title: string;
|
|
117
|
+
description: string;
|
|
118
|
+
status?: string | undefined;
|
|
119
|
+
taskSpecs: readonly CompactTaskSpec[];
|
|
120
|
+
subtaskSpecs: readonly CompactSubtaskSpec[];
|
|
121
|
+
dependencySpecs: readonly CompactDependencySpec[];
|
|
122
|
+
}): CompactEpicCreateResult {
|
|
123
|
+
return this.#db.transaction((): CompactEpicCreateResult => {
|
|
124
|
+
const epic = this.#domain.createEpic(input);
|
|
125
|
+
const created = this.#domain.expandEpic({
|
|
126
|
+
epicId: epic.id,
|
|
127
|
+
taskSpecs: input.taskSpecs,
|
|
128
|
+
subtaskSpecs: input.subtaskSpecs,
|
|
129
|
+
dependencySpecs: input.dependencySpecs,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.#appendEntityEvent("epic", epic.id, ENTITY_OPERATIONS.epic.created, {
|
|
133
|
+
title: epic.title,
|
|
134
|
+
description: epic.description,
|
|
135
|
+
status: epic.status,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
for (const task of created.tasks) {
|
|
139
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.created, {
|
|
140
|
+
epic_id: task.epicId,
|
|
141
|
+
title: task.title,
|
|
142
|
+
description: task.description,
|
|
143
|
+
status: task.status,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const subtask of created.subtasks) {
|
|
148
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.created, {
|
|
149
|
+
task_id: subtask.taskId,
|
|
150
|
+
title: subtask.title,
|
|
151
|
+
description: subtask.description,
|
|
152
|
+
status: subtask.status,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const dependency of created.dependencies) {
|
|
157
|
+
this.#appendEntityEvent("dependency", dependency.id, ENTITY_OPERATIONS.dependency.added, {
|
|
158
|
+
source_id: dependency.sourceId,
|
|
159
|
+
source_kind: dependency.sourceKind,
|
|
160
|
+
depends_on_id: dependency.dependsOnId,
|
|
161
|
+
depends_on_kind: dependency.dependsOnKind,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
epic,
|
|
167
|
+
tasks: created.tasks,
|
|
168
|
+
subtasks: created.subtasks,
|
|
169
|
+
dependencies: created.dependencies,
|
|
170
|
+
result: created.result,
|
|
171
|
+
};
|
|
172
|
+
})();
|
|
173
|
+
}
|
|
174
|
+
|
|
107
175
|
updateEpic(
|
|
108
176
|
id: string,
|
|
109
177
|
input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
|
|
@@ -139,6 +207,60 @@ export class MutationService {
|
|
|
139
207
|
})();
|
|
140
208
|
}
|
|
141
209
|
|
|
210
|
+
createTaskBatch(input: { epicId: string; specs: readonly CompactTaskSpec[] }): CompactTaskBatchCreateResult {
|
|
211
|
+
return this.#db.transaction((): CompactTaskBatchCreateResult => {
|
|
212
|
+
const created = this.#domain.createTaskBatch(input);
|
|
213
|
+
for (const task of created.tasks) {
|
|
214
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.created, {
|
|
215
|
+
epic_id: task.epicId,
|
|
216
|
+
title: task.title,
|
|
217
|
+
description: task.description,
|
|
218
|
+
status: task.status,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return created;
|
|
222
|
+
})();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
expandEpic(input: {
|
|
226
|
+
epicId: string;
|
|
227
|
+
taskSpecs: readonly CompactTaskSpec[];
|
|
228
|
+
subtaskSpecs: readonly CompactSubtaskSpec[];
|
|
229
|
+
dependencySpecs: readonly CompactDependencySpec[];
|
|
230
|
+
}): CompactEpicExpandResult {
|
|
231
|
+
return this.#db.transaction((): CompactEpicExpandResult => {
|
|
232
|
+
const created = this.#domain.expandEpic(input);
|
|
233
|
+
for (const task of created.tasks) {
|
|
234
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.created, {
|
|
235
|
+
epic_id: task.epicId,
|
|
236
|
+
title: task.title,
|
|
237
|
+
description: task.description,
|
|
238
|
+
status: task.status,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const subtask of created.subtasks) {
|
|
243
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.created, {
|
|
244
|
+
task_id: subtask.taskId,
|
|
245
|
+
title: subtask.title,
|
|
246
|
+
description: subtask.description,
|
|
247
|
+
status: subtask.status,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const dependency of created.dependencies) {
|
|
252
|
+
this.#appendEntityEvent("dependency", dependency.id, ENTITY_OPERATIONS.dependency.added, {
|
|
253
|
+
source_id: dependency.sourceId,
|
|
254
|
+
source_kind: dependency.sourceKind,
|
|
255
|
+
depends_on_id: dependency.dependsOnId,
|
|
256
|
+
depends_on_kind: dependency.dependsOnKind,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return created;
|
|
261
|
+
})();
|
|
262
|
+
}
|
|
263
|
+
|
|
142
264
|
updateTask(
|
|
143
265
|
id: string,
|
|
144
266
|
input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
|
|
@@ -180,6 +302,21 @@ export class MutationService {
|
|
|
180
302
|
})();
|
|
181
303
|
}
|
|
182
304
|
|
|
305
|
+
createSubtaskBatch(input: { taskId: string; specs: readonly CompactSubtaskSpec[] }): CompactSubtaskBatchCreateResult {
|
|
306
|
+
return this.#db.transaction((): CompactSubtaskBatchCreateResult => {
|
|
307
|
+
const created = this.#domain.createSubtaskBatch(input);
|
|
308
|
+
for (const subtask of created.subtasks) {
|
|
309
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.created, {
|
|
310
|
+
task_id: subtask.taskId,
|
|
311
|
+
title: subtask.title,
|
|
312
|
+
description: subtask.description,
|
|
313
|
+
status: subtask.status,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return created;
|
|
317
|
+
})();
|
|
318
|
+
}
|
|
319
|
+
|
|
183
320
|
updateSubtask(
|
|
184
321
|
id: string,
|
|
185
322
|
input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
|
|
@@ -216,6 +353,21 @@ export class MutationService {
|
|
|
216
353
|
})();
|
|
217
354
|
}
|
|
218
355
|
|
|
356
|
+
addDependencyBatch(input: { specs: readonly CompactDependencySpec[] }): CompactDependencyBatchAddResult {
|
|
357
|
+
return this.#db.transaction((): CompactDependencyBatchAddResult => {
|
|
358
|
+
const created = this.#domain.addDependencyBatch(input);
|
|
359
|
+
for (const dependency of created.dependencies) {
|
|
360
|
+
this.#appendEntityEvent("dependency", dependency.id, ENTITY_OPERATIONS.dependency.added, {
|
|
361
|
+
source_id: dependency.sourceId,
|
|
362
|
+
source_kind: dependency.sourceKind,
|
|
363
|
+
depends_on_id: dependency.dependsOnId,
|
|
364
|
+
depends_on_kind: dependency.dependsOnKind,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return created;
|
|
368
|
+
})();
|
|
369
|
+
}
|
|
370
|
+
|
|
219
371
|
removeDependency(sourceId: string, dependsOnId: string): number {
|
|
220
372
|
return this.#db.transaction((): number => {
|
|
221
373
|
const removed = this.#domain.removeDependency(sourceId, dependsOnId);
|