@critical-path/core 0.32.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 +19 -0
- package/README.md +12 -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/conformance.test.d.ts +2 -0
- package/dist/store/conformance.test.d.ts.map +1 -0
- package/dist/store/conformance.test.js +15 -0
- package/dist/store/conformance.test.js.map +1 -0
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +14 -3
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/index.js +13 -13
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +6 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +59 -18
- package/dist/store/sqlite.js.map +1 -1
- package/dist/testing/conformance.d.ts +36 -0
- package/dist/testing/conformance.d.ts.map +1 -0
- package/dist/testing/conformance.js +244 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +5 -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/conformance.test.ts +15 -0
- package/src/store/firebase.ts +17 -3
- package/src/store/index.ts +13 -13
- package/src/store/sqlite.ts +67 -18
- package/src/testing/conformance.ts +301 -0
- package/src/testing/index.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/domain/entities.ts
CHANGED
|
@@ -146,7 +146,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
146
146
|
input: CreateTaskInput,
|
|
147
147
|
options?: { id?: string; workflow?: Workflow; customFieldDefs?: CustomFieldDefinition[] }
|
|
148
148
|
): TaskEntity {
|
|
149
|
-
const id = options?.id || `task_${
|
|
149
|
+
const id = options?.id || `task_${crypto.randomUUID()}`;
|
|
150
150
|
const now = new Date().toISOString();
|
|
151
151
|
|
|
152
152
|
if (options?.customFieldDefs) {
|
|
@@ -186,7 +186,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
const createdEvent: TaskCreatedEvent = {
|
|
189
|
-
id: `evt_${
|
|
189
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
190
190
|
name: 'task.created',
|
|
191
191
|
aggregateId: task.id,
|
|
192
192
|
aggregateType: 'Task',
|
|
@@ -277,7 +277,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
277
277
|
this.markUpdated();
|
|
278
278
|
|
|
279
279
|
const event: TaskStatusChangedEvent = {
|
|
280
|
-
id: `evt_${
|
|
280
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
281
281
|
name: 'task.status_changed',
|
|
282
282
|
aggregateId: this.id,
|
|
283
283
|
aggregateType: 'Task',
|
|
@@ -310,7 +310,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
310
310
|
this.markUpdated();
|
|
311
311
|
|
|
312
312
|
const timeEntry: TimeEntry = {
|
|
313
|
-
id: `time_${
|
|
313
|
+
id: `time_${crypto.randomUUID()}`,
|
|
314
314
|
taskId: this.id,
|
|
315
315
|
userId: entry.userId || this.assigneeId || 'system',
|
|
316
316
|
hours: entry.hours,
|
|
@@ -320,7 +320,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
320
320
|
};
|
|
321
321
|
|
|
322
322
|
const event: TimeLoggedEvent = {
|
|
323
|
-
id: `evt_${
|
|
323
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
324
324
|
name: 'time.logged',
|
|
325
325
|
aggregateId: this.id,
|
|
326
326
|
aggregateType: 'Task',
|
|
@@ -338,7 +338,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
338
338
|
public addTodo(title: string, id?: string): TaskTodoItem {
|
|
339
339
|
const previous = this.toPlain();
|
|
340
340
|
const item: TaskTodoItem = {
|
|
341
|
-
id: id || `todo_${
|
|
341
|
+
id: id || `todo_${crypto.randomUUID()}`,
|
|
342
342
|
title,
|
|
343
343
|
completed: false,
|
|
344
344
|
createdAt: new Date().toISOString()
|
|
@@ -350,7 +350,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
350
350
|
this.markUpdated();
|
|
351
351
|
|
|
352
352
|
const event: TaskUpdatedEvent = {
|
|
353
|
-
id: `evt_${
|
|
353
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
354
354
|
name: 'task.updated',
|
|
355
355
|
aggregateId: this.id,
|
|
356
356
|
aggregateType: 'Task',
|
|
@@ -375,7 +375,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
375
375
|
this.markUpdated();
|
|
376
376
|
|
|
377
377
|
const event: TaskUpdatedEvent = {
|
|
378
|
-
id: `evt_${
|
|
378
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
379
379
|
name: 'task.updated',
|
|
380
380
|
aggregateId: this.id,
|
|
381
381
|
aggregateType: 'Task',
|
|
@@ -397,7 +397,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
397
397
|
this.markUpdated();
|
|
398
398
|
|
|
399
399
|
const event: TaskUpdatedEvent = {
|
|
400
|
-
id: `evt_${
|
|
400
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
401
401
|
name: 'task.updated',
|
|
402
402
|
aggregateId: this.id,
|
|
403
403
|
aggregateType: 'Task',
|
|
@@ -459,7 +459,7 @@ export class TaskEntity extends BaseEntity {
|
|
|
459
459
|
this.markUpdated();
|
|
460
460
|
|
|
461
461
|
const event: TaskUpdatedEvent = {
|
|
462
|
-
id: `evt_${
|
|
462
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
463
463
|
name: 'task.updated',
|
|
464
464
|
aggregateId: this.id,
|
|
465
465
|
aggregateType: 'Task',
|
|
@@ -554,7 +554,7 @@ export class ProjectEntity extends BaseEntity {
|
|
|
554
554
|
input: CreateProjectInput,
|
|
555
555
|
options?: { id?: string }
|
|
556
556
|
): ProjectEntity {
|
|
557
|
-
const id = options?.id || `proj_${
|
|
557
|
+
const id = options?.id || `proj_${crypto.randomUUID()}`;
|
|
558
558
|
const now = new Date().toISOString();
|
|
559
559
|
const key = input.key || generateProjectKey(input.name);
|
|
560
560
|
|
|
@@ -567,7 +567,7 @@ export class ProjectEntity extends BaseEntity {
|
|
|
567
567
|
});
|
|
568
568
|
|
|
569
569
|
const event: ProjectCreatedEvent = {
|
|
570
|
-
id: `evt_${
|
|
570
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
571
571
|
name: 'project.created',
|
|
572
572
|
aggregateId: project.id,
|
|
573
573
|
aggregateType: 'Project',
|
|
@@ -638,7 +638,7 @@ export class DeliverableEntity extends BaseEntity {
|
|
|
638
638
|
input: CreateDeliverableInput,
|
|
639
639
|
options?: { id?: string; customFieldDefs?: CustomFieldDefinition[] }
|
|
640
640
|
): DeliverableEntity {
|
|
641
|
-
const id = options?.id || `deliv_${
|
|
641
|
+
const id = options?.id || `deliv_${crypto.randomUUID()}`;
|
|
642
642
|
const now = new Date().toISOString();
|
|
643
643
|
|
|
644
644
|
if (options?.customFieldDefs) {
|
|
@@ -656,7 +656,7 @@ export class DeliverableEntity extends BaseEntity {
|
|
|
656
656
|
});
|
|
657
657
|
|
|
658
658
|
const createdEvent: DeliverableCreatedEvent = {
|
|
659
|
-
id: `evt_${
|
|
659
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
660
660
|
name: 'deliverable.created',
|
|
661
661
|
aggregateId: deliverable.id,
|
|
662
662
|
aggregateType: 'Deliverable',
|
|
@@ -682,7 +682,7 @@ export class DeliverableEntity extends BaseEntity {
|
|
|
682
682
|
this.markUpdated();
|
|
683
683
|
|
|
684
684
|
const event: DeliverableStatusChangedEvent = {
|
|
685
|
-
id: `evt_${
|
|
685
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
686
686
|
name: 'deliverable.status_changed',
|
|
687
687
|
aggregateId: this.id,
|
|
688
688
|
aggregateType: 'Deliverable',
|
|
@@ -708,7 +708,7 @@ export class DeliverableEntity extends BaseEntity {
|
|
|
708
708
|
this.markUpdated();
|
|
709
709
|
|
|
710
710
|
const event: DeliverableUpdatedEvent = {
|
|
711
|
-
id: `evt_${
|
|
711
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
712
712
|
name: 'deliverable.updated',
|
|
713
713
|
aggregateId: this.id,
|
|
714
714
|
aggregateType: 'Deliverable',
|
|
@@ -742,7 +742,7 @@ export class DeliverableEntity extends BaseEntity {
|
|
|
742
742
|
this.markUpdated();
|
|
743
743
|
|
|
744
744
|
const event: DeliverableUpdatedEvent = {
|
|
745
|
-
id: `evt_${
|
|
745
|
+
id: `evt_${crypto.randomUUID()}`,
|
|
746
746
|
name: 'deliverable.updated',
|
|
747
747
|
aggregateId: this.id,
|
|
748
748
|
aggregateType: 'Deliverable',
|
package/src/domain/events.ts
CHANGED
|
@@ -174,6 +174,41 @@ export interface TeamCreatedEvent extends DomainEvent<{ team: Team }> {
|
|
|
174
174
|
readonly aggregateType: 'Team';
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
export interface TeamUpdatedEvent extends DomainEvent<{ team: Team; previous: Team }> {
|
|
178
|
+
readonly name: 'team.updated';
|
|
179
|
+
readonly aggregateType: 'Team';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface TeamDeletedEvent extends DomainEvent<{ teamId: string; name: string; tenantId?: string }> {
|
|
183
|
+
readonly name: 'team.deleted';
|
|
184
|
+
readonly aggregateType: 'Team';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface ContainerUpdatedEvent extends DomainEvent<{ container: TaskContainer; previous: TaskContainer }> {
|
|
188
|
+
readonly name: 'container.updated';
|
|
189
|
+
readonly aggregateType: 'Container';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface ContainerDeletedEvent extends DomainEvent<{ containerId: string; projectId: string; name: string }> {
|
|
193
|
+
readonly name: 'container.deleted';
|
|
194
|
+
readonly aggregateType: 'Container';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface IterationCreatedEvent extends DomainEvent<{ iteration: Iteration }> {
|
|
198
|
+
readonly name: 'iteration.created';
|
|
199
|
+
readonly aggregateType: 'Iteration';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface IterationUpdatedEvent extends DomainEvent<{ iteration: Iteration; previous: Iteration }> {
|
|
203
|
+
readonly name: 'iteration.updated';
|
|
204
|
+
readonly aggregateType: 'Iteration';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface IterationDeletedEvent extends DomainEvent<{ iterationId: string; projectId: string; name: string }> {
|
|
208
|
+
readonly name: 'iteration.deleted';
|
|
209
|
+
readonly aggregateType: 'Iteration';
|
|
210
|
+
}
|
|
211
|
+
|
|
177
212
|
export interface ContainerCreatedEvent extends DomainEvent<{ container: TaskContainer }> {
|
|
178
213
|
readonly name: 'container.created';
|
|
179
214
|
readonly aggregateType: 'Container';
|
|
@@ -225,7 +260,14 @@ export type CriticalPathDomainEvent =
|
|
|
225
260
|
| IterationStartedEvent
|
|
226
261
|
| IterationCompletedEvent
|
|
227
262
|
| TeamCreatedEvent
|
|
263
|
+
| TeamUpdatedEvent
|
|
264
|
+
| TeamDeletedEvent
|
|
228
265
|
| ContainerCreatedEvent
|
|
266
|
+
| ContainerUpdatedEvent
|
|
267
|
+
| ContainerDeletedEvent
|
|
268
|
+
| IterationCreatedEvent
|
|
269
|
+
| IterationUpdatedEvent
|
|
270
|
+
| IterationDeletedEvent
|
|
229
271
|
| DeliverableCreatedEvent
|
|
230
272
|
| DeliverableUpdatedEvent
|
|
231
273
|
| DeliverableStatusChangedEvent
|
|
@@ -289,6 +331,8 @@ export const DOMAIN_EVENT_NAMES = [
|
|
|
289
331
|
'comment.reaction.removed',
|
|
290
332
|
'comment.updated',
|
|
291
333
|
'container.created',
|
|
334
|
+
'container.deleted',
|
|
335
|
+
'container.updated',
|
|
292
336
|
'deliverable.created',
|
|
293
337
|
'deliverable.deleted',
|
|
294
338
|
'deliverable.status_changed',
|
|
@@ -296,7 +340,10 @@ export const DOMAIN_EVENT_NAMES = [
|
|
|
296
340
|
'dependency.added',
|
|
297
341
|
'dependency.removed',
|
|
298
342
|
'iteration.completed',
|
|
343
|
+
'iteration.created',
|
|
344
|
+
'iteration.deleted',
|
|
299
345
|
'iteration.started',
|
|
346
|
+
'iteration.updated',
|
|
300
347
|
'project.created',
|
|
301
348
|
'project.deleted',
|
|
302
349
|
'project.updated',
|
|
@@ -307,6 +354,8 @@ export const DOMAIN_EVENT_NAMES = [
|
|
|
307
354
|
'task.unblocked',
|
|
308
355
|
'task.updated',
|
|
309
356
|
'team.created',
|
|
357
|
+
'team.deleted',
|
|
358
|
+
'team.updated',
|
|
310
359
|
'time.logged',
|
|
311
360
|
'workflow.created',
|
|
312
361
|
'workflow.deleted',
|
|
@@ -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
|
+
});
|