@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.
Files changed (214) hide show
  1. package/.pnp.cjs +22192 -0
  2. package/.pnp.loader.mjs +2126 -0
  3. package/.yarnrc.yml +1 -0
  4. package/CHANGELOG.md +527 -0
  5. package/README.md +3 -0
  6. package/eslint.config.mjs +52 -0
  7. package/invariant.json +22 -0
  8. package/jest/jest.config.base.ts +30 -0
  9. package/jest/jest.spec.base.ts +7 -0
  10. package/jest.config.js +49 -0
  11. package/package.json +99 -0
  12. package/src/core/application/index.ts +1 -0
  13. package/src/core/application/persistence/index.ts +3 -0
  14. package/src/core/application/persistence/query-builder.ts +2 -0
  15. package/src/core/application/persistence/read-projection.ts +2 -0
  16. package/src/core/application/persistence/repository.ts +23 -0
  17. package/src/core/domain/entities/aggregate-root.ts +34 -0
  18. package/src/core/domain/entities/entity.spec.ts +43 -0
  19. package/src/core/domain/entities/entity.ts +29 -0
  20. package/src/core/domain/entities/identifier.spec.ts +34 -0
  21. package/src/core/domain/entities/identifier.ts +16 -0
  22. package/src/core/domain/entities/index.ts +5 -0
  23. package/src/core/domain/entities/projection.ts +7 -0
  24. package/src/core/domain/entities/unique-entity-id.ts +9 -0
  25. package/src/core/domain/events/domain-event.ts +7 -0
  26. package/src/core/domain/events/index.ts +1 -0
  27. package/src/core/domain/index.ts +3 -0
  28. package/src/core/domain/value-objects/index.ts +2 -0
  29. package/src/core/domain/value-objects/range.ts +4 -0
  30. package/src/core/domain/value-objects/value-object.spec.ts +45 -0
  31. package/src/core/domain/value-objects/value-object.ts +17 -0
  32. package/src/core/errors/command-failure.error.spec.ts +30 -0
  33. package/src/core/errors/command-failure.error.ts +9 -0
  34. package/src/core/errors/command-filter.error.ts +3 -0
  35. package/src/core/errors/detailed.error.ts +25 -0
  36. package/src/core/errors/domain.error.spec.ts +27 -0
  37. package/src/core/errors/domain.error.ts +9 -0
  38. package/src/core/errors/entity-not-found.error.spec.ts +32 -0
  39. package/src/core/errors/entity-not-found.error.ts +9 -0
  40. package/src/core/errors/fake-implementation.error.spec.ts +27 -0
  41. package/src/core/errors/fake-implementation.error.ts +15 -0
  42. package/src/core/errors/index.ts +8 -0
  43. package/src/core/errors/query-failure.error.ts +8 -0
  44. package/src/core/errors/too-many-results.error.ts +3 -0
  45. package/src/core/index.ts +4 -0
  46. package/src/core/infrastructure/config/config.provider.ts +78 -0
  47. package/src/core/infrastructure/config/config.service.ts +25 -0
  48. package/src/core/infrastructure/config/index.ts +2 -0
  49. package/src/core/infrastructure/messaging/fake/fake-messaging.service.ts +17 -0
  50. package/src/core/infrastructure/messaging/fake/fake-queue-manager.service.ts +9 -0
  51. package/src/core/infrastructure/messaging/fake/fake-queue-messaging.service.ts +9 -0
  52. package/src/core/infrastructure/messaging/fake/index.ts +3 -0
  53. package/src/core/infrastructure/messaging/index.ts +6 -0
  54. package/src/core/infrastructure/messaging/messaging.service.ts +3 -0
  55. package/src/core/infrastructure/messaging/queue-manager.service.ts +6 -0
  56. package/src/core/infrastructure/messaging/queue-messaging.service.ts +3 -0
  57. package/src/core/infrastructure/messaging/rabbitmq/index.ts +2 -0
  58. package/src/core/infrastructure/messaging/rabbitmq/rabbit-messaging.service.ts +11 -0
  59. package/src/core/infrastructure/messaging/rabbitmq/rabbit-queue-messaging.service.ts +11 -0
  60. package/src/core/infrastructure/messaging/types.ts +28 -0
  61. package/src/core/infrastructure/persistence/errors/index.ts +2 -0
  62. package/src/core/infrastructure/persistence/errors/model-to-entity-conversion.error.ts +9 -0
  63. package/src/core/infrastructure/persistence/errors/persistence.error.ts +8 -0
  64. package/src/core/infrastructure/persistence/in-memory/fake.repository.ts +79 -0
  65. package/src/core/infrastructure/persistence/in-memory/in-memory.query-builder.ts +4 -0
  66. package/src/core/infrastructure/persistence/in-memory/in-memory.repository.spec.ts +50 -0
  67. package/src/core/infrastructure/persistence/in-memory/in-memory.repository.ts +81 -0
  68. package/src/core/infrastructure/persistence/in-memory/index.ts +3 -0
  69. package/src/core/infrastructure/persistence/index.ts +4 -0
  70. package/src/core/infrastructure/persistence/read-side/in-memory.query-builder.ts +4 -0
  71. package/src/core/infrastructure/persistence/read-side/index.ts +1 -0
  72. package/src/core/infrastructure/persistence/read-side/knex/index.ts +2 -0
  73. package/src/core/infrastructure/persistence/read-side/knex/knex-types.definition.ts +13 -0
  74. package/src/core/infrastructure/persistence/read-side/knex/knex.query-builder.ts +70 -0
  75. package/src/core/infrastructure/persistence/read-side/knex-query-builder.ts +70 -0
  76. package/src/core/infrastructure/persistence/read-side/knex-types.definition.ts +13 -0
  77. package/src/core/infrastructure/persistence/write-side/aggregate-typeorm-repository.ts +87 -0
  78. package/src/core/infrastructure/persistence/write-side/entity-typeorm-repository.ts +64 -0
  79. package/src/core/infrastructure/persistence/write-side/in-memory.repository.ts +82 -0
  80. package/src/core/infrastructure/persistence/write-side/index.ts +1 -0
  81. package/src/core/infrastructure/persistence/write-side/model-attributes.ts +4 -0
  82. package/src/core/infrastructure/persistence/write-side/orm-embedded-mapper.ts +11 -0
  83. package/src/core/infrastructure/persistence/write-side/orm-mapper.ts +11 -0
  84. package/src/core/infrastructure/persistence/write-side/typeorm/aggregate-typeorm.repository.ts +87 -0
  85. package/src/core/infrastructure/persistence/write-side/typeorm/entity-typeorm.repository.ts +64 -0
  86. package/src/core/infrastructure/persistence/write-side/typeorm/index.ts +5 -0
  87. package/src/core/infrastructure/persistence/write-side/typeorm/model-attributes.ts +4 -0
  88. package/src/core/infrastructure/persistence/write-side/typeorm/orm-embedded.mapper.ts +11 -0
  89. package/src/core/infrastructure/persistence/write-side/typeorm/orm.mapper.ts +11 -0
  90. package/src/core/types/architecture-layer.ts +52 -0
  91. package/src/core/types/array-element.ts +5 -0
  92. package/src/core/types/index.ts +2 -0
  93. package/src/index.ts +30 -0
  94. package/src/modules/config/config.module.ts +9 -0
  95. package/src/modules/config/index.ts +2 -0
  96. package/src/modules/graphql/index.ts +1 -0
  97. package/src/modules/graphql/paginated-response.object-type.ts +23 -0
  98. package/src/modules/healthcheck/healthcheck-out.dto.ts +43 -0
  99. package/src/modules/healthcheck/index.ts +1 -0
  100. package/src/modules/knex/index.ts +3 -0
  101. package/src/modules/knex/knex-core.module.ts +30 -0
  102. package/src/modules/knex/knex.decorator.ts +5 -0
  103. package/src/modules/knex/knex.interface.ts +12 -0
  104. package/src/modules/knex/knex.module.ts +14 -0
  105. package/src/modules/knex/knex.token.ts +2 -0
  106. package/src/modules/logger/app-logger.ts +28 -0
  107. package/src/modules/logger/index.ts +5 -0
  108. package/src/modules/logger/log.ts +26 -0
  109. package/src/modules/logger/logger.module.ts +47 -0
  110. package/src/modules/logger/logger.service.ts +131 -0
  111. package/src/modules/logger/transports/console-color.transport.ts +41 -0
  112. package/src/modules/logger/transports/console-json.transport.ts +23 -0
  113. package/src/modules/logger/transports/console.transport.ts +10 -0
  114. package/src/modules/logger/transports/fake.transport.ts +18 -0
  115. package/src/modules/logger/transports/index.ts +5 -0
  116. package/src/modules/logger/transports/logger-transport.ts +22 -0
  117. package/src/modules/messaging/index.ts +2 -0
  118. package/src/modules/messaging/messaging.module.ts +92 -0
  119. package/src/modules/queue/default-queue-name.resolver.ts +5 -0
  120. package/src/modules/queue/index.ts +3 -0
  121. package/src/modules/queue/queue.module.ts +73 -0
  122. package/src/modules/queue/rabbit-queue-manager.service.ts +67 -0
  123. package/src/nestjs/errors/handlers/error-handler.ts +38 -0
  124. package/src/nestjs/errors/handlers/error-handler.type.ts +23 -0
  125. package/src/nestjs/errors/handlers/generic-error-handler.ts +59 -0
  126. package/src/nestjs/errors/handlers/handle-errors.decorator.ts +43 -0
  127. package/src/nestjs/errors/handlers/index.ts +5 -0
  128. package/src/nestjs/errors/handlers/validation-error-handler.ts +46 -0
  129. package/src/nestjs/errors/index.ts +2 -0
  130. package/src/nestjs/errors/parsers/axios-metadata.parser.ts +21 -0
  131. package/src/nestjs/errors/parsers/error-metadata.definition.ts +1 -0
  132. package/src/nestjs/errors/parsers/index.ts +5 -0
  133. package/src/nestjs/errors/parsers/knex-metadata.parser.ts +18 -0
  134. package/src/nestjs/errors/parsers/typeorm-metadata.parser.ts +19 -0
  135. package/src/nestjs/errors/parsers/workos-metadata.parser.ts +17 -0
  136. package/src/nestjs/http/decorators/index.ts +1 -0
  137. package/src/nestjs/http/decorators/log-app-ctx.decorator.ts +32 -0
  138. package/src/nestjs/http/dtos/date-range.dto.ts +15 -0
  139. package/src/nestjs/http/dtos/index.ts +1 -0
  140. package/src/nestjs/http/filters/all-exceptions.filter.ts +92 -0
  141. package/src/nestjs/http/filters/command-failure.filter.ts +12 -0
  142. package/src/nestjs/http/filters/index.ts +4 -0
  143. package/src/nestjs/http/filters/rpc-exceptions.filter.ts +10 -0
  144. package/src/nestjs/http/filters/too-many-results.filter.ts +12 -0
  145. package/src/nestjs/http/index.ts +6 -0
  146. package/src/nestjs/http/interceptors/index.ts +2 -0
  147. package/src/nestjs/http/interceptors/log-app-ctx.interceptor.ts +109 -0
  148. package/src/nestjs/http/interceptors/serialize-output.interceptor.ts +19 -0
  149. package/src/nestjs/http/middleware/http-logger.middleware.ts +31 -0
  150. package/src/nestjs/http/middleware/index.ts +2 -0
  151. package/src/nestjs/http/middleware/logger.middleware.ts +21 -0
  152. package/src/nestjs/http/swagger/index.ts +1 -0
  153. package/src/nestjs/http/swagger/swagger.ts +44 -0
  154. package/src/nestjs/index.ts +2 -0
  155. package/src/testing/command-bus.stub.ts +33 -0
  156. package/src/testing/event-bus.stub.ts +56 -0
  157. package/src/testing/event-publisher.stub.ts +24 -0
  158. package/src/testing/fake-logger.ts +20 -0
  159. package/src/testing/index.ts +2 -0
  160. package/src/testing/query-bus.stub.ts +27 -0
  161. package/src/testing/stub.spec.ts +250 -0
  162. package/src/testing/stub.ts +170 -0
  163. package/src/testing/stubs/command-bus.stub.ts +33 -0
  164. package/src/testing/stubs/event-bus.stub.ts +56 -0
  165. package/src/testing/stubs/event-publisher.stub.ts +24 -0
  166. package/src/testing/stubs/index.ts +5 -0
  167. package/src/testing/stubs/query-bus.stub.ts +27 -0
  168. package/src/testing/stubs/stub.ts +170 -0
  169. package/src/utils/array.spec.ts +29 -0
  170. package/src/utils/array.ts +10 -0
  171. package/src/utils/base64.spec.ts +18 -0
  172. package/src/utils/base64.ts +6 -0
  173. package/src/utils/collection.ts +4 -0
  174. package/src/utils/common.ts +13 -0
  175. package/src/utils/csv.ts +49 -0
  176. package/src/utils/date/date-range.ts +4 -0
  177. package/src/utils/date/date.spec.ts +100 -0
  178. package/src/utils/date/date.ts +177 -0
  179. package/src/utils/date/diff.spec.ts +66 -0
  180. package/src/utils/date/diffYear.spec.ts +23 -0
  181. package/src/utils/date/fillMissingRangeValues.spec.ts +523 -0
  182. package/src/utils/date/groubBy.spec.ts +183 -0
  183. package/src/utils/date/index.ts +2 -0
  184. package/src/utils/date/isSame.spec.ts +111 -0
  185. package/src/utils/file.spec.ts +66 -0
  186. package/src/utils/file.ts +5 -0
  187. package/src/utils/hash-key.ts +23 -0
  188. package/src/utils/index.ts +14 -0
  189. package/src/utils/invariant.ts +3 -0
  190. package/src/utils/iso-date.ts +11 -0
  191. package/src/utils/object.spec.ts +18 -0
  192. package/src/utils/object.ts +6 -0
  193. package/src/utils/paginated.ts +36 -0
  194. package/src/utils/string.spec.ts +10 -0
  195. package/src/utils/string.ts +19 -0
  196. package/src/utils/type.ts +9 -0
  197. package/src/utils/xml.ts +6 -0
  198. package/src/validation/ensure-array.decorator.ts +5 -0
  199. package/src/validation/index.ts +7 -0
  200. package/src/validation/is-iso-date.decorator.spec.ts +29 -0
  201. package/src/validation/is-iso-date.decorator.ts +30 -0
  202. package/src/validation/is-less-than-or-equal.decorator.spec.ts +30 -0
  203. package/src/validation/is-less-than-or-equal.decorator.ts +52 -0
  204. package/src/validation/is-less-than.decorator.spec.ts +36 -0
  205. package/src/validation/is-less-than.decorator.ts +52 -0
  206. package/src/validation/is-more-than-or-equal.decorator.spec.ts +30 -0
  207. package/src/validation/is-more-than-or-equal.decorator.ts +52 -0
  208. package/src/validation/is-more-than.decorator.spec.ts +36 -0
  209. package/src/validation/is-more-than.decorator.ts +52 -0
  210. package/src/validation/is-time-string.decorator.spec.ts +35 -0
  211. package/src/validation/is-time-string.decorator.ts +29 -0
  212. package/tsconfig.build.json +6 -0
  213. package/tsconfig.json +34 -0
  214. package/tsconfig.spec.json +14 -0
@@ -0,0 +1,73 @@
1
+ import { DynamicModule, Global, Module } from "@nestjs/common";
2
+
3
+ import {
4
+ QueueManagerService,
5
+ FakeQueueManagerService,
6
+ CLIENT_PROXY_OPTIONS,
7
+ ClientProxyOptions,
8
+ QUEUE_CORE_OPTIONS_TOKEN,
9
+ QUEUE_NAME_RESOLVER,
10
+ QueueModuleAsyncOptions,
11
+ QueueModuleOptions,
12
+ QueueNameResolver,
13
+ } from "../../core/infrastructure/messaging";
14
+ import { ConfigModule, ConfigService } from "../config";
15
+ import { LoggerModule, LoggerService } from "../logger";
16
+ import { defaultQueueNameResolverFactory } from "./default-queue-name.resolver";
17
+ import { RabbitQueueManagerService } from "./rabbit-queue-manager.service";
18
+
19
+ @Module({})
20
+ @Global()
21
+ export class QueueModule {
22
+ static forRootAsync(options?: QueueModuleAsyncOptions): DynamicModule {
23
+ return {
24
+ module: QueueModule,
25
+ imports: [QueueCoreModule.forRootAsync(options)],
26
+ exports: [QueueCoreModule],
27
+ };
28
+ }
29
+ }
30
+
31
+ @Module({})
32
+ class QueueCoreModule {
33
+ static forRootAsync(options?: QueueModuleAsyncOptions): DynamicModule {
34
+ return {
35
+ module: QueueCoreModule,
36
+ imports: [...(options?.imports ?? []), ConfigModule, LoggerModule],
37
+ providers: [
38
+ {
39
+ provide: QUEUE_NAME_RESOLVER,
40
+ useFactory: (configService: ConfigService, option?: QueueModuleOptions) =>
41
+ option?.queueNameResolver ?? defaultQueueNameResolverFactory(configService),
42
+ inject: [ConfigService, { token: QUEUE_CORE_OPTIONS_TOKEN, optional: true }],
43
+ },
44
+ {
45
+ provide: CLIENT_PROXY_OPTIONS,
46
+ useFactory: (option?: QueueModuleOptions) => option?.clientProxyOptions,
47
+ inject: [QUEUE_CORE_OPTIONS_TOKEN],
48
+ },
49
+ ...(options
50
+ ? [{
51
+ provide: QUEUE_CORE_OPTIONS_TOKEN,
52
+ useFactory: options.useFactory,
53
+ inject: options.inject ?? [],
54
+ }]
55
+ : []),
56
+ {
57
+ provide: QueueManagerService,
58
+ useFactory: (
59
+ queueNameResolver: QueueNameResolver,
60
+ clientProxyOptions: ClientProxyOptions,
61
+ configService: ConfigService,
62
+ logger: LoggerService,
63
+ ) =>
64
+ configService.isTest()
65
+ ? new FakeQueueManagerService()
66
+ : new RabbitQueueManagerService(queueNameResolver, clientProxyOptions, logger),
67
+ inject: [QUEUE_NAME_RESOLVER, CLIENT_PROXY_OPTIONS, ConfigService, LoggerService],
68
+ },
69
+ ],
70
+ exports: [QueueManagerService],
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,67 @@
1
+ import { Inject, Injectable, OnModuleDestroy } from "@nestjs/common";
2
+ import { ClientProxy, ClientProxyFactory, Transport } from "@nestjs/microservices";
3
+
4
+ import {
5
+ QueueMessagingService,
6
+ QueueManagerService,
7
+ CLIENT_PROXY_OPTIONS,
8
+ ClientProxyOptions,
9
+ QUEUE_NAME_RESOLVER,
10
+ QueueName,
11
+ QueueNameResolver,
12
+ } from "../../core/infrastructure/messaging";
13
+ import { RabbitQueueMessagingService } from "../../core/infrastructure/messaging/rabbitmq/rabbit-queue-messaging.service";
14
+ import { LoggerService } from "../logger";
15
+
16
+ @Injectable()
17
+ export class RabbitQueueManagerService implements QueueManagerService, OnModuleDestroy {
18
+ private readonly queueMessagingServiceMap = new Map<QueueName, QueueMessagingService>();
19
+ private readonly clientProxyMap = new Map<QueueName, ClientProxy>();
20
+
21
+ constructor(
22
+ @Inject(QUEUE_NAME_RESOLVER) private readonly queueNameResolver: QueueNameResolver,
23
+ @Inject(CLIENT_PROXY_OPTIONS) private readonly clientProxyOptions: ClientProxyOptions,
24
+ private readonly logger: LoggerService,
25
+ ) {}
26
+
27
+ async onModuleDestroy() {
28
+ for (const [queueName, clientProxy] of this.clientProxyMap) {
29
+ try {
30
+ await clientProxy.close();
31
+ this.logger.info(`Closed client proxy for queue: ${queueName}`);
32
+ } catch (error) {
33
+ this.logger.error(`Error closing client proxy for queue ${queueName}:`, { error });
34
+ }
35
+ }
36
+ this.clientProxyMap.clear();
37
+ this.queueMessagingServiceMap.clear();
38
+ }
39
+
40
+ getMessagingService(queueName: QueueName): QueueMessagingService {
41
+ const existing = this.queueMessagingServiceMap.get(queueName);
42
+ if (existing) {return existing;}
43
+
44
+ const clientProxy = ClientProxyFactory.create({
45
+ transport: Transport.RMQ,
46
+ options: {
47
+ ...this.clientProxyOptions,
48
+ queue: this.queueNameResolver(queueName),
49
+ queueOptions: {
50
+ ...this.clientProxyOptions.queueOptions,
51
+ exclusive: false,
52
+ autoDelete: false,
53
+ },
54
+ socketOptions: {
55
+ heartbeatIntervalInSeconds: 30,
56
+ reconnectTimeInSeconds: 5,
57
+ },
58
+ },
59
+ });
60
+
61
+ this.clientProxyMap.set(queueName, clientProxy);
62
+
63
+ const messagingService = new RabbitQueueMessagingService(clientProxy);
64
+ this.queueMessagingServiceMap.set(queueName, messagingService);
65
+ return messagingService;
66
+ }
67
+ }
@@ -0,0 +1,38 @@
1
+ import { HttpException } from "@nestjs/common";
2
+
3
+ import { DetailedError } from "../../../core/errors";
4
+ import { ErrorHandlerEntry, HttpExceptionClass, HttpExceptionReturn } from "./error-handler.type";
5
+
6
+ export class ErrorHandler {
7
+ constructor(private readonly entries: ErrorHandlerEntry[]) {}
8
+
9
+ toHttpException(error: unknown): HttpException | undefined {
10
+ if (!(error instanceof DetailedError)) {
11
+ return undefined;
12
+ }
13
+
14
+ const entry = this.entries.find((entry) => error instanceof entry.on);
15
+
16
+ if (!entry) {
17
+ return undefined;
18
+ }
19
+
20
+ return ErrorHandler.buildHttpException(error, entry.use, entry.return?.(error));
21
+ }
22
+
23
+ static buildHttpException<TError extends DetailedError, TException extends HttpException>(
24
+ error: TError,
25
+ exception: HttpExceptionClass<TException>,
26
+ exceptionReturn: HttpExceptionReturn = {},
27
+ ): TException {
28
+ return new exception(exceptionReturn.message, {
29
+ cause: {
30
+ layer: error.layer(),
31
+ details: error.details(),
32
+ stack: error.stack,
33
+ message: error.message,
34
+ response: exceptionReturn.response,
35
+ },
36
+ });
37
+ }
38
+ }
@@ -0,0 +1,23 @@
1
+ import { HttpException } from "@nestjs/common";
2
+
3
+ import { DetailedError } from "../../../core/errors";
4
+
5
+ export type DetailedErrorClass<TError extends DetailedError> = new (...args: never[]) => TError;
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ export type HttpExceptionClass<TException extends HttpException> = new (...args: any[]) => TException;
8
+
9
+ export type HttpExceptionReturn = {
10
+ message?: string;
11
+ response?: unknown;
12
+ };
13
+
14
+ export type HttpExceptionReturnFn<TError extends DetailedError> = (error: TError) => HttpExceptionReturn;
15
+
16
+ export type ErrorHandlerEntry<
17
+ TError extends DetailedError = DetailedError,
18
+ TException extends HttpException = HttpException,
19
+ > = {
20
+ on: DetailedErrorClass<TError>;
21
+ use: HttpExceptionClass<TException>;
22
+ return?: HttpExceptionReturnFn<TError>;
23
+ };
@@ -0,0 +1,59 @@
1
+ import {
2
+ BadGatewayException,
3
+ BadRequestException,
4
+ ConflictException,
5
+ ForbiddenException,
6
+ HttpException,
7
+ InternalServerErrorException,
8
+ ServiceUnavailableException,
9
+ UnauthorizedException,
10
+ UnprocessableEntityException,
11
+ } from "@nestjs/common";
12
+
13
+ import { ArchitectureLayerType, ArchitectureLevel } from "../../../core/types";
14
+ import { DetailedError } from "../../../core/errors";
15
+ import { ErrorHandler } from "./error-handler";
16
+ import { HttpExceptionClass } from "./error-handler.type";
17
+
18
+ const LAYER_TO_EXCEPTION: Record<ArchitectureLayerType, HttpExceptionClass<HttpException>> = {
19
+ "aggregate-root": UnprocessableEntityException,
20
+ "entity": UnprocessableEntityException,
21
+ "value-object": UnprocessableEntityException,
22
+ "command": UnprocessableEntityException,
23
+ "query": BadGatewayException,
24
+ "event-handler": ConflictException,
25
+ "event-saga": ConflictException,
26
+ "service": InternalServerErrorException,
27
+ "authorization": ForbiddenException,
28
+ "authentication": UnauthorizedException,
29
+ "web-dto": BadRequestException,
30
+ "web-rest": InternalServerErrorException,
31
+ "web-graphql": InternalServerErrorException,
32
+ "web-socket": InternalServerErrorException,
33
+ "web-rpc": InternalServerErrorException,
34
+ "persistence-sql": InternalServerErrorException,
35
+ "persistence-disk": InternalServerErrorException,
36
+ "persistence-fake": InternalServerErrorException,
37
+ "persistence-saas": ServiceUnavailableException,
38
+ "persistence-s3": ServiceUnavailableException,
39
+ "persistence-other": ServiceUnavailableException,
40
+ "messaging-inmem": InternalServerErrorException,
41
+ "messaging-pubsub": ServiceUnavailableException,
42
+ "unknown": InternalServerErrorException,
43
+ };
44
+
45
+ export class GenericErrorHandler {
46
+ toHttpException(error: unknown): HttpException | undefined {
47
+ if (!(error instanceof DetailedError)) {return undefined;}
48
+
49
+ const layerType = this.extractArchitectureLayerType(error);
50
+ const ExceptionClass = LAYER_TO_EXCEPTION[layerType] ?? InternalServerErrorException;
51
+ return ErrorHandler.buildHttpException(error, ExceptionClass);
52
+ }
53
+
54
+ private extractArchitectureLayerType(error: DetailedError): ArchitectureLayerType {
55
+ const layer = error.layer();
56
+ const level = Object.keys(layer)[0] as ArchitectureLevel;
57
+ return Object.keys(layer[level as never])[0] as ArchitectureLayerType;
58
+ }
59
+ }
@@ -0,0 +1,43 @@
1
+ import { isObservable, throwError } from "rxjs";
2
+ import { catchError } from "rxjs/operators";
3
+
4
+ import { DetailedError } from "../../../core/errors";
5
+ import { ErrorHandler } from "./error-handler";
6
+ import { ErrorHandlerEntry } from "./error-handler.type";
7
+
8
+ export function HandleErrors(entries?: ErrorHandlerEntry[]) {
9
+ return function (_target: object, _propertyKey: string, descriptor: PropertyDescriptor) {
10
+ const originalMethod = descriptor.value as (...args: unknown[]) => unknown;
11
+ const errorHandler = new ErrorHandler(entries ?? []);
12
+
13
+ const convertError = (rawError: unknown): unknown =>
14
+ rawError instanceof DetailedError ? (errorHandler.toHttpException(rawError) ?? rawError) : rawError;
15
+
16
+ descriptor.value = function (...args: unknown[]) {
17
+ try {
18
+ const result = originalMethod.apply(this, args) as unknown;
19
+
20
+ if (result && typeof (result as Promise<unknown>).then === "function") {
21
+ return (result as Promise<unknown>).catch((err: unknown) => {
22
+ throw convertError(err);
23
+ });
24
+ }
25
+
26
+ if (isObservable(result)) {
27
+ return result.pipe(
28
+ catchError((err: unknown) => {
29
+ const converted = convertError(err);
30
+ return throwError(() => (converted instanceof Error ? converted : new Error(String(converted))));
31
+ }),
32
+ );
33
+ }
34
+
35
+ return result;
36
+ } catch (rawError) {
37
+ throw convertError(rawError);
38
+ }
39
+ };
40
+
41
+ return descriptor;
42
+ };
43
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./error-handler";
2
+ export * from "./error-handler.type";
3
+ export * from "./generic-error-handler";
4
+ export * from "./handle-errors.decorator";
5
+ export * from "./validation-error-handler";
@@ -0,0 +1,46 @@
1
+ import { BadRequestException } from "@nestjs/common";
2
+ import { ValidationError } from "class-validator";
3
+
4
+ import { ArchitectureLayer, ArchitectureLevel } from "../../../core/types";
5
+
6
+ export class ValidationErrorHandler {
7
+ toHttpException(validationErrors: ValidationError[]): BadRequestException {
8
+ const firstError = validationErrors[0];
9
+ const constraints = firstError?.constraints;
10
+ const firstMessage = constraints?.[Object.keys(constraints ?? {})[0]];
11
+ const details = this.formatValidationErrorsNested(validationErrors);
12
+
13
+ return new BadRequestException(firstMessage, {
14
+ cause: {
15
+ layer: {
16
+ [ArchitectureLevel.INFRASTRUCTURE]: {
17
+ "web-dto": {
18
+ className: firstError?.target?.constructor.name ?? ValidationErrorHandler.name,
19
+ },
20
+ },
21
+ } satisfies ArchitectureLayer,
22
+ details,
23
+ response: details,
24
+ },
25
+ });
26
+ }
27
+
28
+ private formatValidationErrorsNested(errors: ValidationError[]): Record<string, unknown> {
29
+ const formatted: Record<string, unknown> = {};
30
+
31
+ for (const err of errors) {
32
+ if (err.constraints) {
33
+ formatted[err.property] = Object.values(err.constraints)[0];
34
+ }
35
+
36
+ if (err.children?.length) {
37
+ const nested = this.formatValidationErrorsNested(err.children);
38
+ formatted[err.property] = formatted[err.property]
39
+ ? { errors: formatted[err.property], children: nested }
40
+ : nested;
41
+ }
42
+ }
43
+
44
+ return formatted;
45
+ }
46
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./handlers";
2
+ export * from "./parsers";
@@ -0,0 +1,21 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+ import { AxiosError } from "axios";
3
+
4
+ import { ErrorMetadata } from "./error-metadata.definition";
5
+
6
+ export const axiosMetadataParser = (error: unknown, extraMetadata: ErrorMetadata = {}): ErrorMetadata => {
7
+ if (!(error instanceof AxiosError)) {
8
+ return { ...extraMetadata, rawError: error };
9
+ }
10
+
11
+ const { status, statusText, data, config } = error.response ?? {};
12
+
13
+ return {
14
+ ...extraMetadata,
15
+ code: status,
16
+ status: statusText,
17
+ data,
18
+ url: config?.url,
19
+ httpMethod: config?.method,
20
+ };
21
+ };
@@ -0,0 +1 @@
1
+ export type ErrorMetadata = Record<string, unknown>;
@@ -0,0 +1,5 @@
1
+ export * from "./axios-metadata.parser";
2
+ export * from "./error-metadata.definition";
3
+ export * from "./knex-metadata.parser";
4
+ export * from "./typeorm-metadata.parser";
5
+ export * from "./workos-metadata.parser";
@@ -0,0 +1,18 @@
1
+ import { ErrorMetadata } from "./error-metadata.definition";
2
+
3
+ export const knexMetadataParser = (error: unknown, extraMetadata: ErrorMetadata = {}): ErrorMetadata => {
4
+ const knexError = error as Record<string, unknown> | null;
5
+
6
+ const metadata: ErrorMetadata = {
7
+ code: knexError?.code,
8
+ hint: knexError?.hint,
9
+ detail: knexError?.detail,
10
+ ...extraMetadata,
11
+ };
12
+
13
+ if (!metadata.code && !metadata.hint && !metadata.detail) {
14
+ metadata.rawError = error;
15
+ }
16
+
17
+ return metadata;
18
+ };
@@ -0,0 +1,19 @@
1
+ import { TypeORMError } from "typeorm";
2
+
3
+ import { ErrorMetadata } from "./error-metadata.definition";
4
+
5
+ export const typeormMetadataParser = (error: unknown, extraMetadata: ErrorMetadata = {}): ErrorMetadata => {
6
+ const metadata: ErrorMetadata = {
7
+ detail: (error as Record<string, unknown> | null)?.detail,
8
+ ...extraMetadata,
9
+ };
10
+
11
+ if (error instanceof TypeORMError) {
12
+ metadata.message = error.message;
13
+ metadata.name = error.name;
14
+ } else {
15
+ metadata.rawError = error;
16
+ }
17
+
18
+ return metadata;
19
+ };
@@ -0,0 +1,17 @@
1
+ import { ErrorMetadata } from "./error-metadata.definition";
2
+
3
+ export const workosMetadataParser = (error: unknown, extraMetadata: ErrorMetadata = {}): ErrorMetadata => {
4
+ const rawData = (error as Record<string, Record<string, unknown>> | null)?.rawData;
5
+
6
+ const metadata: ErrorMetadata = {
7
+ code: rawData?.code,
8
+ message: rawData?.message,
9
+ ...extraMetadata,
10
+ };
11
+
12
+ if (!metadata.code) {
13
+ metadata.rawError = error;
14
+ }
15
+
16
+ return metadata;
17
+ };
@@ -0,0 +1 @@
1
+ export * from "./log-app-ctx.decorator";
@@ -0,0 +1,32 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
2
+ import { Inject } from "@nestjs/common";
3
+
4
+ import { ArchitectureLayer, ArchitectureLevel } from "../../../core/types";
5
+ import { LoggerService } from "../../../modules/logger/logger.service";
6
+
7
+ export function LogAppCtx() {
8
+ const injectLogger = Inject(LoggerService);
9
+
10
+ return function (target: object, _propertyKey: string, descriptor: unknown) {
11
+ injectLogger(target, "logger");
12
+
13
+ const originalMethod = (descriptor as PropertyDescriptor).value;
14
+
15
+ (descriptor as PropertyDescriptor).value = function (...args: unknown[]) {
16
+ const className = target.constructor.name;
17
+ const type = className.includes("CommandHandler")
18
+ ? "command"
19
+ : className.includes("QueryHandler")
20
+ ? "query"
21
+ : null;
22
+
23
+ if (type) {
24
+ (this as { logger: LoggerService }).logger?.ctx({
25
+ [ArchitectureLevel.APPLICATION]: { [type]: { className } },
26
+ } as ArchitectureLayer);
27
+ }
28
+
29
+ return originalMethod.apply(this, args);
30
+ };
31
+ };
32
+ }
@@ -0,0 +1,15 @@
1
+ import { Type } from "class-transformer";
2
+ import { IsDate, IsOptional } from "class-validator";
3
+
4
+ // NOTE: only use this dto for POST query
5
+ export class DateRangeDto {
6
+ @IsOptional()
7
+ @Type(() => Date)
8
+ @IsDate()
9
+ from?: Date;
10
+
11
+ @IsOptional()
12
+ @Type(() => Date)
13
+ @IsDate()
14
+ to?: Date;
15
+ }
@@ -0,0 +1 @@
1
+ export * from "./date-range.dto";
@@ -0,0 +1,92 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
2
+ import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from "@nestjs/common";
3
+ import { HttpArgumentsHost, RpcArgumentsHost } from "@nestjs/common/interfaces";
4
+ import * as Sentry from "@sentry/nestjs";
5
+ import { Response } from "express";
6
+
7
+ import { ArchitectureLayer, ArchitectureLevel } from "../../../core/types";
8
+ import { GenericErrorHandler } from "../../errors/handlers/generic-error-handler";
9
+ import { LoggerService } from "../../../modules/logger/logger.service";
10
+
11
+ @Catch()
12
+ export class AllExceptionsFilter implements ExceptionFilter {
13
+ private readonly genericErrorHandler = new GenericErrorHandler();
14
+ private readonly currentLayer: ArchitectureLayer = {
15
+ [ArchitectureLevel.INFRASTRUCTURE]: {
16
+ "web-rest": {
17
+ className: AllExceptionsFilter.name,
18
+ },
19
+ },
20
+ };
21
+
22
+ constructor(private readonly loggerService: LoggerService) {}
23
+
24
+ catch(error: unknown, host: ArgumentsHost) {
25
+ if (!(error instanceof HttpException)) {
26
+ Sentry.captureException(error);
27
+ }
28
+
29
+ if (host.getType() === "http") {
30
+ return this.throwHttp(error, host.switchToHttp());
31
+ }
32
+
33
+ if (host.getType() === "rpc") {
34
+ this.logRpc(error, host.switchToRpc());
35
+ return;
36
+ }
37
+
38
+ this.loggerService.warn("Unhandled error", error instanceof Error ? { error: error.message, stack: error.stack } : undefined);
39
+ }
40
+
41
+ private throwHttp(error: unknown, httpContext: HttpArgumentsHost) {
42
+ const response = httpContext.getResponse<Response>();
43
+
44
+ if (error instanceof HttpException) {
45
+ return this.throwHttpException(error, response);
46
+ }
47
+
48
+ const genericHttpException = this.genericErrorHandler.toHttpException(error);
49
+ if (genericHttpException) {
50
+ return this.throwHttpException(genericHttpException, response);
51
+ }
52
+
53
+ return this.throwUnhandledException(error, response);
54
+ }
55
+
56
+ private throwHttpException(exception: HttpException, response: Response<unknown, Record<string, unknown>>) {
57
+ const statusCode = exception.getStatus();
58
+ const cause = exception.cause as Record<string, unknown> | undefined;
59
+
60
+ const layer = (cause?.layer ?? cause?.layerObject ?? this.currentLayer) as ArchitectureLayer;
61
+ const details = (cause?.details ?? cause?.detailsObject ?? {}) as Record<string, unknown>;
62
+ const message = (cause?.message as string) ?? "";
63
+ const errorResponse = cause?.response ?? {};
64
+
65
+ this.loggerService.ctx(layer, { ...details });
66
+ this.loggerService.info(`[Platform] error thrown: ${message}`);
67
+
68
+ return response.status(statusCode).json({
69
+ statusCode,
70
+ message: exception.message,
71
+ cause: errorResponse,
72
+ });
73
+ }
74
+
75
+ private throwUnhandledException(error: unknown, response: Response<unknown, Record<string, unknown>>) {
76
+ this.loggerService.warn(`Unhandled error: ${String(error)}`, error instanceof Error ? { error: error.message, stack: error.stack } : undefined);
77
+ return response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
78
+ statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
79
+ message: "Unexpected error",
80
+ });
81
+ }
82
+
83
+ private logRpc(error: unknown, rpcContext: RpcArgumentsHost) {
84
+ const context = rpcContext.getContext();
85
+ const queue = context.args[0].fields.routingKey;
86
+ const pattern = context.args[2];
87
+ this.loggerService.warn(
88
+ "RPC error",
89
+ error instanceof Error ? { error, queue, pattern, payload: rpcContext.getData() } : undefined,
90
+ );
91
+ }
92
+ }
@@ -0,0 +1,12 @@
1
+ import { ArgumentsHost, Catch } from "@nestjs/common";
2
+ import { GqlArgumentsHost, GqlExceptionFilter } from "@nestjs/graphql";
3
+
4
+ import { CommandFailureError } from "../../../core/errors/command-failure.error";
5
+
6
+ @Catch(CommandFailureError)
7
+ export class CommandFailureFilter implements GqlExceptionFilter {
8
+ catch(error: CommandFailureError, host: ArgumentsHost) {
9
+ if (GqlArgumentsHost.create(host).getInfo() != null) {return error;}
10
+ throw error;
11
+ }
12
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./all-exceptions.filter";
2
+ export * from "./command-failure.filter";
3
+ export * from "./rpc-exceptions.filter";
4
+ export * from "./too-many-results.filter";
@@ -0,0 +1,10 @@
1
+ import { Catch, RpcExceptionFilter as NestRpcExceptionFilter } from "@nestjs/common";
2
+ import { RpcException } from "@nestjs/microservices";
3
+ import { Observable, throwError } from "rxjs";
4
+
5
+ @Catch(RpcException)
6
+ export class RpcExceptionFilter implements NestRpcExceptionFilter {
7
+ catch(exception: RpcException): Observable<unknown> {
8
+ return throwError(() => exception.getError());
9
+ }
10
+ }
@@ -0,0 +1,12 @@
1
+ import { ArgumentsHost, Catch } from "@nestjs/common";
2
+ import { GqlArgumentsHost, GqlExceptionFilter } from "@nestjs/graphql";
3
+
4
+ import { TooManyResultsError } from "../../../core/errors/too-many-results.error";
5
+
6
+ @Catch(TooManyResultsError)
7
+ export class TooManyResultsFilter implements GqlExceptionFilter {
8
+ catch(error: TooManyResultsError, host: ArgumentsHost) {
9
+ if (GqlArgumentsHost.create(host).getInfo() != null) {return error;}
10
+ throw error;
11
+ }
12
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./decorators";
2
+ export * from "./dtos";
3
+ export * from "./filters";
4
+ export * from "./interceptors";
5
+ export * from "./middleware";
6
+ export * from "./swagger";
@@ -0,0 +1,2 @@
1
+ export * from "./log-app-ctx.interceptor";
2
+ export * from "./serialize-output.interceptor";