@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,15 @@
|
|
|
1
|
+
import { ArchitectureLevel } from "../types";
|
|
2
|
+
import { DetailedError } from "./detailed.error";
|
|
3
|
+
|
|
4
|
+
export class FakeImplementationError extends DetailedError {
|
|
5
|
+
constructor(className: string, implementationName: string) {
|
|
6
|
+
super(
|
|
7
|
+
`[${className}] This is a fake ${implementationName} method, either\n- You didn't stub the corresponding args in your test\n- OR you forgot to stub the method`,
|
|
8
|
+
{
|
|
9
|
+
[ArchitectureLevel.INFRASTRUCTURE]: {
|
|
10
|
+
"persistence-fake": { className },
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./command-failure.error";
|
|
2
|
+
export * from "./command-filter.error";
|
|
3
|
+
export * from "./detailed.error";
|
|
4
|
+
export * from "./domain.error";
|
|
5
|
+
export * from "./entity-not-found.error";
|
|
6
|
+
export * from "./fake-implementation.error";
|
|
7
|
+
export * from "./query-failure.error";
|
|
8
|
+
export * from "./too-many-results.error";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ArchitectureLevel } from "../types";
|
|
2
|
+
import { DetailedError } from "./detailed.error";
|
|
3
|
+
|
|
4
|
+
export class QueryFailureError extends DetailedError {
|
|
5
|
+
constructor(message: string, className: string, cause?: unknown) {
|
|
6
|
+
super(message, { [ArchitectureLevel.APPLICATION]: { query: { className } } }, cause);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common";
|
|
2
|
+
import * as dotenvSafe from "dotenv-safe";
|
|
3
|
+
|
|
4
|
+
import { ConfigService } from "./config.service";
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class ConfigProvider extends ConfigService {
|
|
8
|
+
private readonly dotenvValues: Record<string, string>;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.assertNodeEnv();
|
|
13
|
+
|
|
14
|
+
const envFilePath = this.getDotenvFileName();
|
|
15
|
+
if (!envFilePath) {
|
|
16
|
+
this.dotenvValues = {};
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const result = dotenvSafe.config({ allowEmptyValues: true, path: envFilePath });
|
|
21
|
+
if (result.error) {throw result.error;}
|
|
22
|
+
this.dotenvValues = result.parsed ?? {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get(key: string): string | undefined {
|
|
26
|
+
if (key in process.env) {return process.env[key];}
|
|
27
|
+
if (key in this.dotenvValues) {return this.dotenvValues[key];}
|
|
28
|
+
throw new Error(`Configuration key ${key} not found`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getUrl(key: string): string | undefined {
|
|
32
|
+
const url = this.get(key);
|
|
33
|
+
if (!url) {return undefined;}
|
|
34
|
+
if (!url.startsWith("http")) {return `https://${url}`;}
|
|
35
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getBoolean(key: string): boolean {
|
|
39
|
+
return this.get(key) === "true";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getNumber(key: string): number | undefined {
|
|
43
|
+
const value = this.get(key);
|
|
44
|
+
if (!value) {return undefined;}
|
|
45
|
+
const num = Number(value);
|
|
46
|
+
if (Number.isNaN(num)) {throw new Error(`Configuration key ${key} is not a number`);}
|
|
47
|
+
return num;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private get nodeEnv() {
|
|
51
|
+
return process.env["NODE_ENV"];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
isProduction() { return this.nodeEnv === "production"; }
|
|
55
|
+
isStaging() { return this.nodeEnv === "staging"; }
|
|
56
|
+
isSandbox() { return this.nodeEnv === "sandbox"; }
|
|
57
|
+
isDemo() { return this.nodeEnv === "demo"; }
|
|
58
|
+
isPreview() { return this.nodeEnv === "preview"; }
|
|
59
|
+
isTest() { return this.nodeEnv === "test"; }
|
|
60
|
+
isDevelopment() { return this.nodeEnv === "development"; }
|
|
61
|
+
|
|
62
|
+
useFake() {
|
|
63
|
+
return !this.isProduction() && !this.isStaging() && !this.isSandbox() && !this.isDemo();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getDotenvFileName() {
|
|
67
|
+
if (this.isTest()) {return ".env.test";}
|
|
68
|
+
if (process.env["CI"] === "true") {return ".env.ci";}
|
|
69
|
+
if (this.isProduction() || this.isStaging() || this.isPreview()) {return undefined;}
|
|
70
|
+
return `.env.${this.nodeEnv ?? "development"}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
assertNodeEnv() {
|
|
74
|
+
if (!this.nodeEnv) {
|
|
75
|
+
throw new Error("System variable NODE_ENV should be defined before reading env file");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export abstract class ConfigService {
|
|
2
|
+
abstract get(key: string): string | undefined;
|
|
3
|
+
|
|
4
|
+
abstract getUrl(key: string): string | undefined;
|
|
5
|
+
|
|
6
|
+
abstract getBoolean(key: string): boolean;
|
|
7
|
+
|
|
8
|
+
abstract getNumber(key: string): number | undefined;
|
|
9
|
+
|
|
10
|
+
abstract isProduction(): boolean;
|
|
11
|
+
|
|
12
|
+
abstract isStaging(): boolean;
|
|
13
|
+
|
|
14
|
+
abstract isSandbox(): boolean;
|
|
15
|
+
|
|
16
|
+
abstract isDemo(): boolean;
|
|
17
|
+
|
|
18
|
+
abstract useFake(): boolean;
|
|
19
|
+
|
|
20
|
+
abstract isPreview(): boolean;
|
|
21
|
+
|
|
22
|
+
abstract isTest(): boolean;
|
|
23
|
+
|
|
24
|
+
abstract isDevelopment(): boolean;
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MessagingService } from "../messaging.service";
|
|
2
|
+
|
|
3
|
+
export class FakeMessagingService implements MessagingService {
|
|
4
|
+
private emittedMessages: Record<"pattern" | "payload", unknown>[] = [];
|
|
5
|
+
|
|
6
|
+
emit<TPayload>(pattern: string, payload: TPayload) {
|
|
7
|
+
this.emittedMessages.push({ pattern, payload });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getEmittedCount() {
|
|
11
|
+
return this.emittedMessages.length;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
reset() {
|
|
15
|
+
this.emittedMessages = [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { QueueManagerService } from "../queue-manager.service";
|
|
2
|
+
import { QueueMessagingService } from "../queue-messaging.service";
|
|
3
|
+
import { FakeQueueMessagingService } from "./fake-queue-messaging.service";
|
|
4
|
+
|
|
5
|
+
export class FakeQueueManagerService implements QueueManagerService {
|
|
6
|
+
getMessagingService(): QueueMessagingService {
|
|
7
|
+
return new FakeQueueMessagingService();
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { QueueMessagingService } from "../queue-messaging.service";
|
|
2
|
+
|
|
3
|
+
export class FakeQueueMessagingService implements QueueMessagingService {
|
|
4
|
+
private emittedMessages: Record<"pattern" | "payload", unknown>[] = [];
|
|
5
|
+
|
|
6
|
+
publish<TPayload>(pattern: string, payload: TPayload) {
|
|
7
|
+
this.emittedMessages.push({ pattern, payload });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ClientProxy } from "@nestjs/microservices";
|
|
2
|
+
|
|
3
|
+
import { MessagingService } from "../messaging.service";
|
|
4
|
+
|
|
5
|
+
export class RabbitMessagingService implements MessagingService {
|
|
6
|
+
constructor(private readonly eventBus: ClientProxy) {}
|
|
7
|
+
|
|
8
|
+
emit<TPayload>(pattern: string, payload: TPayload) {
|
|
9
|
+
this.eventBus.emit<void, TPayload>(pattern, payload);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ClientProxy } from "@nestjs/microservices";
|
|
2
|
+
|
|
3
|
+
import { QueueMessagingService } from "../queue-messaging.service";
|
|
4
|
+
|
|
5
|
+
export class RabbitQueueMessagingService implements QueueMessagingService {
|
|
6
|
+
constructor(private readonly clientProxy: ClientProxy) {}
|
|
7
|
+
|
|
8
|
+
publish<TPayload>(pattern: string, payload: TPayload) {
|
|
9
|
+
this.clientProxy.emit<void, TPayload>(pattern, payload);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { InjectionToken, ModuleMetadata, OptionalFactoryDependency } from "@nestjs/common";
|
|
2
|
+
|
|
3
|
+
export enum QueueName {
|
|
4
|
+
COMMUNICATION = "COMMUNICATION",
|
|
5
|
+
HARBOR_FEEDBACK = "HARBOR_FEEDBACK",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const QUEUE_NAME_RESOLVER = Symbol();
|
|
9
|
+
export type QueueNameResolver = (queueName: QueueName) => string;
|
|
10
|
+
|
|
11
|
+
export const CLIENT_PROXY_OPTIONS = Symbol("CLIENT_PROXY_OPTIONS");
|
|
12
|
+
export type ClientProxyOptions = {
|
|
13
|
+
urls: string[] | Record<string, unknown>[];
|
|
14
|
+
queueOptions?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const QUEUE_CORE_OPTIONS_TOKEN = Symbol("QUEUE_CORE_OPTIONS_TOKEN");
|
|
18
|
+
|
|
19
|
+
export interface QueueModuleOptions {
|
|
20
|
+
clientProxyOptions: ClientProxyOptions;
|
|
21
|
+
queueNameResolver?: QueueNameResolver;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface QueueModuleAsyncOptions extends Pick<ModuleMetadata, "imports"> {
|
|
25
|
+
inject: (InjectionToken | OptionalFactoryDependency)[];
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
useFactory: (...args: any[]) => Promise<QueueModuleOptions> | QueueModuleOptions;
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { PersistenceError } from "./persistence.error";
|
|
2
|
+
|
|
3
|
+
export class ModelToEntityConversionError<Model> extends PersistenceError {
|
|
4
|
+
constructor(className: string, model: Model, cause: unknown) {
|
|
5
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
6
|
+
super(`ORM Model ${typeof model} cannot be converted to Entity: ${message}`, { "persistence-sql": { className } });
|
|
7
|
+
this.setDetails({ model });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ArchitectureLayerTrace, ArchitectureLevel } from "../../../types";
|
|
2
|
+
import { DetailedError } from "../../../errors";
|
|
3
|
+
|
|
4
|
+
export class PersistenceError extends DetailedError {
|
|
5
|
+
constructor(message: string, architectureTrace: ArchitectureLayerTrace, cause?: unknown) {
|
|
6
|
+
super(message, { [ArchitectureLevel.INFRASTRUCTURE]: { ...architectureTrace } }, cause);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
|
|
4
|
+
import { ArchitectureLevel } from "../../../types";
|
|
5
|
+
import { Entity, EntityProps } from "../../../domain/entities/entity";
|
|
6
|
+
import { EntityNotFoundError } from "../../../errors";
|
|
7
|
+
import { FindManyOptions, Repository } from "../../../application/persistence/repository";
|
|
8
|
+
|
|
9
|
+
export class FakeRepository<DomainEntity extends Entity<EntityProps>> implements Repository<DomainEntity> {
|
|
10
|
+
protected store: Map<string, DomainEntity>;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
defaultEntities: DomainEntity[] = [],
|
|
14
|
+
protected error?: Error,
|
|
15
|
+
) {
|
|
16
|
+
this.store = new Map(defaultEntities.map((e) => [e.id.toString(), e]));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async save(entity: DomainEntity): Promise<DomainEntity> {
|
|
20
|
+
this.checkError();
|
|
21
|
+
this.store.set(entity.id.toString(), entity);
|
|
22
|
+
return entity;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async saveAll(entities: DomainEntity[]): Promise<void> {
|
|
26
|
+
this.checkError();
|
|
27
|
+
for (const entity of entities) {await this.save(entity);}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async findOne(id: string): Promise<DomainEntity | null> {
|
|
31
|
+
return this.store.get(id) ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async findOneOrFail(id: string): Promise<DomainEntity> {
|
|
35
|
+
const entity = await this.findOne(id);
|
|
36
|
+
if (!entity) {
|
|
37
|
+
throw new EntityNotFoundError("FakeEntity", {
|
|
38
|
+
[ArchitectureLevel.INFRASTRUCTURE]: { "persistence-fake": { className: "FakeRepository" } },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return entity;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async findByIds(ids: string[]): Promise<DomainEntity[]> {
|
|
45
|
+
return this.toArray().filter((entity) => ids.includes(entity.id.toString()));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async delete(entity: DomainEntity): Promise<void> {
|
|
49
|
+
this.checkError();
|
|
50
|
+
this.store.delete(entity.id.toString());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async exists(id: string): Promise<boolean> {
|
|
54
|
+
this.checkError();
|
|
55
|
+
return this.store.has(id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async count(options: FindManyOptions<DomainEntity>): Promise<number> {
|
|
59
|
+
this.checkError();
|
|
60
|
+
return _.filter(this.toArray(), options.filters).length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clear(): void {
|
|
64
|
+
this.store.clear();
|
|
65
|
+
this.error = undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setError(error: Error): void {
|
|
69
|
+
this.error = error;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected checkError(): void {
|
|
73
|
+
if (this.error) {throw this.error;}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
protected toArray(): DomainEntity[] {
|
|
77
|
+
return [...this.store.values()];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import { Entity, EntityProps } from "../../../domain/entities/entity";
|
|
4
|
+
import { UniqueEntityId } from "../../../domain/entities/unique-entity-id";
|
|
5
|
+
import { EntityNotFoundError } from "../../../errors";
|
|
6
|
+
import { InMemoryRepository } from "./in-memory.repository";
|
|
7
|
+
|
|
8
|
+
interface ItemProps extends EntityProps { name: string }
|
|
9
|
+
class Item extends Entity<ItemProps> {}
|
|
10
|
+
|
|
11
|
+
describe("InMemoryRepository", () => {
|
|
12
|
+
let repo: InMemoryRepository<Item>;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => { repo = new InMemoryRepository<Item>(); });
|
|
15
|
+
|
|
16
|
+
it("should save and find an entity", async () => {
|
|
17
|
+
const item = new Item({ name: "a", id: new UniqueEntityId("1") });
|
|
18
|
+
await repo.save(item);
|
|
19
|
+
expect(await repo.findOne("1")).toBe(item);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should return null for missing entity", async () => {
|
|
23
|
+
expect(await repo.findOne("missing")).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should throw on findOneOrFail for missing entity", async () => {
|
|
27
|
+
await expect(repo.findOneOrFail("x")).rejects.toThrow(EntityNotFoundError);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should delete an entity", async () => {
|
|
31
|
+
const item = new Item({ name: "a", id: new UniqueEntityId("1") });
|
|
32
|
+
await repo.save(item);
|
|
33
|
+
await repo.delete(item);
|
|
34
|
+
expect(await repo.exists("1")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should find entities by ids", async () => {
|
|
38
|
+
const a = new Item({ name: "a", id: new UniqueEntityId("1") });
|
|
39
|
+
const b = new Item({ name: "b", id: new UniqueEntityId("2") });
|
|
40
|
+
await repo.saveAll([a, b]);
|
|
41
|
+
const result = await repo.findByIds(["1"]);
|
|
42
|
+
expect(result).toEqual([a]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should throw stored error on save", async () => {
|
|
46
|
+
repo.setError(new Error("boom"));
|
|
47
|
+
const item = new Item({ name: "a" });
|
|
48
|
+
await expect(repo.save(item)).rejects.toThrow("boom");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
|
|
4
|
+
import { FindManyOptions, Repository } from "../../../application/persistence/repository";
|
|
5
|
+
import { AggregateRoot, Entity, EntityProps } from "../../../domain";
|
|
6
|
+
import { ArchitectureLevel } from "../../../types";
|
|
7
|
+
import { EntityNotFoundError } from "../../../errors";
|
|
8
|
+
|
|
9
|
+
export class InMemoryRepository<DomainEntity extends Entity<EntityProps> | AggregateRoot<EntityProps>>
|
|
10
|
+
implements Repository<DomainEntity>
|
|
11
|
+
{
|
|
12
|
+
protected store: Map<string, DomainEntity>;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
defaultEntities: DomainEntity[] = [],
|
|
16
|
+
protected error?: Error,
|
|
17
|
+
) {
|
|
18
|
+
this.store = new Map(defaultEntities.map((e) => [e.id.toString(), e]));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async save(entity: DomainEntity): Promise<DomainEntity> {
|
|
22
|
+
this.checkError();
|
|
23
|
+
this.store.set(entity.id.toString(), entity);
|
|
24
|
+
return entity;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async saveAll(entities: DomainEntity[]): Promise<void> {
|
|
28
|
+
this.checkError();
|
|
29
|
+
for (const entity of entities) {await this.save(entity);}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async findOne(id: string): Promise<DomainEntity | null> {
|
|
33
|
+
return this.store.get(id) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async findOneOrFail(id: string): Promise<DomainEntity> {
|
|
37
|
+
const entity = await this.findOne(id);
|
|
38
|
+
if (!entity) {
|
|
39
|
+
throw new EntityNotFoundError("InMemoryEntity", {
|
|
40
|
+
[ArchitectureLevel.INFRASTRUCTURE]: { "persistence-fake": { className: "InMemoryRepository" } },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return entity;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async findByIds(ids: string[]): Promise<DomainEntity[]> {
|
|
47
|
+
return this.toArray().filter((entity) => ids.includes(entity.id.toString()));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async delete(entity: DomainEntity): Promise<void> {
|
|
51
|
+
this.checkError();
|
|
52
|
+
this.store.delete(entity.id.toString());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async exists(id: string): Promise<boolean> {
|
|
56
|
+
this.checkError();
|
|
57
|
+
return this.store.has(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async count(options: FindManyOptions<DomainEntity>): Promise<number> {
|
|
61
|
+
this.checkError();
|
|
62
|
+
return _.filter(this.toArray(), options.filters).length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
clear(): void {
|
|
66
|
+
this.store.clear();
|
|
67
|
+
this.error = undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setError(error: Error): void {
|
|
71
|
+
this.error = error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected checkError(): void {
|
|
75
|
+
if (this.error) {throw this.error;}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected toArray(): DomainEntity[] {
|
|
79
|
+
return [...this.store.values()];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./knex";
|
|
@@ -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,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
|
+
}
|