@critical-path/core 0.33.0 → 0.35.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 +18 -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/firebase.js +5 -5
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +30 -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/dist/testing/conformance.d.ts.map +1 -1
- package/dist/testing/conformance.js +15 -0
- package/dist/testing/conformance.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/firebase.ts +5 -5
- package/src/store/index.ts +30 -13
- package/src/store/sqlite.ts +13 -13
- package/src/testing/conformance.ts +18 -0
- package/tsconfig.tsbuildinfo +1 -1
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/firebase.ts
CHANGED
|
@@ -56,14 +56,14 @@ export class InMemoryFirestoreMock implements FirestoreDBInterface {
|
|
|
56
56
|
return {
|
|
57
57
|
exists: !!data,
|
|
58
58
|
id: docId,
|
|
59
|
-
data: () => data
|
|
59
|
+
data: () => structuredClone(data)
|
|
60
60
|
};
|
|
61
61
|
},
|
|
62
62
|
async set(data: any, options?: { merge?: boolean }) {
|
|
63
63
|
if (options?.merge && colMap.has(docId)) {
|
|
64
|
-
colMap.set(docId, { ...colMap.get(docId), ...data });
|
|
64
|
+
colMap.set(docId, structuredClone({ ...colMap.get(docId), ...data }));
|
|
65
65
|
} else {
|
|
66
|
-
colMap.set(docId, data);
|
|
66
|
+
colMap.set(docId, structuredClone(data));
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
async delete() {
|
|
@@ -74,7 +74,7 @@ export class InMemoryFirestoreMock implements FirestoreDBInterface {
|
|
|
74
74
|
async get() {
|
|
75
75
|
const docs = Array.from(colMap.entries()).map(([id, data]) => ({
|
|
76
76
|
id,
|
|
77
|
-
data: () => data
|
|
77
|
+
data: () => structuredClone(data)
|
|
78
78
|
}));
|
|
79
79
|
return { docs };
|
|
80
80
|
},
|
|
@@ -88,7 +88,7 @@ export class InMemoryFirestoreMock implements FirestoreDBInterface {
|
|
|
88
88
|
})
|
|
89
89
|
.map(([id, data]) => ({
|
|
90
90
|
id,
|
|
91
|
-
data: () => data
|
|
91
|
+
data: () => structuredClone(data)
|
|
92
92
|
}));
|
|
93
93
|
return { docs };
|
|
94
94
|
}
|
package/src/store/index.ts
CHANGED
|
@@ -148,6 +148,10 @@ export interface StorageAdapter
|
|
|
148
148
|
DependencyRepository,
|
|
149
149
|
WebhookRepository {}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Map-backed store for development and tests. Reads and writes are deep-copied, so it behaves
|
|
153
|
+
* like a database: returned records can be mutated freely without affecting stored state.
|
|
154
|
+
*/
|
|
151
155
|
export class InMemoryStore implements StorageAdapter {
|
|
152
156
|
private projects = new Map<string, Project>();
|
|
153
157
|
private workflows = new Map<string, Workflow>();
|
|
@@ -163,6 +167,19 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
163
167
|
private dependencies = new Map<string, TaskDependency>();
|
|
164
168
|
private webhooks = new Map<string, Webhook>();
|
|
165
169
|
|
|
170
|
+
constructor() {
|
|
171
|
+
// Hand out copies: callers mutating a returned record (or an input they keep using) must not
|
|
172
|
+
// change what is stored, matching the database-backed adapters. Internal calls between
|
|
173
|
+
// methods use the unwrapped instance, so each call clones only at the boundary.
|
|
174
|
+
return new Proxy(this, {
|
|
175
|
+
get(target, property, receiver) {
|
|
176
|
+
const value = Reflect.get(target, property, receiver);
|
|
177
|
+
if (typeof value !== 'function' || property === 'constructor') return value;
|
|
178
|
+
return async (...args: unknown[]) => structuredClone(await value.apply(target, structuredClone(args)));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
166
183
|
async getProjects(): Promise<Project[]> {
|
|
167
184
|
return Array.from(this.projects.values());
|
|
168
185
|
}
|
|
@@ -172,7 +189,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
172
189
|
}
|
|
173
190
|
|
|
174
191
|
async createProject(project: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
|
|
175
|
-
const id = `proj_${
|
|
192
|
+
const id = `proj_${crypto.randomUUID()}`;
|
|
176
193
|
const now = new Date().toISOString();
|
|
177
194
|
const key = project.key || generateProjectKey(project.name);
|
|
178
195
|
const newProject: Project = {
|
|
@@ -212,7 +229,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
212
229
|
}
|
|
213
230
|
|
|
214
231
|
async createWorkflow(workflow: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'>): Promise<Workflow> {
|
|
215
|
-
const id = `wf_${
|
|
232
|
+
const id = `wf_${crypto.randomUUID()}`;
|
|
216
233
|
const now = new Date().toISOString();
|
|
217
234
|
const newWorkflow: Workflow = {
|
|
218
235
|
...workflow,
|
|
@@ -258,7 +275,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
258
275
|
}
|
|
259
276
|
|
|
260
277
|
async createTask(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
|
|
261
|
-
const id = `task_${
|
|
278
|
+
const id = `task_${crypto.randomUUID()}`;
|
|
262
279
|
const now = new Date().toISOString();
|
|
263
280
|
const newTask: Task = {
|
|
264
281
|
...task,
|
|
@@ -296,7 +313,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
296
313
|
}
|
|
297
314
|
|
|
298
315
|
async createTeam(team: Omit<Team, 'id' | 'createdAt' | 'updatedAt'>): Promise<Team> {
|
|
299
|
-
const id = `team_${
|
|
316
|
+
const id = `team_${crypto.randomUUID()}`;
|
|
300
317
|
const now = new Date().toISOString();
|
|
301
318
|
const newTeam: Team = { ...team, id, createdAt: now, updatedAt: now };
|
|
302
319
|
this.teams.set(id, newTeam);
|
|
@@ -325,7 +342,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
325
342
|
}
|
|
326
343
|
|
|
327
344
|
async createContainer(container: Omit<TaskContainer, 'id' | 'createdAt' | 'updatedAt'>): Promise<TaskContainer> {
|
|
328
|
-
const id = `cnt_${
|
|
345
|
+
const id = `cnt_${crypto.randomUUID()}`;
|
|
329
346
|
const now = new Date().toISOString();
|
|
330
347
|
const newContainer: TaskContainer = { ...container, id, createdAt: now, updatedAt: now };
|
|
331
348
|
this.containers.set(id, newContainer);
|
|
@@ -354,7 +371,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
354
371
|
}
|
|
355
372
|
|
|
356
373
|
async createDeliverable(deliverable: CreateDeliverableInput): Promise<Deliverable> {
|
|
357
|
-
const id = (deliverable as any).id || `deliv_${
|
|
374
|
+
const id = (deliverable as any).id || `deliv_${crypto.randomUUID()}`;
|
|
358
375
|
const now = new Date().toISOString();
|
|
359
376
|
const newDeliverable: Deliverable = {
|
|
360
377
|
...deliverable,
|
|
@@ -395,7 +412,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
395
412
|
}
|
|
396
413
|
|
|
397
414
|
async createIteration(iteration: Omit<Iteration, 'id' | 'createdAt'>): Promise<Iteration> {
|
|
398
|
-
const id = `iter_${
|
|
415
|
+
const id = `iter_${crypto.randomUUID()}`;
|
|
399
416
|
const newIteration: Iteration = {
|
|
400
417
|
...iteration,
|
|
401
418
|
id,
|
|
@@ -427,7 +444,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
427
444
|
}
|
|
428
445
|
|
|
429
446
|
async addComment(comment: Omit<Comment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Comment> {
|
|
430
|
-
const id = `cmt_${
|
|
447
|
+
const id = `cmt_${crypto.randomUUID()}`;
|
|
431
448
|
const now = new Date().toISOString();
|
|
432
449
|
const newComment: Comment = { ...comment, id, createdAt: now, updatedAt: now };
|
|
433
450
|
this.comments.set(id, newComment);
|
|
@@ -501,7 +518,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
501
518
|
}
|
|
502
519
|
|
|
503
520
|
async createAttachment(attachment: Omit<Attachment, 'id' | 'createdAt' | 'updatedAt'>): Promise<Attachment> {
|
|
504
|
-
const id = `att_${
|
|
521
|
+
const id = `att_${crypto.randomUUID()}`;
|
|
505
522
|
const now = new Date().toISOString();
|
|
506
523
|
const newAttachment: Attachment = {
|
|
507
524
|
...attachment,
|
|
@@ -532,7 +549,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
532
549
|
}
|
|
533
550
|
|
|
534
551
|
async logActivity(activity: Omit<Activity, 'id' | 'createdAt'>): Promise<Activity> {
|
|
535
|
-
const id = `act_${
|
|
552
|
+
const id = `act_${crypto.randomUUID()}`;
|
|
536
553
|
const newAct: Activity = { ...activity, id, createdAt: new Date().toISOString() };
|
|
537
554
|
this.activities.unshift(newAct);
|
|
538
555
|
return newAct;
|
|
@@ -544,7 +561,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
544
561
|
}
|
|
545
562
|
|
|
546
563
|
async logTime(entry: Omit<TimeEntry, 'id' | 'loggedAt'> & { loggedAt?: string }): Promise<TimeEntry> {
|
|
547
|
-
const id = `time_${
|
|
564
|
+
const id = `time_${crypto.randomUUID()}`;
|
|
548
565
|
const newEntry: TimeEntry = { ...entry, id, loggedAt: entry.loggedAt || new Date().toISOString() };
|
|
549
566
|
this.timeEntries.set(id, newEntry);
|
|
550
567
|
return newEntry;
|
|
@@ -566,7 +583,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
566
583
|
}
|
|
567
584
|
|
|
568
585
|
async addDependency(dep: Omit<TaskDependency, 'id'>): Promise<TaskDependency> {
|
|
569
|
-
const id = `dep_${
|
|
586
|
+
const id = `dep_${crypto.randomUUID()}`;
|
|
570
587
|
const newDep: TaskDependency = { ...dep, id };
|
|
571
588
|
this.dependencies.set(id, newDep);
|
|
572
589
|
return newDep;
|
|
@@ -586,7 +603,7 @@ export class InMemoryStore implements StorageAdapter {
|
|
|
586
603
|
}
|
|
587
604
|
|
|
588
605
|
async addWebhook(webhook: Omit<Webhook, 'id' | 'createdAt'>): Promise<Webhook> {
|
|
589
|
-
const id = `wh_${
|
|
606
|
+
const id = `wh_${crypto.randomUUID()}`;
|
|
590
607
|
const newWh: Webhook = { ...webhook, id, createdAt: new Date().toISOString() };
|
|
591
608
|
this.webhooks.set(id, newWh);
|
|
592
609
|
return newWh;
|