@invariant--labs/foundation 1.1.2
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/.pnp.cjs +22192 -0
- package/.pnp.loader.mjs +2126 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +527 -0
- package/README.md +3 -0
- package/eslint.config.mjs +52 -0
- package/invariant.json +22 -0
- package/jest/jest.config.base.ts +30 -0
- package/jest/jest.spec.base.ts +7 -0
- package/jest.config.js +49 -0
- package/package.json +99 -0
- package/src/core/application/index.ts +1 -0
- package/src/core/application/persistence/index.ts +3 -0
- package/src/core/application/persistence/query-builder.ts +2 -0
- package/src/core/application/persistence/read-projection.ts +2 -0
- package/src/core/application/persistence/repository.ts +23 -0
- package/src/core/domain/entities/aggregate-root.ts +34 -0
- package/src/core/domain/entities/entity.spec.ts +43 -0
- package/src/core/domain/entities/entity.ts +29 -0
- package/src/core/domain/entities/identifier.spec.ts +34 -0
- package/src/core/domain/entities/identifier.ts +16 -0
- package/src/core/domain/entities/index.ts +5 -0
- package/src/core/domain/entities/projection.ts +7 -0
- package/src/core/domain/entities/unique-entity-id.ts +9 -0
- package/src/core/domain/events/domain-event.ts +7 -0
- package/src/core/domain/events/index.ts +1 -0
- package/src/core/domain/index.ts +3 -0
- package/src/core/domain/value-objects/index.ts +2 -0
- package/src/core/domain/value-objects/range.ts +4 -0
- package/src/core/domain/value-objects/value-object.spec.ts +45 -0
- package/src/core/domain/value-objects/value-object.ts +17 -0
- package/src/core/errors/command-failure.error.spec.ts +30 -0
- package/src/core/errors/command-failure.error.ts +9 -0
- package/src/core/errors/command-filter.error.ts +3 -0
- package/src/core/errors/detailed.error.ts +25 -0
- package/src/core/errors/domain.error.spec.ts +27 -0
- package/src/core/errors/domain.error.ts +9 -0
- package/src/core/errors/entity-not-found.error.spec.ts +32 -0
- package/src/core/errors/entity-not-found.error.ts +9 -0
- package/src/core/errors/fake-implementation.error.spec.ts +27 -0
- package/src/core/errors/fake-implementation.error.ts +15 -0
- package/src/core/errors/index.ts +8 -0
- package/src/core/errors/query-failure.error.ts +8 -0
- package/src/core/errors/too-many-results.error.ts +3 -0
- package/src/core/index.ts +4 -0
- package/src/core/infrastructure/config/config.provider.ts +78 -0
- package/src/core/infrastructure/config/config.service.ts +25 -0
- package/src/core/infrastructure/config/index.ts +2 -0
- package/src/core/infrastructure/messaging/fake/fake-messaging.service.ts +17 -0
- package/src/core/infrastructure/messaging/fake/fake-queue-manager.service.ts +9 -0
- package/src/core/infrastructure/messaging/fake/fake-queue-messaging.service.ts +9 -0
- package/src/core/infrastructure/messaging/fake/index.ts +3 -0
- package/src/core/infrastructure/messaging/index.ts +6 -0
- package/src/core/infrastructure/messaging/messaging.service.ts +3 -0
- package/src/core/infrastructure/messaging/queue-manager.service.ts +6 -0
- package/src/core/infrastructure/messaging/queue-messaging.service.ts +3 -0
- package/src/core/infrastructure/messaging/rabbitmq/index.ts +2 -0
- package/src/core/infrastructure/messaging/rabbitmq/rabbit-messaging.service.ts +11 -0
- package/src/core/infrastructure/messaging/rabbitmq/rabbit-queue-messaging.service.ts +11 -0
- package/src/core/infrastructure/messaging/types.ts +28 -0
- package/src/core/infrastructure/persistence/errors/index.ts +2 -0
- package/src/core/infrastructure/persistence/errors/model-to-entity-conversion.error.ts +9 -0
- package/src/core/infrastructure/persistence/errors/persistence.error.ts +8 -0
- package/src/core/infrastructure/persistence/in-memory/fake.repository.ts +79 -0
- package/src/core/infrastructure/persistence/in-memory/in-memory.query-builder.ts +4 -0
- package/src/core/infrastructure/persistence/in-memory/in-memory.repository.spec.ts +50 -0
- package/src/core/infrastructure/persistence/in-memory/in-memory.repository.ts +81 -0
- package/src/core/infrastructure/persistence/in-memory/index.ts +3 -0
- package/src/core/infrastructure/persistence/index.ts +4 -0
- package/src/core/infrastructure/persistence/read-side/in-memory.query-builder.ts +4 -0
- package/src/core/infrastructure/persistence/read-side/index.ts +1 -0
- package/src/core/infrastructure/persistence/read-side/knex/index.ts +2 -0
- package/src/core/infrastructure/persistence/read-side/knex/knex-types.definition.ts +13 -0
- package/src/core/infrastructure/persistence/read-side/knex/knex.query-builder.ts +70 -0
- package/src/core/infrastructure/persistence/read-side/knex-query-builder.ts +70 -0
- package/src/core/infrastructure/persistence/read-side/knex-types.definition.ts +13 -0
- package/src/core/infrastructure/persistence/write-side/aggregate-typeorm-repository.ts +87 -0
- package/src/core/infrastructure/persistence/write-side/entity-typeorm-repository.ts +64 -0
- package/src/core/infrastructure/persistence/write-side/in-memory.repository.ts +82 -0
- package/src/core/infrastructure/persistence/write-side/index.ts +1 -0
- package/src/core/infrastructure/persistence/write-side/model-attributes.ts +4 -0
- package/src/core/infrastructure/persistence/write-side/orm-embedded-mapper.ts +11 -0
- package/src/core/infrastructure/persistence/write-side/orm-mapper.ts +11 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/aggregate-typeorm.repository.ts +87 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/entity-typeorm.repository.ts +64 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/index.ts +5 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/model-attributes.ts +4 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/orm-embedded.mapper.ts +11 -0
- package/src/core/infrastructure/persistence/write-side/typeorm/orm.mapper.ts +11 -0
- package/src/core/types/architecture-layer.ts +52 -0
- package/src/core/types/array-element.ts +5 -0
- package/src/core/types/index.ts +2 -0
- package/src/index.ts +30 -0
- package/src/modules/config/config.module.ts +9 -0
- package/src/modules/config/index.ts +2 -0
- package/src/modules/graphql/index.ts +1 -0
- package/src/modules/graphql/paginated-response.object-type.ts +23 -0
- package/src/modules/healthcheck/healthcheck-out.dto.ts +43 -0
- package/src/modules/healthcheck/index.ts +1 -0
- package/src/modules/knex/index.ts +3 -0
- package/src/modules/knex/knex-core.module.ts +30 -0
- package/src/modules/knex/knex.decorator.ts +5 -0
- package/src/modules/knex/knex.interface.ts +12 -0
- package/src/modules/knex/knex.module.ts +14 -0
- package/src/modules/knex/knex.token.ts +2 -0
- package/src/modules/logger/app-logger.ts +28 -0
- package/src/modules/logger/index.ts +5 -0
- package/src/modules/logger/log.ts +26 -0
- package/src/modules/logger/logger.module.ts +47 -0
- package/src/modules/logger/logger.service.ts +131 -0
- package/src/modules/logger/transports/console-color.transport.ts +41 -0
- package/src/modules/logger/transports/console-json.transport.ts +23 -0
- package/src/modules/logger/transports/console.transport.ts +10 -0
- package/src/modules/logger/transports/fake.transport.ts +18 -0
- package/src/modules/logger/transports/index.ts +5 -0
- package/src/modules/logger/transports/logger-transport.ts +22 -0
- package/src/modules/messaging/index.ts +2 -0
- package/src/modules/messaging/messaging.module.ts +92 -0
- package/src/modules/queue/default-queue-name.resolver.ts +5 -0
- package/src/modules/queue/index.ts +3 -0
- package/src/modules/queue/queue.module.ts +73 -0
- package/src/modules/queue/rabbit-queue-manager.service.ts +67 -0
- package/src/nestjs/errors/handlers/error-handler.ts +38 -0
- package/src/nestjs/errors/handlers/error-handler.type.ts +23 -0
- package/src/nestjs/errors/handlers/generic-error-handler.ts +59 -0
- package/src/nestjs/errors/handlers/handle-errors.decorator.ts +43 -0
- package/src/nestjs/errors/handlers/index.ts +5 -0
- package/src/nestjs/errors/handlers/validation-error-handler.ts +46 -0
- package/src/nestjs/errors/index.ts +2 -0
- package/src/nestjs/errors/parsers/axios-metadata.parser.ts +21 -0
- package/src/nestjs/errors/parsers/error-metadata.definition.ts +1 -0
- package/src/nestjs/errors/parsers/index.ts +5 -0
- package/src/nestjs/errors/parsers/knex-metadata.parser.ts +18 -0
- package/src/nestjs/errors/parsers/typeorm-metadata.parser.ts +19 -0
- package/src/nestjs/errors/parsers/workos-metadata.parser.ts +17 -0
- package/src/nestjs/http/decorators/index.ts +1 -0
- package/src/nestjs/http/decorators/log-app-ctx.decorator.ts +32 -0
- package/src/nestjs/http/dtos/date-range.dto.ts +15 -0
- package/src/nestjs/http/dtos/index.ts +1 -0
- package/src/nestjs/http/filters/all-exceptions.filter.ts +92 -0
- package/src/nestjs/http/filters/command-failure.filter.ts +12 -0
- package/src/nestjs/http/filters/index.ts +4 -0
- package/src/nestjs/http/filters/rpc-exceptions.filter.ts +10 -0
- package/src/nestjs/http/filters/too-many-results.filter.ts +12 -0
- package/src/nestjs/http/index.ts +6 -0
- package/src/nestjs/http/interceptors/index.ts +2 -0
- package/src/nestjs/http/interceptors/log-app-ctx.interceptor.ts +109 -0
- package/src/nestjs/http/interceptors/serialize-output.interceptor.ts +19 -0
- package/src/nestjs/http/middleware/http-logger.middleware.ts +31 -0
- package/src/nestjs/http/middleware/index.ts +2 -0
- package/src/nestjs/http/middleware/logger.middleware.ts +21 -0
- package/src/nestjs/http/swagger/index.ts +1 -0
- package/src/nestjs/http/swagger/swagger.ts +44 -0
- package/src/nestjs/index.ts +2 -0
- package/src/testing/command-bus.stub.ts +33 -0
- package/src/testing/event-bus.stub.ts +56 -0
- package/src/testing/event-publisher.stub.ts +24 -0
- package/src/testing/fake-logger.ts +20 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/query-bus.stub.ts +27 -0
- package/src/testing/stub.spec.ts +250 -0
- package/src/testing/stub.ts +170 -0
- package/src/testing/stubs/command-bus.stub.ts +33 -0
- package/src/testing/stubs/event-bus.stub.ts +56 -0
- package/src/testing/stubs/event-publisher.stub.ts +24 -0
- package/src/testing/stubs/index.ts +5 -0
- package/src/testing/stubs/query-bus.stub.ts +27 -0
- package/src/testing/stubs/stub.ts +170 -0
- package/src/utils/array.spec.ts +29 -0
- package/src/utils/array.ts +10 -0
- package/src/utils/base64.spec.ts +18 -0
- package/src/utils/base64.ts +6 -0
- package/src/utils/collection.ts +4 -0
- package/src/utils/common.ts +13 -0
- package/src/utils/csv.ts +49 -0
- package/src/utils/date/date-range.ts +4 -0
- package/src/utils/date/date.spec.ts +100 -0
- package/src/utils/date/date.ts +177 -0
- package/src/utils/date/diff.spec.ts +66 -0
- package/src/utils/date/diffYear.spec.ts +23 -0
- package/src/utils/date/fillMissingRangeValues.spec.ts +523 -0
- package/src/utils/date/groubBy.spec.ts +183 -0
- package/src/utils/date/index.ts +2 -0
- package/src/utils/date/isSame.spec.ts +111 -0
- package/src/utils/file.spec.ts +66 -0
- package/src/utils/file.ts +5 -0
- package/src/utils/hash-key.ts +23 -0
- package/src/utils/index.ts +14 -0
- package/src/utils/invariant.ts +3 -0
- package/src/utils/iso-date.ts +11 -0
- package/src/utils/object.spec.ts +18 -0
- package/src/utils/object.ts +6 -0
- package/src/utils/paginated.ts +36 -0
- package/src/utils/string.spec.ts +10 -0
- package/src/utils/string.ts +19 -0
- package/src/utils/type.ts +9 -0
- package/src/utils/xml.ts +6 -0
- package/src/validation/ensure-array.decorator.ts +5 -0
- package/src/validation/index.ts +7 -0
- package/src/validation/is-iso-date.decorator.spec.ts +29 -0
- package/src/validation/is-iso-date.decorator.ts +30 -0
- package/src/validation/is-less-than-or-equal.decorator.spec.ts +30 -0
- package/src/validation/is-less-than-or-equal.decorator.ts +52 -0
- package/src/validation/is-less-than.decorator.spec.ts +36 -0
- package/src/validation/is-less-than.decorator.ts +52 -0
- package/src/validation/is-more-than-or-equal.decorator.spec.ts +30 -0
- package/src/validation/is-more-than-or-equal.decorator.ts +52 -0
- package/src/validation/is-more-than.decorator.spec.ts +36 -0
- package/src/validation/is-more-than.decorator.ts +52 -0
- package/src/validation/is-time-string.decorator.spec.ts +35 -0
- package/src/validation/is-time-string.decorator.ts +29 -0
- package/tsconfig.build.json +6 -0
- package/tsconfig.json +34 -0
- package/tsconfig.spec.json +14 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
import { QueryBuilder } from "../../../application";
|
|
4
|
+
import { ArchitectureLevel } from "../../../types";
|
|
5
|
+
import { EntityNotFoundError } from "../../../errors";
|
|
6
|
+
import { knexTableAlias } from "./knex-types.definition";
|
|
7
|
+
|
|
8
|
+
export abstract class KnexQueryBuilder implements QueryBuilder {
|
|
9
|
+
constructor(private readonly knex: Knex) {}
|
|
10
|
+
|
|
11
|
+
private createDefaultQueryBuilder<TModel extends object, TResult = Partial<TModel>>(
|
|
12
|
+
tableName: string,
|
|
13
|
+
): Knex.QueryBuilder<TModel, TResult> {
|
|
14
|
+
return this.knex.from<TModel, TResult>(tableName);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protected exists(tableName: string, id: string): Promise<boolean> {
|
|
18
|
+
return this.createDefaultQueryBuilder(tableName)
|
|
19
|
+
.where("id", id)
|
|
20
|
+
.first()
|
|
21
|
+
.then((result) => !!result);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected findOne<TModel extends object, TResult = Partial<TModel>>(tableName: string, id: string): Promise<TResult | null> {
|
|
25
|
+
return this.createDefaultQueryBuilder<TModel, TResult>(tableName)
|
|
26
|
+
.select("*")
|
|
27
|
+
.where("id", id)
|
|
28
|
+
.first()
|
|
29
|
+
.then((result) => (result as TResult) || null);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected findOneOrFail<TModel extends object, TResult = Partial<TModel>>(tableName: string, id: string): Promise<TResult> {
|
|
33
|
+
return this.createDefaultQueryBuilder<TModel, TResult>(tableName)
|
|
34
|
+
.select("*")
|
|
35
|
+
.where("id", id)
|
|
36
|
+
.first()
|
|
37
|
+
.then((result) => {
|
|
38
|
+
if (!result) {
|
|
39
|
+
throw new EntityNotFoundError(
|
|
40
|
+
tableName,
|
|
41
|
+
{ [ArchitectureLevel.INFRASTRUCTURE]: { "persistence-sql": { className: KnexQueryBuilder.name } } },
|
|
42
|
+
{ id },
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result as TResult;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected findByIds<TModel extends object, TResult = Partial<TModel>>(tableName: string, ids: string[]): Promise<TResult[]> {
|
|
51
|
+
return this.createDefaultQueryBuilder<TModel, TResult>(tableName)
|
|
52
|
+
.whereIn("id", ids)
|
|
53
|
+
.then((result) => result as TResult[]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
protected count(tableName: string, options: Record<string, unknown>): Promise<number> {
|
|
57
|
+
return this.createDefaultQueryBuilder(tableName)
|
|
58
|
+
.count<Array<{ count: number }>>("*", { as: "count" })
|
|
59
|
+
.where(options)
|
|
60
|
+
.then(([{ count }]) => count);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get queryBuilder() {
|
|
64
|
+
return this.knex.queryBuilder();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
dataSource<T, Alias extends string>(tableName: Alias) {
|
|
68
|
+
return this.knex<knexTableAlias<T, Alias>>(tableName);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type CamelToSnakeCase<S extends string> = S extends `${infer T}${infer U}`
|
|
2
|
+
? `${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${CamelToSnakeCase<U>}`
|
|
3
|
+
: S;
|
|
4
|
+
|
|
5
|
+
export type knexInfer<T> = {
|
|
6
|
+
[K in keyof T as CamelToSnakeCase<Extract<K, string>>]: T[K] extends CallableFunction ? never : T[K];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type knexTableAlias<Table, Alias extends string> = {
|
|
10
|
+
[K in keyof Table as `${Alias}.${string & CamelToSnakeCase<Extract<K, string>>}`]: Table[K] extends CallableFunction
|
|
11
|
+
? never
|
|
12
|
+
: Table[K];
|
|
13
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DataSource, EntityManager, ObjectLiteral, SelectQueryBuilder } from "typeorm";
|
|
2
|
+
|
|
3
|
+
import { Repository } from "../../../application";
|
|
4
|
+
import { AggregateRoot, EntityProps } from "../../../domain";
|
|
5
|
+
import { ArchitectureLevel } from "../../../types";
|
|
6
|
+
import { typeormMetadataParser } from "../../../../nestjs/errors/parsers/typeorm-metadata.parser";
|
|
7
|
+
import { EntityNotFoundError } from "../../../errors";
|
|
8
|
+
import { PersistenceError } from "../errors/persistence.error";
|
|
9
|
+
|
|
10
|
+
export abstract class AggregateTypeormRepository<
|
|
11
|
+
DomainAggregate extends AggregateRoot<EntityProps>,
|
|
12
|
+
OrmModel extends ObjectLiteral,
|
|
13
|
+
> implements Repository<AggregateRoot<EntityProps>>
|
|
14
|
+
{
|
|
15
|
+
protected abstract createDefaultQueryBuilder(): SelectQueryBuilder<OrmModel>;
|
|
16
|
+
protected abstract toEntity(model: OrmModel): DomainAggregate;
|
|
17
|
+
protected abstract toModel(aggregate: DomainAggregate): OrmModel | Promise<OrmModel>;
|
|
18
|
+
protected manager: EntityManager;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
dataSource: DataSource,
|
|
22
|
+
private readonly aggregateName: string,
|
|
23
|
+
) {
|
|
24
|
+
this.manager = dataSource.manager;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async count(): Promise<number> {
|
|
28
|
+
return this.createDefaultQueryBuilder().getCount();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async exists(id: string): Promise<boolean> {
|
|
32
|
+
const count = await this.createDefaultQueryBuilder().where({ id }).getCount();
|
|
33
|
+
return count === 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async findOne(id: string): Promise<DomainAggregate | null> {
|
|
37
|
+
const model = await this.createDefaultQueryBuilder().where({ id }).getOne();
|
|
38
|
+
return model ? this.toEntity(model) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async findOneOrFail(aggregateId: string): Promise<DomainAggregate> {
|
|
42
|
+
const aggregate = await this.findOne(aggregateId);
|
|
43
|
+
if (!aggregate) {
|
|
44
|
+
throw new EntityNotFoundError(
|
|
45
|
+
this.aggregateName,
|
|
46
|
+
{ [ArchitectureLevel.INFRASTRUCTURE]: { "persistence-sql": { className: "AggregateTypeormRepository" } } },
|
|
47
|
+
{ id: aggregateId },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return aggregate;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async findByIds(ids: string[]): Promise<DomainAggregate[]> {
|
|
54
|
+
const models = await this.createDefaultQueryBuilder().whereInIds(ids).getMany();
|
|
55
|
+
return models.map((model) => this.toEntity(model));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async save(aggregate: DomainAggregate): Promise<DomainAggregate> {
|
|
59
|
+
try {
|
|
60
|
+
const model = await this.manager.save(this.toModel(aggregate));
|
|
61
|
+
aggregate.commit();
|
|
62
|
+
return this.toEntity(model);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new PersistenceError("Failed to save aggregate", {
|
|
65
|
+
"aggregate-root": {
|
|
66
|
+
className: AggregateTypeormRepository.name,
|
|
67
|
+
meta: typeormMetadataParser(error, { method: this.save.name }),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async saveAll(aggregates: DomainAggregate[]): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
const models = await Promise.all(aggregates.map((a) => Promise.resolve(this.toModel(a))));
|
|
76
|
+
await this.manager.save(models);
|
|
77
|
+
aggregates.forEach((a) => a.commit());
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new PersistenceError("Failed to save aggregates", {
|
|
80
|
+
"aggregate-root": {
|
|
81
|
+
className: AggregateTypeormRepository.name,
|
|
82
|
+
meta: typeormMetadataParser(error, { method: this.saveAll.name }),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DataSource, EntityManager, ObjectLiteral, SelectQueryBuilder } from "typeorm";
|
|
2
|
+
|
|
3
|
+
import { Repository } from "../../../application";
|
|
4
|
+
import { Entity, EntityProps } from "../../../domain";
|
|
5
|
+
import { ArchitectureLevel } from "../../../types";
|
|
6
|
+
import { EntityNotFoundError } from "../../../errors";
|
|
7
|
+
|
|
8
|
+
export abstract class EntityTypeormRepository<DomainEntity extends Entity<EntityProps>, OrmModel extends ObjectLiteral>
|
|
9
|
+
implements Repository<Entity<EntityProps>>
|
|
10
|
+
{
|
|
11
|
+
protected manager: EntityManager;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
dataSource: DataSource,
|
|
15
|
+
private readonly entityName: string,
|
|
16
|
+
) {
|
|
17
|
+
this.manager = dataSource.manager;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async count(): Promise<number> {
|
|
21
|
+
return this.createDefaultQueryBuilder().getCount();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async exists(id: string): Promise<boolean> {
|
|
25
|
+
const count = await this.createDefaultQueryBuilder().where({ id }).getCount();
|
|
26
|
+
return count === 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async findOne(id: string): Promise<DomainEntity | null> {
|
|
30
|
+
const model = await this.createDefaultQueryBuilder().where({ id }).getOne();
|
|
31
|
+
return model ? this.toEntity(model) : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async findOneOrFail(entityId: string): Promise<DomainEntity> {
|
|
35
|
+
const entity = await this.findOne(entityId);
|
|
36
|
+
if (!entity) {
|
|
37
|
+
throw new EntityNotFoundError(
|
|
38
|
+
this.entityName,
|
|
39
|
+
{ [ArchitectureLevel.INFRASTRUCTURE]: { "persistence-sql": { className: "EntityTypeormRepository" } } },
|
|
40
|
+
{ id: entityId },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return entity;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async findByIds(ids: string[]): Promise<DomainEntity[]> {
|
|
47
|
+
const models = await this.createDefaultQueryBuilder().whereInIds(ids).getMany();
|
|
48
|
+
return models.map((model) => this.toEntity(model));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async save(entity: DomainEntity): Promise<DomainEntity> {
|
|
52
|
+
const model = await this.manager.save(await this.toModel(entity));
|
|
53
|
+
return this.toEntity(model);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async saveAll(entities: DomainEntity[]): Promise<void> {
|
|
57
|
+
const models = await Promise.all(entities.map((e) => Promise.resolve(this.toModel(e))));
|
|
58
|
+
await this.manager.save(models);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected abstract createDefaultQueryBuilder(): SelectQueryBuilder<OrmModel>;
|
|
62
|
+
protected abstract toEntity(model: OrmModel): DomainEntity;
|
|
63
|
+
protected abstract toModel(entity: DomainEntity): OrmModel | Promise<OrmModel>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
|
|
4
|
+
import { FindManyOptions, Repository } from "../../../application/persistence/repository";
|
|
5
|
+
import { AggregateRoot } from "../../../domain";
|
|
6
|
+
import { Entity, EntityProps } from "../../../domain/entities/entity";
|
|
7
|
+
import { ArchitectureLevel } from "../../../types";
|
|
8
|
+
import { EntityNotFoundError } from "../../../errors";
|
|
9
|
+
|
|
10
|
+
export class InMemoryRepository<DomainEntity extends Entity<EntityProps> | AggregateRoot<EntityProps>>
|
|
11
|
+
implements Repository<DomainEntity>
|
|
12
|
+
{
|
|
13
|
+
protected store: Map<string, DomainEntity>;
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
defaultEntities: DomainEntity[] = [],
|
|
17
|
+
protected error?: Error,
|
|
18
|
+
) {
|
|
19
|
+
this.store = new Map(defaultEntities.map((e) => [e.id.toString(), e]));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async save(entity: DomainEntity): Promise<DomainEntity> {
|
|
23
|
+
this.checkError();
|
|
24
|
+
this.store.set(entity.id.toString(), entity);
|
|
25
|
+
return entity;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async saveAll(entities: DomainEntity[]): Promise<void> {
|
|
29
|
+
this.checkError();
|
|
30
|
+
for (const entity of entities) {await this.save(entity);}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async findOne(id: string): Promise<DomainEntity | null> {
|
|
34
|
+
return this.store.get(id) ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async findOneOrFail(id: string): Promise<DomainEntity> {
|
|
38
|
+
const entity = await this.findOne(id);
|
|
39
|
+
if (!entity) {
|
|
40
|
+
throw new EntityNotFoundError("InMemoryEntity", {
|
|
41
|
+
[ArchitectureLevel.INFRASTRUCTURE]: { "persistence-fake": { className: "InMemoryRepository" } },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return entity;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async findByIds(ids: string[]): Promise<DomainEntity[]> {
|
|
48
|
+
return this.toArray().filter((entity) => ids.includes(entity.id.toString()));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async delete(entity: DomainEntity): Promise<void> {
|
|
52
|
+
this.checkError();
|
|
53
|
+
this.store.delete(entity.id.toString());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async exists(id: string): Promise<boolean> {
|
|
57
|
+
this.checkError();
|
|
58
|
+
return this.store.has(id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async count(options: FindManyOptions<DomainEntity>): Promise<number> {
|
|
62
|
+
this.checkError();
|
|
63
|
+
return _.filter(this.toArray(), options.filters).length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
clear(): void {
|
|
67
|
+
this.store.clear();
|
|
68
|
+
this.error = undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setError(error: Error): void {
|
|
72
|
+
this.error = error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected checkError(): void {
|
|
76
|
+
if (this.error) {throw this.error;}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected toArray(): DomainEntity[] {
|
|
80
|
+
return [...this.store.values()];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./typeorm";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ValueObject } from "../../../domain";
|
|
2
|
+
|
|
3
|
+
export interface OrmEmbeddedMapper<
|
|
4
|
+
DomainValueObject extends ValueObject,
|
|
5
|
+
OrmEmbeddedModel,
|
|
6
|
+
ToPersistenceArgs extends unknown[] = [],
|
|
7
|
+
ToDomainArgs extends unknown[] = [],
|
|
8
|
+
> {
|
|
9
|
+
toObjectValue(embeddedModel: OrmEmbeddedModel | null, ...args: ToDomainArgs): DomainValueObject | undefined;
|
|
10
|
+
toEmbeddedModel(valueObject?: DomainValueObject, ...args: ToPersistenceArgs): OrmEmbeddedModel | null;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AggregateRoot, Entity } from "../../../domain";
|
|
2
|
+
|
|
3
|
+
export interface OrmMapper<
|
|
4
|
+
DomainObject extends AggregateRoot | Entity,
|
|
5
|
+
OrmModel,
|
|
6
|
+
ToPersistenceArgs extends unknown[] = [],
|
|
7
|
+
ToDomainArgs extends unknown[] = [],
|
|
8
|
+
> {
|
|
9
|
+
toEntity(model: OrmModel, ...args: ToDomainArgs): DomainObject;
|
|
10
|
+
toModel(entity: DomainObject, ...args: ToPersistenceArgs): OrmModel;
|
|
11
|
+
}
|
package/src/core/infrastructure/persistence/write-side/typeorm/aggregate-typeorm.repository.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DataSource, EntityManager, ObjectLiteral, SelectQueryBuilder } from "typeorm";
|
|
2
|
+
|
|
3
|
+
import { Repository } from "../../../../application";
|
|
4
|
+
import { AggregateRoot, EntityProps } from "../../../../domain";
|
|
5
|
+
import { ArchitectureLevel } from "../../../../types";
|
|
6
|
+
import { EntityNotFoundError } from "../../../../errors";
|
|
7
|
+
import { typeormMetadataParser } from "../../../../../nestjs/errors/parsers/typeorm-metadata.parser";
|
|
8
|
+
import { PersistenceError } from "../../errors/persistence.error";
|
|
9
|
+
|
|
10
|
+
export abstract class AggregateTypeormRepository<
|
|
11
|
+
DomainAggregate extends AggregateRoot<EntityProps>,
|
|
12
|
+
OrmModel extends ObjectLiteral,
|
|
13
|
+
> implements Repository<AggregateRoot<EntityProps>>
|
|
14
|
+
{
|
|
15
|
+
protected abstract createDefaultQueryBuilder(): SelectQueryBuilder<OrmModel>;
|
|
16
|
+
protected abstract toEntity(model: OrmModel): DomainAggregate;
|
|
17
|
+
protected abstract toModel(aggregate: DomainAggregate): OrmModel | Promise<OrmModel>;
|
|
18
|
+
protected manager: EntityManager;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
dataSource: DataSource,
|
|
22
|
+
private readonly aggregateName: string,
|
|
23
|
+
) {
|
|
24
|
+
this.manager = dataSource.manager;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async count(): Promise<number> {
|
|
28
|
+
return this.createDefaultQueryBuilder().getCount();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async exists(id: string): Promise<boolean> {
|
|
32
|
+
const count = await this.createDefaultQueryBuilder().where({ id }).getCount();
|
|
33
|
+
return count === 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async findOne(id: string): Promise<DomainAggregate | null> {
|
|
37
|
+
const model = await this.createDefaultQueryBuilder().where({ id }).getOne();
|
|
38
|
+
return model ? this.toEntity(model) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async findOneOrFail(aggregateId: string): Promise<DomainAggregate> {
|
|
42
|
+
const aggregate = await this.findOne(aggregateId);
|
|
43
|
+
if (!aggregate) {
|
|
44
|
+
throw new EntityNotFoundError(
|
|
45
|
+
this.aggregateName,
|
|
46
|
+
{ [ArchitectureLevel.INFRASTRUCTURE]: { "persistence-sql": { className: "AggregateTypeormRepository" } } },
|
|
47
|
+
{ id: aggregateId },
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return aggregate;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async findByIds(ids: string[]): Promise<DomainAggregate[]> {
|
|
54
|
+
const models = await this.createDefaultQueryBuilder().whereInIds(ids).getMany();
|
|
55
|
+
return models.map((model) => this.toEntity(model));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async save(aggregate: DomainAggregate): Promise<DomainAggregate> {
|
|
59
|
+
try {
|
|
60
|
+
const model = await this.manager.save(this.toModel(aggregate));
|
|
61
|
+
aggregate.commit();
|
|
62
|
+
return this.toEntity(model);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new PersistenceError("Failed to save aggregate", {
|
|
65
|
+
"aggregate-root": {
|
|
66
|
+
className: AggregateTypeormRepository.name,
|
|
67
|
+
meta: typeormMetadataParser(error, { method: this.save.name }),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async saveAll(aggregates: DomainAggregate[]): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
const models = await Promise.all(aggregates.map((a) => Promise.resolve(this.toModel(a))));
|
|
76
|
+
await this.manager.save(models);
|
|
77
|
+
aggregates.forEach((a) => a.commit());
|
|
78
|
+
} catch (error) {
|
|
79
|
+
throw new PersistenceError("Failed to save aggregates", {
|
|
80
|
+
"aggregate-root": {
|
|
81
|
+
className: AggregateTypeormRepository.name,
|
|
82
|
+
meta: typeormMetadataParser(error, { method: this.saveAll.name }),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { DataSource, EntityManager, ObjectLiteral, SelectQueryBuilder } from "typeorm";
|
|
2
|
+
|
|
3
|
+
import { Repository } from "../../../../application";
|
|
4
|
+
import { Entity, EntityProps } from "../../../../domain";
|
|
5
|
+
import { ArchitectureLevel } from "../../../../types";
|
|
6
|
+
import { EntityNotFoundError } from "../../../../errors";
|
|
7
|
+
|
|
8
|
+
export abstract class EntityTypeormRepository<DomainEntity extends Entity<EntityProps>, OrmModel extends ObjectLiteral>
|
|
9
|
+
implements Repository<Entity<EntityProps>>
|
|
10
|
+
{
|
|
11
|
+
protected manager: EntityManager;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
dataSource: DataSource,
|
|
15
|
+
private readonly entityName: string,
|
|
16
|
+
) {
|
|
17
|
+
this.manager = dataSource.manager;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async count(): Promise<number> {
|
|
21
|
+
return this.createDefaultQueryBuilder().getCount();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async exists(id: string): Promise<boolean> {
|
|
25
|
+
const count = await this.createDefaultQueryBuilder().where({ id }).getCount();
|
|
26
|
+
return count === 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async findOne(id: string): Promise<DomainEntity | null> {
|
|
30
|
+
const model = await this.createDefaultQueryBuilder().where({ id }).getOne();
|
|
31
|
+
return model ? this.toEntity(model) : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async findOneOrFail(entityId: string): Promise<DomainEntity> {
|
|
35
|
+
const entity = await this.findOne(entityId);
|
|
36
|
+
if (!entity) {
|
|
37
|
+
throw new EntityNotFoundError(
|
|
38
|
+
this.entityName,
|
|
39
|
+
{ [ArchitectureLevel.INFRASTRUCTURE]: { "persistence-sql": { className: "EntityTypeormRepository" } } },
|
|
40
|
+
{ id: entityId },
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return entity;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async findByIds(ids: string[]): Promise<DomainEntity[]> {
|
|
47
|
+
const models = await this.createDefaultQueryBuilder().whereInIds(ids).getMany();
|
|
48
|
+
return models.map((model) => this.toEntity(model));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async save(entity: DomainEntity): Promise<DomainEntity> {
|
|
52
|
+
const model = await this.manager.save(await this.toModel(entity));
|
|
53
|
+
return this.toEntity(model);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async saveAll(entities: DomainEntity[]): Promise<void> {
|
|
57
|
+
const models = await Promise.all(entities.map((e) => Promise.resolve(this.toModel(e))));
|
|
58
|
+
await this.manager.save(models);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected abstract createDefaultQueryBuilder(): SelectQueryBuilder<OrmModel>;
|
|
62
|
+
protected abstract toEntity(model: OrmModel): DomainEntity;
|
|
63
|
+
protected abstract toModel(entity: DomainEntity): OrmModel | Promise<OrmModel>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ValueObject } from "../../../../domain";
|
|
2
|
+
|
|
3
|
+
export interface OrmEmbeddedMapper<
|
|
4
|
+
DomainValueObject extends ValueObject,
|
|
5
|
+
OrmEmbeddedModel,
|
|
6
|
+
ToPersistenceArgs extends unknown[] = [],
|
|
7
|
+
ToDomainArgs extends unknown[] = [],
|
|
8
|
+
> {
|
|
9
|
+
toObjectValue(embeddedModel: OrmEmbeddedModel | null, ...args: ToDomainArgs): DomainValueObject | undefined;
|
|
10
|
+
toEmbeddedModel(valueObject?: DomainValueObject, ...args: ToPersistenceArgs): OrmEmbeddedModel | null;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AggregateRoot, Entity } from "../../../../domain";
|
|
2
|
+
|
|
3
|
+
export interface OrmMapper<
|
|
4
|
+
DomainObject extends AggregateRoot | Entity,
|
|
5
|
+
OrmModel,
|
|
6
|
+
ToPersistenceArgs extends unknown[] = [],
|
|
7
|
+
ToDomainArgs extends unknown[] = [],
|
|
8
|
+
> {
|
|
9
|
+
toEntity(model: OrmModel, ...args: ToDomainArgs): DomainObject;
|
|
10
|
+
toModel(entity: DomainObject, ...args: ToPersistenceArgs): OrmModel;
|
|
11
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type Values<V> = V[keyof V];
|
|
2
|
+
type OneOf<V> = Values<{
|
|
3
|
+
[K in keyof V]: { [K1 in K]: V[K1] };
|
|
4
|
+
}>;
|
|
5
|
+
|
|
6
|
+
export enum ArchitectureLevel {
|
|
7
|
+
DOMAIN = "DOMAIN",
|
|
8
|
+
APPLICATION = "APPLICATION",
|
|
9
|
+
INFRASTRUCTURE = "INFRASTRUCTURE",
|
|
10
|
+
UNDEFINED = "UNDEFINED",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ArchitectureLayerType =
|
|
14
|
+
| "aggregate-root"
|
|
15
|
+
| "entity"
|
|
16
|
+
| "value-object"
|
|
17
|
+
| "query"
|
|
18
|
+
| "command"
|
|
19
|
+
| "event-handler"
|
|
20
|
+
| "event-saga"
|
|
21
|
+
| "service"
|
|
22
|
+
| "authorization"
|
|
23
|
+
| "authentication"
|
|
24
|
+
| "web-dto"
|
|
25
|
+
| "web-rest"
|
|
26
|
+
| "web-graphql"
|
|
27
|
+
| "web-socket"
|
|
28
|
+
| "web-rpc"
|
|
29
|
+
| "persistence-sql"
|
|
30
|
+
| "persistence-saas"
|
|
31
|
+
| "persistence-fake"
|
|
32
|
+
| "persistence-other"
|
|
33
|
+
| "persistence-s3"
|
|
34
|
+
| "persistence-disk"
|
|
35
|
+
| "messaging-inmem"
|
|
36
|
+
| "messaging-pubsub"
|
|
37
|
+
| "unknown";
|
|
38
|
+
|
|
39
|
+
export type ArchitectureLayerTraceBase = {
|
|
40
|
+
[Type in ArchitectureLayerType]: {
|
|
41
|
+
className: string;
|
|
42
|
+
meta?: unknown;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type ArchitectureLayerTrace = OneOf<ArchitectureLayerTraceBase>;
|
|
47
|
+
|
|
48
|
+
export type DDD = {
|
|
49
|
+
[key in `${ArchitectureLevel}`]: ArchitectureLayerTrace;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type ArchitectureLayer = OneOf<DDD>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Core
|
|
2
|
+
export * from "./core/domain";
|
|
3
|
+
export * from "./core/application";
|
|
4
|
+
export * from "./core/errors";
|
|
5
|
+
export * from "./core/types";
|
|
6
|
+
export * from "./core/infrastructure/persistence";
|
|
7
|
+
export * from "./core/infrastructure/messaging";
|
|
8
|
+
export * from "./core/infrastructure/config";
|
|
9
|
+
|
|
10
|
+
// Modules
|
|
11
|
+
export * from "./modules/config";
|
|
12
|
+
export * from "./modules/logger";
|
|
13
|
+
export * from "./modules/messaging";
|
|
14
|
+
export * from "./modules/queue";
|
|
15
|
+
export * from "./modules/knex";
|
|
16
|
+
export * from "./modules/graphql";
|
|
17
|
+
export * from "./modules/healthcheck";
|
|
18
|
+
|
|
19
|
+
// NestJS Integration
|
|
20
|
+
export * from "./nestjs/http";
|
|
21
|
+
export * from "./nestjs/errors";
|
|
22
|
+
|
|
23
|
+
// Validation
|
|
24
|
+
export * from "./validation";
|
|
25
|
+
|
|
26
|
+
// Utils
|
|
27
|
+
export * from "./utils";
|
|
28
|
+
|
|
29
|
+
// Testing
|
|
30
|
+
export * from "./testing";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
|
|
3
|
+
import { ConfigProvider, ConfigService } from "../../core/infrastructure/config";
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
providers: [{ provide: ConfigService, useClass: ConfigProvider }],
|
|
7
|
+
exports: [ConfigService],
|
|
8
|
+
})
|
|
9
|
+
export class ConfigModule {}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./paginated-response.object-type";
|