@critical-path/core 0.33.0 → 0.34.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/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/domain/entities.js +17 -17
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/events.d.ts +53 -2
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js +7 -0
- package/dist/domain/events.js.map +1 -1
- package/dist/engine/entity-events.test.d.ts +2 -0
- package/dist/engine/entity-events.test.d.ts.map +1 -0
- package/dist/engine/entity-events.test.js +61 -0
- package/dist/engine/entity-events.test.js.map +1 -0
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +106 -39
- package/dist/engine/index.js.map +1 -1
- package/dist/schemas/index.d.ts +6 -6
- package/dist/store/index.js +13 -13
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.js +13 -13
- package/dist/store/sqlite.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/entities.ts +17 -17
- package/src/domain/events.ts +49 -0
- package/src/engine/entity-events.test.ts +67 -0
- package/src/engine/index.ts +115 -39
- package/src/store/index.ts +13 -13
- package/src/store/sqlite.ts +13 -13
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CriticalPathEngine, type DomainEvent } from '../index.js';
|
|
3
|
+
|
|
4
|
+
function record(engine: CriticalPathEngine) {
|
|
5
|
+
const events: DomainEvent[] = [];
|
|
6
|
+
engine.events.subscribe('*', (e) => void events.push(e));
|
|
7
|
+
return events;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('team, container and iteration events', () => {
|
|
11
|
+
it('publishes updated and deleted events with the previous state', async () => {
|
|
12
|
+
const engine = new CriticalPathEngine();
|
|
13
|
+
const project = await engine.createProject({ name: 'Events' });
|
|
14
|
+
const team = await engine.createTeam({ name: 'Core', memberIds: [] });
|
|
15
|
+
const container = await engine.createContainer({ projectId: project.id, name: 'Epic' });
|
|
16
|
+
const events = record(engine);
|
|
17
|
+
const iteration = await engine.createIteration({ projectId: project.id, name: 'S1', status: 'planning' });
|
|
18
|
+
|
|
19
|
+
await engine.updateTeam(team.id, { name: 'Platform' });
|
|
20
|
+
await engine.updateContainer(container.id, { name: 'Epic 2' });
|
|
21
|
+
await engine.updateIteration(iteration.id, { goal: 'Ship' });
|
|
22
|
+
await engine.deleteTeam(team.id);
|
|
23
|
+
await engine.deleteContainer(container.id);
|
|
24
|
+
await engine.deleteIteration(iteration.id);
|
|
25
|
+
|
|
26
|
+
expect(events.map((e) => e.name)).toEqual([
|
|
27
|
+
'iteration.created',
|
|
28
|
+
'team.updated',
|
|
29
|
+
'container.updated',
|
|
30
|
+
'iteration.updated',
|
|
31
|
+
'team.deleted',
|
|
32
|
+
'container.deleted',
|
|
33
|
+
'iteration.deleted'
|
|
34
|
+
]);
|
|
35
|
+
const teamUpdated = events.find((e) => e.name === 'team.updated')!.payload as any;
|
|
36
|
+
expect(teamUpdated.previous.name).toBe('Core');
|
|
37
|
+
expect(teamUpdated.team.name).toBe('Platform');
|
|
38
|
+
expect(events.find((e) => e.name === 'iteration.deleted')!.payload).toMatchObject({ projectId: project.id, name: 'S1' });
|
|
39
|
+
expect(new Set(events.map((e) => e.id)).size).toBe(events.length);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('routes deletion webhooks to the owning tenant', async () => {
|
|
43
|
+
const delivered: Array<{ url: string; event: string }> = [];
|
|
44
|
+
const engine = new CriticalPathEngine({
|
|
45
|
+
webhookDelivery: {
|
|
46
|
+
fetch: (async (url: string, init: RequestInit) => {
|
|
47
|
+
delivered.push({ url, event: JSON.parse(init.body as string).event });
|
|
48
|
+
return new Response(null, { status: 204 });
|
|
49
|
+
}) as unknown as typeof fetch
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const acme = engine.withActor({ userId: 'a', tenantId: 'acme' });
|
|
53
|
+
const globex = engine.withActor({ userId: 'g', tenantId: 'globex' });
|
|
54
|
+
await acme.createWebhook({ name: 'Acme', url: 'https://hooks.example.com/acme', events: ['team.deleted', 'container.deleted'] });
|
|
55
|
+
await globex.createWebhook({ name: 'Globex', url: 'https://hooks.example.com/globex', events: ['*'] });
|
|
56
|
+
|
|
57
|
+
const project = await acme.createProject({ name: 'Acme' });
|
|
58
|
+
const team = await acme.createTeam({ name: 'Team', memberIds: [] });
|
|
59
|
+
const container = await acme.createContainer({ projectId: project.id, name: 'C' });
|
|
60
|
+
await acme.deleteTeam(team.id);
|
|
61
|
+
await acme.deleteContainer(container.id);
|
|
62
|
+
await engine.webhooks.idle();
|
|
63
|
+
|
|
64
|
+
expect(delivered.map((d) => d.event).sort()).toEqual(['container.deleted', 'team.deleted']);
|
|
65
|
+
expect(delivered.every((d) => d.url.endsWith('/acme'))).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
package/src/engine/index.ts
CHANGED
|
@@ -63,6 +63,14 @@ import {
|
|
|
63
63
|
ProjectUpdatedEvent,
|
|
64
64
|
ProjectDeletedEvent,
|
|
65
65
|
TaskDependencyRemovedEvent,
|
|
66
|
+
TeamUpdatedEvent,
|
|
67
|
+
TeamDeletedEvent,
|
|
68
|
+
ContainerUpdatedEvent,
|
|
69
|
+
ContainerDeletedEvent,
|
|
70
|
+
IterationCreatedEvent,
|
|
71
|
+
IterationUpdatedEvent,
|
|
72
|
+
IterationDeletedEvent,
|
|
73
|
+
CriticalPathDomainEvent,
|
|
66
74
|
WorkflowCreatedEvent,
|
|
67
75
|
WorkflowUpdatedEvent,
|
|
68
76
|
WorkflowDeletedEvent,
|
|
@@ -161,6 +169,15 @@ export class CriticalPathEngine {
|
|
|
161
169
|
return scoped;
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
/** Publishes a domain event, filling in its id and timestamp. */
|
|
173
|
+
private async publishEvent<E extends CriticalPathDomainEvent>(event: Omit<E, 'id' | 'occurredAt'>): Promise<void> {
|
|
174
|
+
await this.events.publish({
|
|
175
|
+
...event,
|
|
176
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
177
|
+
occurredAt: new Date().toISOString()
|
|
178
|
+
} as E);
|
|
179
|
+
}
|
|
180
|
+
|
|
164
181
|
private actorIdOr(fallback: string): string {
|
|
165
182
|
return this.actor?.userId ?? fallback;
|
|
166
183
|
}
|
|
@@ -335,7 +352,7 @@ export class CriticalPathEngine {
|
|
|
335
352
|
const now = new Date().toISOString();
|
|
336
353
|
|
|
337
354
|
const event: WorkflowCreatedEvent = {
|
|
338
|
-
id: `evt_${
|
|
355
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
339
356
|
name: 'workflow.created',
|
|
340
357
|
aggregateId: created.id,
|
|
341
358
|
aggregateType: 'Workflow',
|
|
@@ -362,7 +379,7 @@ export class CriticalPathEngine {
|
|
|
362
379
|
if (updated) {
|
|
363
380
|
const now = new Date().toISOString();
|
|
364
381
|
const event: WorkflowUpdatedEvent = {
|
|
365
|
-
id: `evt_${
|
|
382
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
366
383
|
name: 'workflow.updated',
|
|
367
384
|
aggregateId: updated.id,
|
|
368
385
|
aggregateType: 'Workflow',
|
|
@@ -389,7 +406,7 @@ export class CriticalPathEngine {
|
|
|
389
406
|
if (deleted) {
|
|
390
407
|
const now = new Date().toISOString();
|
|
391
408
|
const event: WorkflowDeletedEvent = {
|
|
392
|
-
id: `evt_${
|
|
409
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
393
410
|
name: 'workflow.deleted',
|
|
394
411
|
aggregateId: id,
|
|
395
412
|
aggregateType: 'Workflow',
|
|
@@ -469,7 +486,7 @@ export class CriticalPathEngine {
|
|
|
469
486
|
const now = new Date().toISOString();
|
|
470
487
|
|
|
471
488
|
const event: ProjectCreatedEvent = {
|
|
472
|
-
id: `evt_${
|
|
489
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
473
490
|
name: 'project.created',
|
|
474
491
|
aggregateId: created.id,
|
|
475
492
|
aggregateType: 'Project',
|
|
@@ -499,7 +516,7 @@ export class CriticalPathEngine {
|
|
|
499
516
|
if (updated) {
|
|
500
517
|
const now = new Date().toISOString();
|
|
501
518
|
const event: ProjectUpdatedEvent = {
|
|
502
|
-
id: `evt_${
|
|
519
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
503
520
|
name: 'project.updated',
|
|
504
521
|
aggregateId: updated.id,
|
|
505
522
|
aggregateType: 'Project',
|
|
@@ -547,7 +564,7 @@ export class CriticalPathEngine {
|
|
|
547
564
|
if (deleted) {
|
|
548
565
|
const now = new Date().toISOString();
|
|
549
566
|
const event: ProjectDeletedEvent = {
|
|
550
|
-
id: `evt_${
|
|
567
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
551
568
|
name: 'project.deleted',
|
|
552
569
|
aggregateId: id,
|
|
553
570
|
aggregateType: 'Project',
|
|
@@ -688,7 +705,7 @@ export class CriticalPathEngine {
|
|
|
688
705
|
|
|
689
706
|
// Publish typed Domain Event
|
|
690
707
|
const event: TaskCreatedEvent = {
|
|
691
|
-
id: `evt_${
|
|
708
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
692
709
|
name: 'task.created',
|
|
693
710
|
aggregateId: created.id,
|
|
694
711
|
aggregateType: 'Task',
|
|
@@ -927,7 +944,7 @@ export class CriticalPathEngine {
|
|
|
927
944
|
// Publish typed Domain Events
|
|
928
945
|
if (isStatusChange) {
|
|
929
946
|
const statusEvent: TaskStatusChangedEvent = {
|
|
930
|
-
id: `evt_${
|
|
947
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
931
948
|
name: 'task.status_changed',
|
|
932
949
|
aggregateId: updated.id,
|
|
933
950
|
aggregateType: 'Task',
|
|
@@ -952,7 +969,7 @@ export class CriticalPathEngine {
|
|
|
952
969
|
}
|
|
953
970
|
} else {
|
|
954
971
|
const updateEvent: TaskUpdatedEvent = {
|
|
955
|
-
id: `evt_${
|
|
972
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
956
973
|
name: 'task.updated',
|
|
957
974
|
aggregateId: updated.id,
|
|
958
975
|
aggregateType: 'Task',
|
|
@@ -974,7 +991,7 @@ export class CriticalPathEngine {
|
|
|
974
991
|
if (!wasBlocked && isNowBlocked) {
|
|
975
992
|
const blockedReason = updated.blockedReason ?? null;
|
|
976
993
|
const blockedEvent: TaskBlockedEvent = {
|
|
977
|
-
id: `evt_${
|
|
994
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
978
995
|
name: 'task.blocked',
|
|
979
996
|
aggregateId: updated.id,
|
|
980
997
|
aggregateType: 'Task',
|
|
@@ -997,7 +1014,7 @@ export class CriticalPathEngine {
|
|
|
997
1014
|
});
|
|
998
1015
|
} else if (wasBlocked && !isNowBlocked) {
|
|
999
1016
|
const unblockedEvent: TaskUnblockedEvent = {
|
|
1000
|
-
id: `evt_${
|
|
1017
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1001
1018
|
name: 'task.unblocked',
|
|
1002
1019
|
aggregateId: updated.id,
|
|
1003
1020
|
aggregateType: 'Task',
|
|
@@ -1081,7 +1098,7 @@ export class CriticalPathEngine {
|
|
|
1081
1098
|
|
|
1082
1099
|
const now = new Date().toISOString();
|
|
1083
1100
|
const unblockedEvent: TaskUnblockedEvent = {
|
|
1084
|
-
id: `evt_${
|
|
1101
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1085
1102
|
name: 'task.unblocked',
|
|
1086
1103
|
aggregateId: downstream.id,
|
|
1087
1104
|
aggregateType: 'Task',
|
|
@@ -1127,7 +1144,7 @@ export class CriticalPathEngine {
|
|
|
1127
1144
|
const now = new Date().toISOString();
|
|
1128
1145
|
|
|
1129
1146
|
const event: TaskDeletedEvent = {
|
|
1130
|
-
id: `evt_${
|
|
1147
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1131
1148
|
name: 'task.deleted',
|
|
1132
1149
|
aggregateId: id,
|
|
1133
1150
|
aggregateType: 'Task',
|
|
@@ -1198,7 +1215,7 @@ export class CriticalPathEngine {
|
|
|
1198
1215
|
const removed = await this.store.removeDependency(id);
|
|
1199
1216
|
if (removed) {
|
|
1200
1217
|
const event: TaskDependencyRemovedEvent = {
|
|
1201
|
-
id: `evt_${
|
|
1218
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1202
1219
|
name: 'dependency.removed',
|
|
1203
1220
|
aggregateId: id,
|
|
1204
1221
|
aggregateType: 'Dependency',
|
|
@@ -1229,7 +1246,7 @@ export class CriticalPathEngine {
|
|
|
1229
1246
|
const now = new Date().toISOString();
|
|
1230
1247
|
|
|
1231
1248
|
const event: TaskDependencyAddedEvent = {
|
|
1232
|
-
id: `evt_${
|
|
1249
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1233
1250
|
name: 'dependency.added',
|
|
1234
1251
|
aggregateId: created.id,
|
|
1235
1252
|
aggregateType: 'Dependency',
|
|
@@ -1357,7 +1374,7 @@ export class CriticalPathEngine {
|
|
|
1357
1374
|
const now = new Date().toISOString();
|
|
1358
1375
|
|
|
1359
1376
|
const event: TimeLoggedEvent = {
|
|
1360
|
-
id: `evt_${
|
|
1377
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1361
1378
|
name: 'time.logged',
|
|
1362
1379
|
aggregateId: entry.taskId,
|
|
1363
1380
|
aggregateType: 'Task',
|
|
@@ -1407,7 +1424,7 @@ export class CriticalPathEngine {
|
|
|
1407
1424
|
const now = new Date().toISOString();
|
|
1408
1425
|
|
|
1409
1426
|
const event: CommentAddedEvent = {
|
|
1410
|
-
id: `evt_${
|
|
1427
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1411
1428
|
name: 'comment.created',
|
|
1412
1429
|
aggregateId: created.id,
|
|
1413
1430
|
aggregateType: 'Comment',
|
|
@@ -1435,7 +1452,7 @@ export class CriticalPathEngine {
|
|
|
1435
1452
|
|
|
1436
1453
|
const now = new Date().toISOString();
|
|
1437
1454
|
const event: CommentUpdatedEvent = {
|
|
1438
|
-
id: `evt_${
|
|
1455
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1439
1456
|
name: 'comment.updated',
|
|
1440
1457
|
aggregateId: updated.id,
|
|
1441
1458
|
aggregateType: 'Comment',
|
|
@@ -1459,7 +1476,7 @@ export class CriticalPathEngine {
|
|
|
1459
1476
|
if (deleted) {
|
|
1460
1477
|
const now = new Date().toISOString();
|
|
1461
1478
|
const event: CommentDeletedEvent = {
|
|
1462
|
-
id: `evt_${
|
|
1479
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1463
1480
|
name: 'comment.deleted',
|
|
1464
1481
|
aggregateId: id,
|
|
1465
1482
|
aggregateType: 'Comment',
|
|
@@ -1498,7 +1515,7 @@ export class CriticalPathEngine {
|
|
|
1498
1515
|
|
|
1499
1516
|
const now = new Date().toISOString();
|
|
1500
1517
|
const event: CommentReactionAddedEvent = {
|
|
1501
|
-
id: `evt_${
|
|
1518
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1502
1519
|
name: 'comment.reaction.added',
|
|
1503
1520
|
aggregateId: commentId,
|
|
1504
1521
|
aggregateType: 'Comment',
|
|
@@ -1538,7 +1555,7 @@ export class CriticalPathEngine {
|
|
|
1538
1555
|
|
|
1539
1556
|
const now = new Date().toISOString();
|
|
1540
1557
|
const event: CommentReactionRemovedEvent = {
|
|
1541
|
-
id: `evt_${
|
|
1558
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1542
1559
|
name: 'comment.reaction.removed',
|
|
1543
1560
|
aggregateId: commentId,
|
|
1544
1561
|
aggregateType: 'Comment',
|
|
@@ -1587,7 +1604,7 @@ export class CriticalPathEngine {
|
|
|
1587
1604
|
const now = new Date().toISOString();
|
|
1588
1605
|
|
|
1589
1606
|
const event: AttachmentCreatedEvent = {
|
|
1590
|
-
id: `evt_${
|
|
1607
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1591
1608
|
name: 'attachment.created',
|
|
1592
1609
|
aggregateId: created.id,
|
|
1593
1610
|
aggregateType: 'Attachment',
|
|
@@ -1619,7 +1636,7 @@ export class CriticalPathEngine {
|
|
|
1619
1636
|
if (deleted) {
|
|
1620
1637
|
const now = new Date().toISOString();
|
|
1621
1638
|
const event: AttachmentDeletedEvent = {
|
|
1622
|
-
id: `evt_${
|
|
1639
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1623
1640
|
name: 'attachment.deleted',
|
|
1624
1641
|
aggregateId: id,
|
|
1625
1642
|
aggregateType: 'Attachment',
|
|
@@ -1774,7 +1791,7 @@ export class CriticalPathEngine {
|
|
|
1774
1791
|
const now = new Date().toISOString();
|
|
1775
1792
|
|
|
1776
1793
|
const event: TeamCreatedEvent = {
|
|
1777
|
-
id: `evt_${
|
|
1794
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1778
1795
|
name: 'team.created',
|
|
1779
1796
|
aggregateId: created.id,
|
|
1780
1797
|
aggregateType: 'Team',
|
|
@@ -1787,15 +1804,35 @@ export class CriticalPathEngine {
|
|
|
1787
1804
|
}
|
|
1788
1805
|
|
|
1789
1806
|
async updateTeam(id: string, updates: Partial<Team>): Promise<Team | null> {
|
|
1790
|
-
|
|
1807
|
+
const existing = await this.getTeam(id);
|
|
1808
|
+
if (!existing) return null;
|
|
1791
1809
|
await this.requireWorkspaceAccess('workspace.manage');
|
|
1792
|
-
|
|
1810
|
+
const updated = await this.store.updateTeam(id, this.withoutTenant(updates));
|
|
1811
|
+
if (updated) {
|
|
1812
|
+
await this.publishEvent<TeamUpdatedEvent>({
|
|
1813
|
+
name: 'team.updated',
|
|
1814
|
+
aggregateId: id,
|
|
1815
|
+
aggregateType: 'Team',
|
|
1816
|
+
payload: { team: updated, previous: existing }
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
return updated;
|
|
1793
1820
|
}
|
|
1794
1821
|
|
|
1795
1822
|
async deleteTeam(id: string): Promise<boolean> {
|
|
1796
|
-
|
|
1823
|
+
const existing = await this.getTeam(id);
|
|
1824
|
+
if (!existing) return false;
|
|
1797
1825
|
await this.requireWorkspaceAccess('workspace.manage');
|
|
1798
|
-
|
|
1826
|
+
const deleted = await this.store.deleteTeam(id);
|
|
1827
|
+
if (deleted) {
|
|
1828
|
+
await this.publishEvent<TeamDeletedEvent>({
|
|
1829
|
+
name: 'team.deleted',
|
|
1830
|
+
aggregateId: id,
|
|
1831
|
+
aggregateType: 'Team',
|
|
1832
|
+
payload: { teamId: id, name: existing.name, tenantId: existing.tenantId }
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
return deleted;
|
|
1799
1836
|
}
|
|
1800
1837
|
|
|
1801
1838
|
// --- Containers ---
|
|
@@ -1816,7 +1853,7 @@ export class CriticalPathEngine {
|
|
|
1816
1853
|
const now = new Date().toISOString();
|
|
1817
1854
|
|
|
1818
1855
|
const event: ContainerCreatedEvent = {
|
|
1819
|
-
id: `evt_${
|
|
1856
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1820
1857
|
name: 'container.created',
|
|
1821
1858
|
aggregateId: created.id,
|
|
1822
1859
|
aggregateType: 'Container',
|
|
@@ -1832,7 +1869,16 @@ export class CriticalPathEngine {
|
|
|
1832
1869
|
const existing = await this.getContainer(id);
|
|
1833
1870
|
if (!existing) return null;
|
|
1834
1871
|
await this.requireProjectAccess('plan.manage', existing.projectId);
|
|
1835
|
-
|
|
1872
|
+
const updated = await this.store.updateContainer(id, updates);
|
|
1873
|
+
if (updated) {
|
|
1874
|
+
await this.publishEvent<ContainerUpdatedEvent>({
|
|
1875
|
+
name: 'container.updated',
|
|
1876
|
+
aggregateId: id,
|
|
1877
|
+
aggregateType: 'Container',
|
|
1878
|
+
payload: { container: updated, previous: existing }
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
return updated;
|
|
1836
1882
|
}
|
|
1837
1883
|
|
|
1838
1884
|
/** Deletes a container. Its tasks and nested containers are kept but no longer reference it. */
|
|
@@ -1844,7 +1890,16 @@ export class CriticalPathEngine {
|
|
|
1844
1890
|
for (const child of (await this.store.getContainers(existing.projectId)).filter((c) => c.parentId === id)) {
|
|
1845
1891
|
await this.store.updateContainer(child.id, { parentId: undefined });
|
|
1846
1892
|
}
|
|
1847
|
-
|
|
1893
|
+
const deleted = await this.store.deleteContainer(id);
|
|
1894
|
+
if (deleted) {
|
|
1895
|
+
await this.publishEvent<ContainerDeletedEvent>({
|
|
1896
|
+
name: 'container.deleted',
|
|
1897
|
+
aggregateId: id,
|
|
1898
|
+
aggregateType: 'Container',
|
|
1899
|
+
payload: { containerId: id, projectId: existing.projectId, name: existing.name }
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
return deleted;
|
|
1848
1903
|
}
|
|
1849
1904
|
|
|
1850
1905
|
/** Unsets a reference (e.g. `iterationId`) on every task in the project that points at `id`. */
|
|
@@ -1886,7 +1941,7 @@ export class CriticalPathEngine {
|
|
|
1886
1941
|
const now = new Date().toISOString();
|
|
1887
1942
|
|
|
1888
1943
|
const event: DeliverableCreatedEvent = {
|
|
1889
|
-
id: `evt_${
|
|
1944
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1890
1945
|
name: 'deliverable.created',
|
|
1891
1946
|
aggregateId: created.id,
|
|
1892
1947
|
aggregateType: 'Deliverable',
|
|
@@ -1919,7 +1974,7 @@ export class CriticalPathEngine {
|
|
|
1919
1974
|
|
|
1920
1975
|
if (updates.status && updates.status !== existing.status) {
|
|
1921
1976
|
const statusEvent: DeliverableStatusChangedEvent = {
|
|
1922
|
-
id: `evt_${
|
|
1977
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1923
1978
|
name: 'deliverable.status_changed',
|
|
1924
1979
|
aggregateId: updated.id,
|
|
1925
1980
|
aggregateType: 'Deliverable',
|
|
@@ -1934,7 +1989,7 @@ export class CriticalPathEngine {
|
|
|
1934
1989
|
}
|
|
1935
1990
|
|
|
1936
1991
|
const updateEvent: DeliverableUpdatedEvent = {
|
|
1937
|
-
id: `evt_${
|
|
1992
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1938
1993
|
name: 'deliverable.updated',
|
|
1939
1994
|
aggregateId: updated.id,
|
|
1940
1995
|
aggregateType: 'Deliverable',
|
|
@@ -1963,7 +2018,7 @@ export class CriticalPathEngine {
|
|
|
1963
2018
|
if (deleted) {
|
|
1964
2019
|
const now = new Date().toISOString();
|
|
1965
2020
|
const event: DeliverableDeletedEvent = {
|
|
1966
|
-
id: `evt_${
|
|
2021
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
1967
2022
|
name: 'deliverable.deleted',
|
|
1968
2023
|
aggregateId: id,
|
|
1969
2024
|
aggregateType: 'Deliverable',
|
|
@@ -2044,10 +2099,16 @@ export class CriticalPathEngine {
|
|
|
2044
2099
|
async createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration> {
|
|
2045
2100
|
await this.requireProjectAccess('plan.manage', iteration.projectId);
|
|
2046
2101
|
const created = await this.store.createIteration(iteration);
|
|
2102
|
+
await this.publishEvent<IterationCreatedEvent>({
|
|
2103
|
+
name: 'iteration.created',
|
|
2104
|
+
aggregateId: created.id,
|
|
2105
|
+
aggregateType: 'Iteration',
|
|
2106
|
+
payload: { iteration: created }
|
|
2107
|
+
});
|
|
2047
2108
|
if (created.status === 'active') {
|
|
2048
2109
|
const now = new Date().toISOString();
|
|
2049
2110
|
const event: IterationStartedEvent = {
|
|
2050
|
-
id: `evt_${
|
|
2111
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
2051
2112
|
name: 'iteration.started',
|
|
2052
2113
|
aggregateId: created.id,
|
|
2053
2114
|
aggregateType: 'Iteration',
|
|
@@ -2066,10 +2127,16 @@ export class CriticalPathEngine {
|
|
|
2066
2127
|
|
|
2067
2128
|
const updated = await this.store.updateIteration(id, updates);
|
|
2068
2129
|
if (updated) {
|
|
2130
|
+
await this.publishEvent<IterationUpdatedEvent>({
|
|
2131
|
+
name: 'iteration.updated',
|
|
2132
|
+
aggregateId: id,
|
|
2133
|
+
aggregateType: 'Iteration',
|
|
2134
|
+
payload: { iteration: updated, previous: existing }
|
|
2135
|
+
});
|
|
2069
2136
|
const now = new Date().toISOString();
|
|
2070
2137
|
if (existing.status !== 'active' && updated.status === 'active') {
|
|
2071
2138
|
const event: IterationStartedEvent = {
|
|
2072
|
-
id: `evt_${
|
|
2139
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
2073
2140
|
name: 'iteration.started',
|
|
2074
2141
|
aggregateId: updated.id,
|
|
2075
2142
|
aggregateType: 'Iteration',
|
|
@@ -2079,7 +2146,7 @@ export class CriticalPathEngine {
|
|
|
2079
2146
|
await this.events.publish(event);
|
|
2080
2147
|
} else if (existing.status !== 'completed' && updated.status === 'completed') {
|
|
2081
2148
|
const event: IterationCompletedEvent = {
|
|
2082
|
-
id: `evt_${
|
|
2149
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
2083
2150
|
name: 'iteration.completed',
|
|
2084
2151
|
aggregateId: updated.id,
|
|
2085
2152
|
aggregateType: 'Iteration',
|
|
@@ -2098,7 +2165,16 @@ export class CriticalPathEngine {
|
|
|
2098
2165
|
if (!existing) return false;
|
|
2099
2166
|
await this.requireProjectAccess('plan.manage', existing.projectId);
|
|
2100
2167
|
await this.clearTaskReferences(existing.projectId, 'iterationId', id);
|
|
2101
|
-
|
|
2168
|
+
const deleted = await this.store.deleteIteration(id);
|
|
2169
|
+
if (deleted) {
|
|
2170
|
+
await this.publishEvent<IterationDeletedEvent>({
|
|
2171
|
+
name: 'iteration.deleted',
|
|
2172
|
+
aggregateId: id,
|
|
2173
|
+
aggregateType: 'Iteration',
|
|
2174
|
+
payload: { iterationId: id, projectId: existing.projectId, name: existing.name }
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
return deleted;
|
|
2102
2178
|
}
|
|
2103
2179
|
|
|
2104
2180
|
// --- Ladder of Abstraction & Critical Path Method ---
|
package/src/store/index.ts
CHANGED
|
@@ -172,7 +172,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
async createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
|
|
175
|
-
const id = `proj_${
|
|
175
|
+
const id = `proj_${crypto.randomUUID()}`;
|
|
176
176
|
const now = new Date().toISOString();
|
|
177
177
|
const key = project.key || generateProjectKey(project.name);
|
|
178
178
|
const newProject: Project = {
|
|
@@ -212,7 +212,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
|
|
215
|
-
const id = `wf_${
|
|
215
|
+
const id = `wf_${crypto.randomUUID()}`;
|
|
216
216
|
const now = new Date().toISOString();
|
|
217
217
|
const newWorkflow: Workflow = {
|
|
218
218
|
...workflow,
|
|
@@ -258,7 +258,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
async createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
|
|
261
|
-
const id = `task_${
|
|
261
|
+
const id = `task_${crypto.randomUUID()}`;
|
|
262
262
|
const now = new Date().toISOString();
|
|
263
263
|
const newTask: Task = {
|
|
264
264
|
...task,
|
|
@@ -296,7 +296,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
async createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team> {
|
|
299
|
-
const id = `team_${
|
|
299
|
+
const id = `team_${crypto.randomUUID()}`;
|
|
300
300
|
const now = new Date().toISOString();
|
|
301
301
|
const newTeam: Team = { ...team, id, createdAt: now, updatedAt: now };
|
|
302
302
|
this.teams.set(id, newTeam);
|
|
@@ -325,7 +325,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
async createContainer(container: Omit<TaskContainer, 'id' | 'createdAt' | 'updatedAt'>): Promise<TaskContainer> {
|
|
328
|
-
const id = `cnt_${
|
|
328
|
+
const id = `cnt_${crypto.randomUUID()}`;
|
|
329
329
|
const now = new Date().toISOString();
|
|
330
330
|
const newContainer: TaskContainer = { ...container, id, createdAt: now, updatedAt: now };
|
|
331
331
|
this.containers.set(id, newContainer);
|
|
@@ -354,7 +354,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
async createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable> {
|
|
357
|
-
const id = (deliverable as any).id || `deliv_${
|
|
357
|
+
const id = (deliverable as any).id || `deliv_${crypto.randomUUID()}`;
|
|
358
358
|
const now = new Date().toISOString();
|
|
359
359
|
const newDeliverable: Deliverable = {
|
|
360
360
|
...deliverable,
|
|
@@ -395,7 +395,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
async createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration> {
|
|
398
|
-
const id = `iter_${
|
|
398
|
+
const id = `iter_${crypto.randomUUID()}`;
|
|
399
399
|
const newIteration: Iteration = {
|
|
400
400
|
...iteration,
|
|
401
401
|
id,
|
|
@@ -427,7 +427,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
|
|
430
|
-
const id = `cmt_${
|
|
430
|
+
const id = `cmt_${crypto.randomUUID()}`;
|
|
431
431
|
const now = new Date().toISOString();
|
|
432
432
|
const newComment: Comment = { ...comment, id, createdAt: now, updatedAt: now };
|
|
433
433
|
this.comments.set(id, newComment);
|
|
@@ -501,7 +501,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
501
501
|
}
|
|
502
502
|
|
|
503
503
|
async createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment> {
|
|
504
|
-
const id = `att_${
|
|
504
|
+
const id = `att_${crypto.randomUUID()}`;
|
|
505
505
|
const now = new Date().toISOString();
|
|
506
506
|
const newAttachment: Attachment = {
|
|
507
507
|
...attachment,
|
|
@@ -532,7 +532,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
async logActivity(activity: Omit<Activity, 'id' | 'createdAt'>): Promise<Activity> {
|
|
535
|
-
const id = `act_${
|
|
535
|
+
const id = `act_${crypto.randomUUID()}`;
|
|
536
536
|
const newAct: Activity = { ...activity, id, createdAt: new Date().toISOString() };
|
|
537
537
|
this.activities.unshift(newAct);
|
|
538
538
|
return newAct;
|
|
@@ -544,7 +544,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
544
544
|
}
|
|
545
545
|
|
|
546
546
|
async logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt'> & { loggedAt?: string }): Promise<TimeEntry> {
|
|
547
|
-
const id = `time_${
|
|
547
|
+
const id = `time_${crypto.randomUUID()}`;
|
|
548
548
|
const newEntry: TimeEntry = { ...entry, id, loggedAt: entry.loggedAt || new Date().toISOString() };
|
|
549
549
|
this.timeEntries.set(id, newEntry);
|
|
550
550
|
return newEntry;
|
|
@@ -566,7 +566,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
566
566
|
}
|
|
567
567
|
|
|
568
568
|
async addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency> {
|
|
569
|
-
const id = `dep_${
|
|
569
|
+
const id = `dep_${crypto.randomUUID()}`;
|
|
570
570
|
const newDep: TaskDependency = { ...dep, id };
|
|
571
571
|
this.dependencies.set(id, newDep);
|
|
572
572
|
return newDep;
|
|
@@ -586,7 +586,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
586
586
|
}
|
|
587
587
|
|
|
588
588
|
async addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook> {
|
|
589
|
-
const id = `wh_${
|
|
589
|
+
const id = `wh_${crypto.randomUUID()}`;
|
|
590
590
|
const newWh: Webhook = { ...webhook, id, createdAt: new Date().toISOString() };
|
|
591
591
|
this.webhooks.set(id, newWh);
|
|
592
592
|
return newWh;
|