@hg-ts/domain 0.5.16 → 0.5.18
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/package.json +9 -9
- package/src/base.aggregate.ts +91 -0
- package/src/base.entity.ts +20 -0
- package/src/created-event.not-found.exception.ts +7 -0
- package/src/events/aggregate-created.event.ts +3 -0
- package/src/events/domain.event.ts +61 -0
- package/src/events/index.ts +2 -0
- package/src/index.ts +6 -0
- package/src/tests/aggregate.test.ts +103 -0
- package/src/tests/example-created.event.ts +11 -0
- package/src/tests/example-updated.event.ts +11 -0
- package/src/tests/example.aggregate.ts +43 -0
- package/src/types.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hg-ts/domain",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"test:dev": "vitest watch"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@hg-ts-config/typescript": "0.5.
|
|
21
|
-
"@hg-ts/events": "0.5.
|
|
22
|
-
"@hg-ts/exception": "0.5.
|
|
23
|
-
"@hg-ts/linter": "0.5.
|
|
24
|
-
"@hg-ts/tests": "0.5.
|
|
25
|
-
"@hg-ts/types": "0.5.
|
|
20
|
+
"@hg-ts-config/typescript": "0.5.18",
|
|
21
|
+
"@hg-ts/events": "0.5.18",
|
|
22
|
+
"@hg-ts/exception": "0.5.18",
|
|
23
|
+
"@hg-ts/linter": "0.5.18",
|
|
24
|
+
"@hg-ts/tests": "0.5.18",
|
|
25
|
+
"@hg-ts/types": "0.5.18",
|
|
26
26
|
"@types/node": "22.19.1",
|
|
27
27
|
"@types/uuid": "10.0.0",
|
|
28
28
|
"@vitest/coverage-v8": "4.0.14",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"vitest": "4.0.14"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@hg-ts/events": "0.5.
|
|
39
|
-
"@hg-ts/exception": "0.5.
|
|
38
|
+
"@hg-ts/events": "0.5.18",
|
|
39
|
+
"@hg-ts/exception": "0.5.18",
|
|
40
40
|
"reflect-metadata": "*",
|
|
41
41
|
"tslib": "*",
|
|
42
42
|
"vitest": "*"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EventHandler,
|
|
3
|
+
getEventsToHandle,
|
|
4
|
+
getListenMethods,
|
|
5
|
+
} from '@hg-ts/events';
|
|
6
|
+
|
|
7
|
+
import { BaseEntity } from './base.entity.js';
|
|
8
|
+
import { CreatedEventNotFoundException } from './created-event.not-found.exception.js';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
AggregateCreatedEvent,
|
|
12
|
+
DomainEvent,
|
|
13
|
+
STORED_PROPERTY,
|
|
14
|
+
} from './events/index.js';
|
|
15
|
+
import type { Identifiable } from './types.js';
|
|
16
|
+
|
|
17
|
+
const AGGREGATE_SYMBOL = Symbol('AGGREGATE_SYMBOL');
|
|
18
|
+
export const AGGREGATE_EVENTS_SYMBOL = Symbol('AGGREGATE_EVENTS_SYMBOL');
|
|
19
|
+
|
|
20
|
+
export abstract class BaseAggregate<Dto extends Identifiable> extends BaseEntity<Dto> {
|
|
21
|
+
protected readonly [AGGREGATE_SYMBOL]: string;
|
|
22
|
+
protected readonly [AGGREGATE_EVENTS_SYMBOL]: DomainEvent<any>[] = [];
|
|
23
|
+
|
|
24
|
+
protected constructor(params: Identifiable) {
|
|
25
|
+
super(params);
|
|
26
|
+
|
|
27
|
+
Object.defineProperty(this, AGGREGATE_SYMBOL, {
|
|
28
|
+
value: params.id,
|
|
29
|
+
configurable: false,
|
|
30
|
+
enumerable: false,
|
|
31
|
+
writable: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public getEvents(): DomainEvent<any>[] {
|
|
36
|
+
return this[AGGREGATE_EVENTS_SYMBOL];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public getNewEvets(): DomainEvent<any>[] {
|
|
40
|
+
return this[AGGREGATE_EVENTS_SYMBOL].filter(item => !item[STORED_PROPERTY]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public static restoreFromEvents<
|
|
44
|
+
Dto extends Identifiable,
|
|
45
|
+
Aggregate extends BaseAggregate<Dto>,
|
|
46
|
+
>(
|
|
47
|
+
aggregateCtor: Class<Aggregate, [Dto]>,
|
|
48
|
+
createdEventCtor: Class<AggregateCreatedEvent<Omit<Dto, keyof Identifiable>>, any[]>,
|
|
49
|
+
events: DomainEvent<any>[],
|
|
50
|
+
): Aggregate {
|
|
51
|
+
const createdEvent = events.find(event => event instanceof createdEventCtor);
|
|
52
|
+
if (!createdEvent) {
|
|
53
|
+
throw new CreatedEventNotFoundException(aggregateCtor.name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const aggregate = new aggregateCtor({
|
|
57
|
+
id: createdEvent.entityId,
|
|
58
|
+
...createdEvent.body,
|
|
59
|
+
} as Dto);
|
|
60
|
+
|
|
61
|
+
aggregate.applyEvents(events);
|
|
62
|
+
|
|
63
|
+
return aggregate;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected applyEvents(events: DomainEvent<any>[]): void {
|
|
67
|
+
events.forEach(event => this.produceEvent(event));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected produceEvent(event: DomainEvent<any>): void {
|
|
71
|
+
this.triggerListeners(event);
|
|
72
|
+
|
|
73
|
+
this[AGGREGATE_EVENTS_SYMBOL].push(event);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private triggerListeners(event: DomainEvent<any>): void {
|
|
77
|
+
const proto = Object.getPrototypeOf(this)!;
|
|
78
|
+
const listenMethods = getListenMethods(proto);
|
|
79
|
+
|
|
80
|
+
listenMethods.forEach(methodName => {
|
|
81
|
+
const method = (this as any)[methodName].bind(this) as EventHandler;
|
|
82
|
+
const eventsToHandle = getEventsToHandle(proto, methodName);
|
|
83
|
+
|
|
84
|
+
eventsToHandle.forEach(eventCtor => {
|
|
85
|
+
if (event instanceof eventCtor) {
|
|
86
|
+
method(event);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DtoSerializable,
|
|
3
|
+
Identifiable,
|
|
4
|
+
} from './types.js';
|
|
5
|
+
|
|
6
|
+
export abstract class BaseEntity<Dto extends Identifiable>
|
|
7
|
+
implements Identifiable, DtoSerializable<Dto> {
|
|
8
|
+
public readonly id: string;
|
|
9
|
+
|
|
10
|
+
protected constructor(params: Identifiable) {
|
|
11
|
+
this.id = params.id;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public equals<T extends BaseEntity<Identifiable>>(entity: T): boolean {
|
|
15
|
+
return Object.getPrototypeOf(this)!.constructor === Object.getPrototypeOf(entity)!.constructor
|
|
16
|
+
&& this.id === entity.id;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public abstract toDto(): Dto;
|
|
20
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseEvent,
|
|
3
|
+
BaseEventDto,
|
|
4
|
+
EventParams,
|
|
5
|
+
} from '@hg-ts/events';
|
|
6
|
+
import { v7 as uuid } from 'uuid';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
DtoSerializable,
|
|
10
|
+
Identifiable,
|
|
11
|
+
} from '../types.js';
|
|
12
|
+
|
|
13
|
+
export const STORED_PROPERTY = Symbol('Domain event stored');
|
|
14
|
+
|
|
15
|
+
export type DomainEventDto = BaseEventDto & {
|
|
16
|
+
id: string;
|
|
17
|
+
entityName: string;
|
|
18
|
+
entityId: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type DomainEventParams<Body = unknown> = EventParams<Body> & {
|
|
22
|
+
id?: string;
|
|
23
|
+
entityName: string;
|
|
24
|
+
entityId: string;
|
|
25
|
+
[STORED_PROPERTY]?: true;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
export class DomainEvent<Body = unknown> extends BaseEvent<Body>
|
|
30
|
+
implements Identifiable, DtoSerializable<DomainEventDto> {
|
|
31
|
+
public readonly id: string;
|
|
32
|
+
public readonly entityName: string;
|
|
33
|
+
public readonly entityId: string;
|
|
34
|
+
public [STORED_PROPERTY]: boolean;
|
|
35
|
+
|
|
36
|
+
public constructor(params: DomainEventParams<Body>) {
|
|
37
|
+
super(params);
|
|
38
|
+
|
|
39
|
+
this.id = params.id ?? DomainEvent.generateId();
|
|
40
|
+
|
|
41
|
+
this.entityName = params.entityName;
|
|
42
|
+
this.entityId = params.entityId;
|
|
43
|
+
this[STORED_PROPERTY] = params[STORED_PROPERTY] ?? false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public toDto(): DomainEventDto {
|
|
47
|
+
return {
|
|
48
|
+
id: this.id,
|
|
49
|
+
entityId: String(this.entityId),
|
|
50
|
+
entityName: this.entityName,
|
|
51
|
+
name: this.name,
|
|
52
|
+
body: this.body,
|
|
53
|
+
occurredOn: this.occurredOn,
|
|
54
|
+
traceId: this.traceId,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public static generateId(): string {
|
|
59
|
+
return uuid();
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Describe,
|
|
3
|
+
expect,
|
|
4
|
+
ExpectException,
|
|
5
|
+
Suite,
|
|
6
|
+
Test,
|
|
7
|
+
} from '@hg-ts/tests';
|
|
8
|
+
|
|
9
|
+
import { AGGREGATE_EVENTS_SYMBOL } from '../base.aggregate.js';
|
|
10
|
+
import { CreatedEventNotFoundException } from '../created-event.not-found.exception.js';
|
|
11
|
+
import { STORED_PROPERTY } from '../events/domain.event.js';
|
|
12
|
+
|
|
13
|
+
import { ExampleCreatedEvent } from './example-created.event.js';
|
|
14
|
+
import { ExampleUpdatedEvent } from './example-updated.event.js';
|
|
15
|
+
import { ExampleAggregate } from './example.aggregate.js';
|
|
16
|
+
|
|
17
|
+
@Describe()
|
|
18
|
+
export class AggregateTest extends Suite {
|
|
19
|
+
@Test()
|
|
20
|
+
public simpleCreate(): void {
|
|
21
|
+
const aggregate = ExampleAggregate.create('original name');
|
|
22
|
+
|
|
23
|
+
const events = aggregate[AGGREGATE_EVENTS_SYMBOL];
|
|
24
|
+
|
|
25
|
+
expect(aggregate).toBeInstanceOf(ExampleAggregate);
|
|
26
|
+
expect(events).toBeInstanceOf(Array);
|
|
27
|
+
expect(events).toHaveLength(1);
|
|
28
|
+
expect(events[0]).toBeInstanceOf(ExampleCreatedEvent);
|
|
29
|
+
expect(events[0]![STORED_PROPERTY]).toBe(false);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Test()
|
|
33
|
+
public equalsSuccess(): void {
|
|
34
|
+
const baseAggregate = ExampleAggregate.create('original name');
|
|
35
|
+
const cloneAggregate1 = new ExampleAggregate(baseAggregate.toDto());
|
|
36
|
+
const cloneAggregate2 = new ExampleAggregate(baseAggregate.toDto());
|
|
37
|
+
|
|
38
|
+
expect(baseAggregate.equals(baseAggregate)).toBeTruthy();
|
|
39
|
+
expect(cloneAggregate1.equals(cloneAggregate2)).toBeTruthy();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test()
|
|
43
|
+
public equalsFails(): void {
|
|
44
|
+
const aggregate = ExampleAggregate.create('original name');
|
|
45
|
+
const cloneAggregate = ExampleAggregate.create('original name');
|
|
46
|
+
|
|
47
|
+
expect(aggregate.equals(cloneAggregate)).toBeFalsy();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test()
|
|
51
|
+
public updateName(): void {
|
|
52
|
+
const aggregate = ExampleAggregate.create('original name');
|
|
53
|
+
|
|
54
|
+
aggregate.update('new name');
|
|
55
|
+
|
|
56
|
+
const events = aggregate[AGGREGATE_EVENTS_SYMBOL];
|
|
57
|
+
|
|
58
|
+
expect(events).toBeInstanceOf(Array);
|
|
59
|
+
expect(events).toHaveLength(2);
|
|
60
|
+
expect(events[0]).toBeInstanceOf(ExampleCreatedEvent);
|
|
61
|
+
expect(events[1]).toBeInstanceOf(ExampleUpdatedEvent);
|
|
62
|
+
|
|
63
|
+
expect(aggregate.toDto().name).toEqual('new name');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test()
|
|
67
|
+
public restoreCreated(): void {
|
|
68
|
+
const aggregate = ExampleAggregate.create('original name');
|
|
69
|
+
const originalEvents = aggregate[AGGREGATE_EVENTS_SYMBOL];
|
|
70
|
+
|
|
71
|
+
originalEvents.forEach(event => {
|
|
72
|
+
event[STORED_PROPERTY] = true;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const restored = ExampleAggregate.restoreFromEvents(ExampleAggregate, ExampleCreatedEvent, originalEvents);
|
|
76
|
+
|
|
77
|
+
expect(restored.toDto()).toMatchObject(aggregate.toDto());
|
|
78
|
+
expect(restored.getNewEvets()).toHaveLength(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Test()
|
|
82
|
+
public restoreUpdated(): void {
|
|
83
|
+
const aggregate = ExampleAggregate.create('original name');
|
|
84
|
+
aggregate.update('new name');
|
|
85
|
+
const originalEvents = aggregate[AGGREGATE_EVENTS_SYMBOL];
|
|
86
|
+
|
|
87
|
+
originalEvents.forEach(event => {
|
|
88
|
+
event[STORED_PROPERTY] = true;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const restored = ExampleAggregate.restoreFromEvents(ExampleAggregate, ExampleCreatedEvent, originalEvents);
|
|
92
|
+
|
|
93
|
+
expect(restored.getEvents()).toHaveLength(originalEvents.length);
|
|
94
|
+
expect(restored.getEvents()[0]?.toDto().name).toBe(ExampleCreatedEvent.name);
|
|
95
|
+
expect(restored.toDto()).toMatchObject(aggregate.toDto());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test()
|
|
99
|
+
@ExpectException(CreatedEventNotFoundException)
|
|
100
|
+
public noCreatedEvent(): void {
|
|
101
|
+
ExampleAggregate.restoreFromEvents(ExampleAggregate, ExampleCreatedEvent, []);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AggregateCreatedEvent } from '../events/aggregate-created.event.js';
|
|
2
|
+
|
|
3
|
+
export class ExampleCreatedEvent extends AggregateCreatedEvent<{ name: string }> {
|
|
4
|
+
public constructor(id: string, name: string) {
|
|
5
|
+
super({
|
|
6
|
+
entityId: id,
|
|
7
|
+
entityName: 'Example',
|
|
8
|
+
body: { name },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DomainEvent } from '../events/domain.event.js';
|
|
2
|
+
|
|
3
|
+
export class ExampleUpdatedEvent extends DomainEvent<{ name: string}> {
|
|
4
|
+
public constructor(id: string, name: string) {
|
|
5
|
+
super({
|
|
6
|
+
entityId: id,
|
|
7
|
+
entityName: 'Example',
|
|
8
|
+
body: { name },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Listen } from '@hg-ts/events';
|
|
2
|
+
import { BaseAggregate } from '../base.aggregate.js';
|
|
3
|
+
import { ExampleCreatedEvent } from './example-created.event.js';
|
|
4
|
+
import { ExampleUpdatedEvent } from './example-updated.event.js';
|
|
5
|
+
|
|
6
|
+
type ExampleAggregateDto = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class ExampleAggregate extends BaseAggregate<ExampleAggregateDto> {
|
|
12
|
+
private name: string;
|
|
13
|
+
|
|
14
|
+
public constructor(dto: ExampleAggregateDto) {
|
|
15
|
+
super(dto);
|
|
16
|
+
|
|
17
|
+
this.name = dto.name;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public update(newName: string): void {
|
|
21
|
+
this.produceEvent(new ExampleUpdatedEvent(this.id, newName));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public toDto(): ExampleAggregateDto {
|
|
25
|
+
return {
|
|
26
|
+
id: this.id,
|
|
27
|
+
name: this.name,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public static create(name: string): ExampleAggregate {
|
|
32
|
+
const aggregate = new ExampleAggregate({ id: crypto.randomUUID(), name });
|
|
33
|
+
|
|
34
|
+
aggregate.produceEvent(new ExampleCreatedEvent(aggregate.id, name));
|
|
35
|
+
|
|
36
|
+
return aggregate;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Listen(ExampleUpdatedEvent)
|
|
40
|
+
protected onUpdated(event: ExampleUpdatedEvent): void {
|
|
41
|
+
this.name = event.body.name;
|
|
42
|
+
}
|
|
43
|
+
}
|