@hg-ts/repository 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 +11 -11
- package/src/exceptions/aggregate-event.already-exists.exception.ts +7 -0
- package/src/exceptions/aggregate-event.delete-restricted.exception.ts +7 -0
- package/src/exceptions/aggregate-event.not-found.exception.ts +7 -0
- package/src/exceptions/aggregate-event.update-restricted.exception.ts +7 -0
- package/src/exceptions/aggregate.already-exists.exception.ts +29 -0
- package/src/exceptions/aggregate.not-found.exception.ts +29 -0
- package/src/exceptions/index.ts +7 -0
- package/src/index.ts +2 -0
- package/src/repositories/base.event.repository.ts +59 -0
- package/src/repositories/base.memory.repository.ts +65 -0
- package/src/repositories/base.repository.ts +107 -0
- package/src/repositories/event.repository.ts +15 -0
- package/src/repositories/index.ts +6 -0
- package/src/repositories/repository.ts +30 -0
- package/src/tests/base-event-repository.test.ts +107 -0
- package/src/tests/base-repository.test.ts +161 -0
- package/src/tests/test-event.memory.repository.ts +52 -0
- package/src/tests/test.aggregate.ts +24 -0
- package/src/tests/test.already-exists.exception.ts +8 -0
- package/src/tests/test.memory.repository.ts +27 -0
- package/src/tests/test.not-found.exception.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hg-ts/repository",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
"test:dev": "vitest watch"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@hg-ts-config/typescript": "0.5.
|
|
22
|
-
"@hg-ts/domain": "0.5.
|
|
23
|
-
"@hg-ts/events": "0.5.
|
|
24
|
-
"@hg-ts/exception": "0.5.
|
|
25
|
-
"@hg-ts/linter": "0.5.
|
|
26
|
-
"@hg-ts/tests": "0.5.
|
|
27
|
-
"@hg-ts/types": "0.5.
|
|
21
|
+
"@hg-ts-config/typescript": "0.5.18",
|
|
22
|
+
"@hg-ts/domain": "0.5.18",
|
|
23
|
+
"@hg-ts/events": "0.5.18",
|
|
24
|
+
"@hg-ts/exception": "0.5.18",
|
|
25
|
+
"@hg-ts/linter": "0.5.18",
|
|
26
|
+
"@hg-ts/tests": "0.5.18",
|
|
27
|
+
"@hg-ts/types": "0.5.18",
|
|
28
28
|
"@types/node": "22.19.1",
|
|
29
29
|
"@types/uuid": "10.0.0",
|
|
30
30
|
"@vitest/coverage-v8": "4.0.14",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"vitest": "4.0.14"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@hg-ts/domain": "0.5.
|
|
42
|
-
"@hg-ts/events": "0.5.
|
|
43
|
-
"@hg-ts/exception": "0.5.
|
|
41
|
+
"@hg-ts/domain": "0.5.18",
|
|
42
|
+
"@hg-ts/events": "0.5.18",
|
|
43
|
+
"@hg-ts/exception": "0.5.18",
|
|
44
44
|
"reflect-metadata": "*",
|
|
45
45
|
"tslib": "*",
|
|
46
46
|
"uuid": "*",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseException } from '@hg-ts/exception';
|
|
2
|
+
import { format } from 'util';
|
|
3
|
+
|
|
4
|
+
export type AggregateAlreadyExistsParams<IdType = unknown> = {
|
|
5
|
+
id?: IdType;
|
|
6
|
+
code?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export abstract class AggregateAlreadyExistsException<IdType = unknown> extends BaseException {
|
|
10
|
+
public readonly id?: IdType;
|
|
11
|
+
|
|
12
|
+
public constructor(aggregateName: string, params: AggregateAlreadyExistsParams<IdType> = {}) {
|
|
13
|
+
const { code, id } = params;
|
|
14
|
+
const messageParts = [aggregateName];
|
|
15
|
+
|
|
16
|
+
if (id) {
|
|
17
|
+
messageParts.push(`with id ${format(id)}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
messageParts.push('already exists');
|
|
21
|
+
|
|
22
|
+
super(messageParts.join(' '), { code });
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line no-undefined
|
|
25
|
+
if (id !== undefined) {
|
|
26
|
+
this.id = id;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseException } from '@hg-ts/exception';
|
|
2
|
+
import { format } from 'util';
|
|
3
|
+
|
|
4
|
+
export type AggregateNotFoundParams<IdType = unknown> = {
|
|
5
|
+
id?: IdType;
|
|
6
|
+
code?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export abstract class AggregateNotFoundException<IdType = unknown> extends BaseException {
|
|
10
|
+
public readonly id?: IdType;
|
|
11
|
+
|
|
12
|
+
public constructor(aggregateName: string, params: AggregateNotFoundParams<IdType> = {}) {
|
|
13
|
+
const { code, id } = params;
|
|
14
|
+
const messageParts = [aggregateName];
|
|
15
|
+
|
|
16
|
+
if (id) {
|
|
17
|
+
messageParts.push(`with id ${format(id)}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
messageParts.push('not found');
|
|
21
|
+
|
|
22
|
+
super(messageParts.join(' '), { code });
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line no-undefined
|
|
25
|
+
if (id !== undefined) {
|
|
26
|
+
this.id = id;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './aggregate.not-found.exception.js';
|
|
2
|
+
export * from './aggregate.already-exists.exception.js';
|
|
3
|
+
|
|
4
|
+
export * from './aggregate-event.not-found.exception.js';
|
|
5
|
+
export * from './aggregate-event.already-exists.exception.js';
|
|
6
|
+
export * from './aggregate-event.update-restricted.exception.js';
|
|
7
|
+
export * from './aggregate-event.delete-restricted.exception.js';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DomainEvent,
|
|
3
|
+
DomainEventDto,
|
|
4
|
+
STORED_PROPERTY,
|
|
5
|
+
} from '@hg-ts/domain';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
AggregateAlreadyExistsException,
|
|
9
|
+
AggregateEventAlreadyExistsException,
|
|
10
|
+
AggregateEventDeleteRestrictedException,
|
|
11
|
+
AggregateEventNotFoundException,
|
|
12
|
+
AggregateEventUpdateRestrictedException,
|
|
13
|
+
AggregateNotFoundException,
|
|
14
|
+
} from '../exceptions/index.js';
|
|
15
|
+
import {
|
|
16
|
+
BaseRepository,
|
|
17
|
+
NotEmptyList,
|
|
18
|
+
} from './base.repository.js';
|
|
19
|
+
import {
|
|
20
|
+
EventFindOptions,
|
|
21
|
+
EventRepository,
|
|
22
|
+
} from './event.repository.js';
|
|
23
|
+
|
|
24
|
+
export abstract class BaseEventRepository
|
|
25
|
+
extends BaseRepository<DomainEvent, EventFindOptions> implements EventRepository {
|
|
26
|
+
public override async add(eventOrEvents: DomainEvent[] | DomainEvent): Promise<void> {
|
|
27
|
+
const events = Array.isArray(eventOrEvents) ? eventOrEvents : [eventOrEvents];
|
|
28
|
+
await super.add(events.filter(item => !item[STORED_PROPERTY]));
|
|
29
|
+
|
|
30
|
+
events.forEach(event => {
|
|
31
|
+
event[STORED_PROPERTY] = true;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public override async getNextId(): Promise<string> {
|
|
36
|
+
return DomainEvent.generateId();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public override async update(_entity: DomainEvent | DomainEvent[]): Promise<void> {
|
|
40
|
+
throw new AggregateEventUpdateRestrictedException();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public override async delete(_entity: DomainEvent | DomainEvent[]): Promise<void> {
|
|
44
|
+
throw new AggregateEventDeleteRestrictedException();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* v8 ignore next*/
|
|
48
|
+
protected override async rawUpdate(_entity: NotEmptyList<DomainEventDto>): Promise<void> {
|
|
49
|
+
throw new AggregateEventUpdateRestrictedException();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected override getNotFoundException(id: string): AggregateNotFoundException {
|
|
53
|
+
throw new AggregateEventNotFoundException(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected override getAlreadyExistsException(id: string): AggregateAlreadyExistsException {
|
|
57
|
+
throw new AggregateEventAlreadyExistsException(id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { BaseAggregate } from '@hg-ts/domain';
|
|
2
|
+
import {
|
|
3
|
+
BaseRepository,
|
|
4
|
+
NotEmptyList,
|
|
5
|
+
} from './base.repository.js';
|
|
6
|
+
import type {
|
|
7
|
+
AggregateDto,
|
|
8
|
+
BaseFindOptions,
|
|
9
|
+
} from './repository.js';
|
|
10
|
+
|
|
11
|
+
function clone<Aggregate extends BaseAggregate<any>>(aggregate: Aggregate): Aggregate {
|
|
12
|
+
const aggregatePrototype = Object.getPrototypeOf(aggregate);
|
|
13
|
+
const result = Object.create(aggregatePrototype);
|
|
14
|
+
|
|
15
|
+
Object.assign(result, aggregate);
|
|
16
|
+
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export abstract class BaseMemoryRepository<
|
|
21
|
+
Aggregate extends BaseAggregate<any>,
|
|
22
|
+
FindOptions extends BaseFindOptions = BaseFindOptions,
|
|
23
|
+
>
|
|
24
|
+
extends BaseRepository<Aggregate, FindOptions> {
|
|
25
|
+
protected readonly entitiesMap = new Map<string, AggregateDto<Aggregate>>();
|
|
26
|
+
|
|
27
|
+
public async rawAdd(entities: NotEmptyList<Aggregate>): Promise<void> {
|
|
28
|
+
for (const entity of entities) {
|
|
29
|
+
this.entitiesMap.set(entity.id, clone(entity.toDto()));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async rawUpdate(entities: NotEmptyList<Aggregate>): Promise<void> {
|
|
34
|
+
for (const entity of entities) {
|
|
35
|
+
this.entitiesMap.set(entity.id, clone(entity.toDto()));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public override async delete(aggregate: Aggregate | Aggregate[]): Promise<void> {
|
|
40
|
+
if (Array.isArray(aggregate)) {
|
|
41
|
+
await Promise.all(aggregate.map(async aggregate => this.delete(aggregate)));
|
|
42
|
+
} else {
|
|
43
|
+
this.entitiesMap.delete(aggregate.id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected async findEntities(options: FindOptions): Promise<Aggregate[]> {
|
|
48
|
+
const { id } = options;
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(id)) {
|
|
51
|
+
return id.filter(id => this.entitiesMap.has(id))
|
|
52
|
+
.map(id => this.entitiesMap.get(id)!).map(dto => this.createAggregate(dto));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof id !== 'undefined') {
|
|
56
|
+
const item = this.entitiesMap.get(id);
|
|
57
|
+
|
|
58
|
+
return item ? [this.createAggregate(item)] : [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return [...this.entitiesMap.values()].map(dto => this.createAggregate(dto));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected abstract createAggregate(dto: AggregateDto<Aggregate>): Aggregate;
|
|
65
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseAggregate,
|
|
3
|
+
DomainEvent,
|
|
4
|
+
} from '@hg-ts/domain';
|
|
5
|
+
import { WillNeverHappenedException } from '@hg-ts/exception';
|
|
6
|
+
import { v7 as uuid } from 'uuid';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AggregateAlreadyExistsException,
|
|
10
|
+
AggregateNotFoundException,
|
|
11
|
+
} from '../exceptions/index.js';
|
|
12
|
+
import {
|
|
13
|
+
BaseFindOptions,
|
|
14
|
+
Repository,
|
|
15
|
+
} from './repository.js';
|
|
16
|
+
|
|
17
|
+
export type NotEmptyList<T> = [T, ...T[]];
|
|
18
|
+
|
|
19
|
+
export abstract class BaseRepository<Aggregate extends (BaseAggregate<any> | DomainEvent),
|
|
20
|
+
FindOptions extends BaseFindOptions = BaseFindOptions>
|
|
21
|
+
extends Repository<Aggregate, FindOptions> {
|
|
22
|
+
public async getNextId(): Promise<string> {
|
|
23
|
+
return uuid();
|
|
24
|
+
}
|
|
25
|
+
public async get(id: string): Promise<Nullable<Aggregate>> {
|
|
26
|
+
// @ts-expect-error почему-то ts не понимает что типы совпадают
|
|
27
|
+
const [entity] = await this.find({ id });
|
|
28
|
+
|
|
29
|
+
return entity || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async getOrFail(id: string): Promise<Aggregate> {
|
|
33
|
+
const entity = await this.get(id);
|
|
34
|
+
|
|
35
|
+
if (entity === null) {
|
|
36
|
+
throw this.getNotFoundException(id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return entity;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async has(id: string): Promise<boolean> {
|
|
43
|
+
const entity = await this.get(id);
|
|
44
|
+
|
|
45
|
+
return entity !== null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public async find(options: Partial<FindOptions> = {}): Promise<Aggregate[]> {
|
|
49
|
+
return this.findEntities(options);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async add(entityOrEntities: Aggregate[] | Aggregate): Promise<void> {
|
|
53
|
+
const entities = Array.isArray(entityOrEntities) ? entityOrEntities : [entityOrEntities];
|
|
54
|
+
|
|
55
|
+
if (entities.length === 0) {
|
|
56
|
+
this.onEmptyAdd();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const ids = entities.map(({ id }) => id);
|
|
60
|
+
// @ts-expect-error почему-то ts не понимает что типы совпадают
|
|
61
|
+
const existEntities = await this.find({ id: ids });
|
|
62
|
+
|
|
63
|
+
if (existEntities.length > 0 && existEntities[0]) {
|
|
64
|
+
throw this.getAlreadyExistsException(existEntities[0].id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await this.rawAdd(entities as NotEmptyList<Aggregate>);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async update(entityOrEntities: Aggregate[] | Aggregate): Promise<void> {
|
|
71
|
+
const entities = Array.isArray(entityOrEntities)
|
|
72
|
+
? entityOrEntities
|
|
73
|
+
: [entityOrEntities];
|
|
74
|
+
|
|
75
|
+
if (entities.length === 0) {
|
|
76
|
+
this.onEmptyUpdate();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const ids = entities.map(({ id }) => id);
|
|
81
|
+
// @ts-expect-error почему-то ts не понимает что типы совпадают
|
|
82
|
+
const existEntities = await this.find({ id: ids });
|
|
83
|
+
|
|
84
|
+
if (existEntities.length < entities.length && entities[0]) {
|
|
85
|
+
throw this.getNotFoundException(existEntities[0]?.id ?? entities[0].id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await this.rawUpdate(entities as NotEmptyList<Aggregate>);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* v8 ignore next */
|
|
92
|
+
public async delete(_entities: Aggregate[] | Aggregate): Promise<void> {
|
|
93
|
+
throw new WillNeverHappenedException('The delete method is not implemented, so deleting entities is not allowed.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
protected onEmptyAdd(): void {}
|
|
97
|
+
protected onEmptyUpdate(): void {}
|
|
98
|
+
|
|
99
|
+
protected abstract findEntities(options?: Partial<FindOptions>): Promise<Aggregate[]>;
|
|
100
|
+
|
|
101
|
+
protected abstract rawAdd(dtos: NotEmptyList<Aggregate>): Promise<void>;
|
|
102
|
+
protected abstract rawUpdate(dtos: NotEmptyList<Aggregate>): Promise<void>;
|
|
103
|
+
|
|
104
|
+
protected abstract getNotFoundException(id: string): AggregateNotFoundException;
|
|
105
|
+
|
|
106
|
+
protected abstract getAlreadyExistsException(id: string): AggregateAlreadyExistsException;
|
|
107
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DomainEvent } from '@hg-ts/domain';
|
|
2
|
+
import {
|
|
3
|
+
BaseFindOptions,
|
|
4
|
+
Repository,
|
|
5
|
+
} from './repository.js';
|
|
6
|
+
|
|
7
|
+
export type EventFindOptions = BaseFindOptions & {
|
|
8
|
+
entityId?: string | string[];
|
|
9
|
+
entityName?: string;
|
|
10
|
+
limit?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export abstract class EventRepository
|
|
14
|
+
extends Repository<DomainEvent, EventFindOptions> {
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseAggregate,
|
|
3
|
+
DomainEvent,
|
|
4
|
+
} from '@hg-ts/domain';
|
|
5
|
+
|
|
6
|
+
export type AggregateDto<Aggregate extends BaseAggregate<any> | DomainEvent> =
|
|
7
|
+
ReturnType<Aggregate['toDto']>
|
|
8
|
+
|
|
9
|
+
export type BaseFindOptions = {
|
|
10
|
+
id?: string | string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export abstract class Repository<Aggregate extends BaseAggregate<any> | DomainEvent,
|
|
14
|
+
FindOptions extends BaseFindOptions = BaseFindOptions> {
|
|
15
|
+
public abstract getNextId(): Promise<string>;
|
|
16
|
+
|
|
17
|
+
public abstract get(id: string): Promise<Nullable<Aggregate>>;
|
|
18
|
+
|
|
19
|
+
public abstract getOrFail(id: string): Promise<Aggregate>;
|
|
20
|
+
|
|
21
|
+
public abstract find(options?: FindOptions): Promise<Aggregate[]>;
|
|
22
|
+
|
|
23
|
+
public abstract add(entity: Aggregate | Aggregate[]): Promise<void>;
|
|
24
|
+
|
|
25
|
+
public abstract update(entity: Aggregate | Aggregate[]): Promise<void>;
|
|
26
|
+
|
|
27
|
+
public abstract delete(entity: Aggregate | Aggregate[]): Promise<void>;
|
|
28
|
+
|
|
29
|
+
public abstract has(id: string): Promise<boolean>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DomainEvent,
|
|
3
|
+
STORED_PROPERTY,
|
|
4
|
+
} from '@hg-ts/domain';
|
|
5
|
+
import {
|
|
6
|
+
Describe,
|
|
7
|
+
expect,
|
|
8
|
+
ExpectException,
|
|
9
|
+
Suite,
|
|
10
|
+
Test,
|
|
11
|
+
} from '@hg-ts/tests';
|
|
12
|
+
import {
|
|
13
|
+
v4 as uuid,
|
|
14
|
+
validate,
|
|
15
|
+
} from 'uuid';
|
|
16
|
+
import {
|
|
17
|
+
AggregateEventAlreadyExistsException,
|
|
18
|
+
AggregateEventDeleteRestrictedException,
|
|
19
|
+
AggregateEventNotFoundException,
|
|
20
|
+
AggregateEventUpdateRestrictedException,
|
|
21
|
+
} from '../exceptions/index.js';
|
|
22
|
+
import { TestEventMemoryRepository } from './test-event.memory.repository.js';
|
|
23
|
+
|
|
24
|
+
@Describe()
|
|
25
|
+
export class BaseRepositoryTestSuite extends Suite {
|
|
26
|
+
private repository: TestEventMemoryRepository;
|
|
27
|
+
|
|
28
|
+
@Test()
|
|
29
|
+
public async getNextId(): Promise<void> {
|
|
30
|
+
const id = await this.repository.getNextId();
|
|
31
|
+
|
|
32
|
+
expect(validate(id)).toBeTruthy();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test()
|
|
36
|
+
public async addEmpty(): Promise<void> {
|
|
37
|
+
await this.repository.add([]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test()
|
|
41
|
+
public async add(): Promise<void> {
|
|
42
|
+
const event = this.createEvent();
|
|
43
|
+
await this.repository.add(event);
|
|
44
|
+
|
|
45
|
+
const found = await this.repository.getOrFail(event.id);
|
|
46
|
+
|
|
47
|
+
expect(found).toMatchObject(event);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test()
|
|
51
|
+
@ExpectException(AggregateEventUpdateRestrictedException)
|
|
52
|
+
public async update(): Promise<void> {
|
|
53
|
+
const event = this.createEvent();
|
|
54
|
+
await this.repository.add(event);
|
|
55
|
+
await this.repository.update(event);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test()
|
|
59
|
+
@ExpectException(AggregateEventDeleteRestrictedException)
|
|
60
|
+
public async delete(): Promise<void> {
|
|
61
|
+
const event = this.createEvent();
|
|
62
|
+
await this.repository.add(event);
|
|
63
|
+
await this.repository.delete(event);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test()
|
|
67
|
+
@ExpectException(AggregateEventNotFoundException)
|
|
68
|
+
public async getNotFound(): Promise<void> {
|
|
69
|
+
await this.repository.getOrFail(DomainEvent.generateId());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test()
|
|
73
|
+
public async addDuplicateAllowed(): Promise<void> {
|
|
74
|
+
const event = this.createEvent();
|
|
75
|
+
await this.repository.add(event);
|
|
76
|
+
await this.repository.add(event);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test()
|
|
80
|
+
@ExpectException(AggregateEventAlreadyExistsException)
|
|
81
|
+
public async addWithDuplicateId(): Promise<void> {
|
|
82
|
+
const event = this.createEvent();
|
|
83
|
+
await this.repository.add(event);
|
|
84
|
+
event[STORED_PROPERTY] = false;
|
|
85
|
+
await this.repository.add(event);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test()
|
|
89
|
+
public async getNotFoundWithoutExceptionTest(): Promise<void> {
|
|
90
|
+
const id = uuid();
|
|
91
|
+
|
|
92
|
+
await this.repository.get(id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public override async beforeEach(): Promise<void> {
|
|
96
|
+
this.repository = new TestEventMemoryRepository();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private createEvent(): DomainEvent {
|
|
100
|
+
return new DomainEvent({
|
|
101
|
+
occurredOn: new Date(),
|
|
102
|
+
entityName: 'Test',
|
|
103
|
+
entityId: uuid(),
|
|
104
|
+
body: {},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Describe,
|
|
3
|
+
expect,
|
|
4
|
+
ExpectException,
|
|
5
|
+
Suite,
|
|
6
|
+
Test,
|
|
7
|
+
} from '@hg-ts/tests';
|
|
8
|
+
import {
|
|
9
|
+
v4 as uuid,
|
|
10
|
+
validate,
|
|
11
|
+
} from 'uuid';
|
|
12
|
+
|
|
13
|
+
import { TestAggregate } from './test.aggregate.js';
|
|
14
|
+
import { TestAlreadyExistsException } from './test.already-exists.exception.js';
|
|
15
|
+
import { TestMemoryRepository } from './test.memory.repository.js';
|
|
16
|
+
import { TestNotFoundException } from './test.not-found.exception.js';
|
|
17
|
+
|
|
18
|
+
@Describe()
|
|
19
|
+
export class BaseRepositoryTestSuite extends Suite {
|
|
20
|
+
private repository: TestMemoryRepository;
|
|
21
|
+
|
|
22
|
+
@Test()
|
|
23
|
+
public async getNextId(): Promise<void> {
|
|
24
|
+
const id = await this.repository.getNextId();
|
|
25
|
+
|
|
26
|
+
expect(validate(id)).toBeTruthy();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test()
|
|
30
|
+
public async has(): Promise<void> {
|
|
31
|
+
const id = uuid();
|
|
32
|
+
const aggregate = new TestAggregate(id);
|
|
33
|
+
|
|
34
|
+
await this.repository.add(aggregate);
|
|
35
|
+
|
|
36
|
+
const exists = await this.repository.has(id);
|
|
37
|
+
|
|
38
|
+
expect(exists).toBeTruthy();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Test()
|
|
42
|
+
public async saveEmpty(): Promise<void> {
|
|
43
|
+
await this.repository.add([]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test()
|
|
47
|
+
public async save(): Promise<void> {
|
|
48
|
+
const id = uuid();
|
|
49
|
+
const aggregate = new TestAggregate(id);
|
|
50
|
+
|
|
51
|
+
await this.repository.add(aggregate);
|
|
52
|
+
|
|
53
|
+
const savedAggregate = await this.repository.getOrFail(id);
|
|
54
|
+
|
|
55
|
+
expect(savedAggregate).toBeInstanceOf(TestAggregate);
|
|
56
|
+
expect(savedAggregate).not.toBe(aggregate);
|
|
57
|
+
expect({ ...savedAggregate }).toMatchObject({ ...aggregate });
|
|
58
|
+
|
|
59
|
+
const found = await this.repository.find();
|
|
60
|
+
|
|
61
|
+
expect(found).toHaveLength(1);
|
|
62
|
+
expect(found[0]).toMatchObject(savedAggregate);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Test()
|
|
66
|
+
@ExpectException(TestAlreadyExistsException)
|
|
67
|
+
public async saveDuplicate(): Promise<void> {
|
|
68
|
+
const id = uuid();
|
|
69
|
+
const aggregate = new TestAggregate(id);
|
|
70
|
+
|
|
71
|
+
await this.repository.add(aggregate);
|
|
72
|
+
await this.repository.add(aggregate);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test()
|
|
76
|
+
public async updateEmpty(): Promise<void> {
|
|
77
|
+
await this.repository.update([]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Test()
|
|
81
|
+
@ExpectException(TestNotFoundException)
|
|
82
|
+
public async updateNotFound(): Promise<void> {
|
|
83
|
+
const id = uuid();
|
|
84
|
+
const aggregate = new TestAggregate(id);
|
|
85
|
+
await this.repository.update(aggregate);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test()
|
|
89
|
+
public async delete(): Promise<void> {
|
|
90
|
+
const id = uuid();
|
|
91
|
+
const aggregate = new TestAggregate(id);
|
|
92
|
+
|
|
93
|
+
await this.repository.add(aggregate);
|
|
94
|
+
|
|
95
|
+
const savedAggregate = await this.repository.getOrFail(id);
|
|
96
|
+
|
|
97
|
+
expect(savedAggregate).toBeInstanceOf(TestAggregate);
|
|
98
|
+
|
|
99
|
+
await this.repository.delete(aggregate);
|
|
100
|
+
const found = await this.repository.get(id);
|
|
101
|
+
|
|
102
|
+
expect(found).toBeNull();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test()
|
|
106
|
+
public async deleteList(): Promise<void> {
|
|
107
|
+
await this.repository.add(new TestAggregate(uuid()));
|
|
108
|
+
await this.repository.add(new TestAggregate(uuid()));
|
|
109
|
+
|
|
110
|
+
const found = await this.repository.find();
|
|
111
|
+
|
|
112
|
+
await this.repository.delete(found);
|
|
113
|
+
|
|
114
|
+
const foundAfterDelete = await this.repository.find();
|
|
115
|
+
|
|
116
|
+
expect(foundAfterDelete).toHaveLength(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Test()
|
|
120
|
+
public async updateTest(): Promise<void> {
|
|
121
|
+
const id = uuid();
|
|
122
|
+
const initialValue = uuid();
|
|
123
|
+
const expectedValue = uuid();
|
|
124
|
+
const aggregate = new TestAggregate(id);
|
|
125
|
+
aggregate.setSomeField(initialValue);
|
|
126
|
+
|
|
127
|
+
await this.repository.add(aggregate);
|
|
128
|
+
|
|
129
|
+
const savedAggregate = await this.repository.getOrFail(id);
|
|
130
|
+
|
|
131
|
+
expect(savedAggregate).toBeInstanceOf(TestAggregate);
|
|
132
|
+
expect(savedAggregate.someField).toBe(initialValue);
|
|
133
|
+
savedAggregate.setSomeField(expectedValue);
|
|
134
|
+
|
|
135
|
+
await this.repository.update(savedAggregate);
|
|
136
|
+
|
|
137
|
+
const updatedAggregate = await this.repository.getOrFail(id);
|
|
138
|
+
|
|
139
|
+
expect(updatedAggregate).toBeInstanceOf(TestAggregate);
|
|
140
|
+
expect(updatedAggregate.someField).toBe(expectedValue);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@Test()
|
|
144
|
+
@ExpectException(TestNotFoundException)
|
|
145
|
+
public async getOrFailNotFoundExceptionTest(): Promise<void> {
|
|
146
|
+
const id = uuid();
|
|
147
|
+
|
|
148
|
+
await this.repository.getOrFail(id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@Test()
|
|
152
|
+
public async getNotFoundWithoutExceptionTest(): Promise<void> {
|
|
153
|
+
const id = uuid();
|
|
154
|
+
|
|
155
|
+
await this.repository.get(id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public override async beforeEach(): Promise<void> {
|
|
159
|
+
this.repository = new TestMemoryRepository();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DomainEvent } from '@hg-ts/domain';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BaseEventRepository,
|
|
5
|
+
EventFindOptions,
|
|
6
|
+
NotEmptyList,
|
|
7
|
+
} from '../repositories/index.js';
|
|
8
|
+
|
|
9
|
+
export class TestEventMemoryRepository extends BaseEventRepository {
|
|
10
|
+
private readonly events = new Map<string, DomainEvent>();
|
|
11
|
+
|
|
12
|
+
/* v8 ignore next */
|
|
13
|
+
protected async findEntities(options: Partial<EventFindOptions> = {}): Promise<DomainEvent[]> {
|
|
14
|
+
const { id, entityId, entityName, limit } = options;
|
|
15
|
+
const events = [...this.events.values()]
|
|
16
|
+
.filter(event => {
|
|
17
|
+
if (id) {
|
|
18
|
+
if (Array.isArray(id)) {
|
|
19
|
+
return id.includes(event.id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return event.id === id;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (entityId) {
|
|
26
|
+
if (Array.isArray(entityId)) {
|
|
27
|
+
return entityId.includes(event.entityId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return event.entityId === entityId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (entityName) {
|
|
34
|
+
return event.entityName === entityName;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (limit && limit > 0) {
|
|
41
|
+
return events.slice(0, limit);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return events;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected async rawAdd(events: NotEmptyList<DomainEvent>): Promise<void> {
|
|
48
|
+
events.forEach(event => {
|
|
49
|
+
this.events.set(event.id, event);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BaseAggregate } from '@hg-ts/domain';
|
|
2
|
+
|
|
3
|
+
type TestAggregateDto = {
|
|
4
|
+
id: string;
|
|
5
|
+
someField: Nullable<string>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class TestAggregate extends BaseAggregate<TestAggregateDto> {
|
|
9
|
+
public someField: Nullable<string> = null;
|
|
10
|
+
|
|
11
|
+
public constructor(id: string, someField: Nullable<string> = null) {
|
|
12
|
+
super({ id });
|
|
13
|
+
|
|
14
|
+
this.someField = someField;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public setSomeField(value: Nullable<string>): void {
|
|
18
|
+
this.someField = value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public toDto(): TestAggregateDto {
|
|
22
|
+
return { id: this.id, someField: this.someField };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AggregateAlreadyExistsException } from '../exceptions/index.js';
|
|
2
|
+
import type { TestAggregate } from './test.aggregate.js';
|
|
3
|
+
|
|
4
|
+
export class TestAlreadyExistsException extends AggregateAlreadyExistsException {
|
|
5
|
+
public constructor(id: Nullable<TestAggregate['id']> = null) {
|
|
6
|
+
super('Test', { id, code: 0 });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AggregateAlreadyExistsException,
|
|
3
|
+
AggregateNotFoundException,
|
|
4
|
+
} from '../exceptions/index.js';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
AggregateDto,
|
|
8
|
+
BaseMemoryRepository,
|
|
9
|
+
} from '../repositories/index.js';
|
|
10
|
+
|
|
11
|
+
import { TestAggregate } from './test.aggregate.js';
|
|
12
|
+
import { TestAlreadyExistsException } from './test.already-exists.exception.js';
|
|
13
|
+
import { TestNotFoundException } from './test.not-found.exception.js';
|
|
14
|
+
|
|
15
|
+
export class TestMemoryRepository extends BaseMemoryRepository<TestAggregate> {
|
|
16
|
+
protected getNotFoundException(id: TestAggregate['id']): AggregateNotFoundException {
|
|
17
|
+
return new TestNotFoundException(id);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected getAlreadyExistsException(id: TestAggregate['id']): AggregateAlreadyExistsException {
|
|
21
|
+
return new TestAlreadyExistsException(id);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected createAggregate(dto: AggregateDto<TestAggregate>): TestAggregate {
|
|
25
|
+
return new TestAggregate(dto.id, dto.someField);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AggregateNotFoundException } from '../exceptions/index.js';
|
|
2
|
+
import type { TestAggregate } from './test.aggregate.js';
|
|
3
|
+
|
|
4
|
+
export class TestNotFoundException extends AggregateNotFoundException {
|
|
5
|
+
public constructor(id: Nullable<TestAggregate['id']> = null) {
|
|
6
|
+
super('Test', { id, code: 0 });
|
|
7
|
+
}
|
|
8
|
+
}
|