@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,109 @@
1
+ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
2
+ import { GqlContextType, GqlExecutionContext } from "@nestjs/graphql";
3
+ import type { Request } from "express";
4
+ import type { GraphQLResolveInfo } from "graphql";
5
+
6
+ import { ArchitectureLevel } from "../../../core/types";
7
+ import { LoggerService } from "../../../modules/logger/logger.service";
8
+
9
+ @Injectable()
10
+ export class LogAppCtxInterceptor implements NestInterceptor {
11
+ constructor(private readonly loggerService: LoggerService) {}
12
+
13
+ intercept(context: ExecutionContext, next: CallHandler) {
14
+ switch (context.getType<GqlContextType>()) {
15
+ case "http": {
16
+ this.setHttpLogContext(context);
17
+ break;
18
+ }
19
+ case "rpc": {
20
+ this.setRpcLogContext(context);
21
+ break;
22
+ }
23
+ case "ws": {
24
+ this.setWsLogContext(context);
25
+ break;
26
+ }
27
+ case "graphql": {
28
+ this.setGraphqlLogContext(context);
29
+ break;
30
+ }
31
+ }
32
+
33
+ return next.handle();
34
+ }
35
+
36
+ private setHttpLogContext(context: ExecutionContext) {
37
+ const className = context.getClass().name;
38
+ const handlerName = context.getHandler().name;
39
+ const httpContext = context.switchToHttp();
40
+ const request = httpContext.getRequest<Request>();
41
+
42
+ this.loggerService.ctx({
43
+ [ArchitectureLevel.INFRASTRUCTURE]: {
44
+ "web-rest": {
45
+ className,
46
+ meta: {
47
+ handlerName,
48
+ path: request.url,
49
+ method: request.method,
50
+ },
51
+ },
52
+ },
53
+ });
54
+ }
55
+
56
+ private setRpcLogContext(context: ExecutionContext) {
57
+ const className = context.getClass().name;
58
+ const handlerName = context.getHandler().name;
59
+
60
+ this.loggerService.ctx({
61
+ [ArchitectureLevel.INFRASTRUCTURE]: {
62
+ "web-rpc": {
63
+ className,
64
+ meta: { handlerName },
65
+ },
66
+ },
67
+ });
68
+ }
69
+
70
+ private setWsLogContext(context: ExecutionContext) {
71
+ const className = context.getClass().name;
72
+ const handlerName = context.getHandler().name;
73
+ const wsContext = context.switchToWs();
74
+ const pattern = wsContext.getPattern();
75
+
76
+ this.loggerService.ctx({
77
+ [ArchitectureLevel.INFRASTRUCTURE]: {
78
+ "web-socket": {
79
+ className,
80
+ meta: {
81
+ handlerName,
82
+ pattern,
83
+ },
84
+ },
85
+ },
86
+ });
87
+ }
88
+
89
+ private setGraphqlLogContext(context: ExecutionContext) {
90
+ const className = context.getClass().name;
91
+ const handlerName = context.getHandler().name;
92
+ const gqlContext = GqlExecutionContext.create(context);
93
+ const info = gqlContext.getInfo<GraphQLResolveInfo>();
94
+
95
+ this.loggerService.ctx({
96
+ [ArchitectureLevel.INFRASTRUCTURE]: {
97
+ "web-graphql": {
98
+ className,
99
+ meta: {
100
+ handlerName,
101
+ operationName: info.operation.name?.value,
102
+ operationType: info.operation.operation,
103
+ fieldName: info.fieldName,
104
+ },
105
+ },
106
+ },
107
+ });
108
+ }
109
+ }
@@ -0,0 +1,19 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument */
2
+ import { ClassSerializerInterceptor, mixin, Type, UseInterceptors } from "@nestjs/common";
3
+ import * as ClassTransformer from "class-transformer";
4
+
5
+ function PojoSerializer<T>(serializedClass: Type<T>): Type<ClassSerializerInterceptor> {
6
+ class CustomSerializer extends ClassSerializerInterceptor {
7
+ override transformToPlain(pojoOrInstance: unknown, transformOptions: ClassTransformer.ClassTransformOptions) {
8
+ transformOptions.strategy ??= "excludeAll";
9
+ const instance = Object.assign(Object.create(serializedClass.prototype), pojoOrInstance);
10
+ return super.transformToPlain(instance, transformOptions);
11
+ }
12
+ }
13
+
14
+ return mixin(CustomSerializer);
15
+ }
16
+
17
+ export function SerializeOutput<T>(dtoClass: Type<T>) {
18
+ return UseInterceptors(PojoSerializer(dtoClass));
19
+ }
@@ -0,0 +1,31 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
+ import { Injectable, NestMiddleware } from "@nestjs/common";
3
+ import type { NextFunction, Request, Response } from "express";
4
+
5
+ import { LoggerService } from "../../../modules/logger/logger.service";
6
+
7
+ @Injectable()
8
+ export class HttpLoggerMiddleware implements NestMiddleware {
9
+ constructor(private readonly loggerService: LoggerService) {}
10
+ use(req?: Request, res?: Response, next?: NextFunction) {
11
+ try {
12
+ if (req && res) {
13
+ const start = Date.now();
14
+ res.on("close", () => {
15
+ const graphql = req.body?.operationName ? ` ${req.body.operationName}` : "";
16
+ this.loggerService.clearCtx();
17
+ this.loggerService.http(`[${req.method}] ${req.originalUrl}${graphql}`, {
18
+ body: req.body,
19
+ method: req.method,
20
+ metrics: { duration: Date.now() - start },
21
+ status: res.statusCode,
22
+ });
23
+ });
24
+ }
25
+ } catch {
26
+ this.loggerService.warn("something went wrong while logging");
27
+ }
28
+
29
+ next?.();
30
+ }
31
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./http-logger.middleware";
2
+ export * from "./logger.middleware";
@@ -0,0 +1,21 @@
1
+ import { Injectable, NestMiddleware } from "@nestjs/common";
2
+ import type { NextFunction, Request, Response } from "express";
3
+ import { v4 as uuidv4 } from "uuid";
4
+
5
+ import { ArchitectureLevel } from "../../../core/types";
6
+ import { LoggerService } from "../../../modules/logger/logger.service";
7
+
8
+ @Injectable()
9
+ export class LoggerMiddleware implements NestMiddleware {
10
+ constructor(private readonly loggerService: LoggerService) {}
11
+ use(req: Request, res: Response, next: NextFunction) {
12
+ const requestId = uuidv4();
13
+ this.loggerService.init(req, res, () => {
14
+ this.loggerService.ctx(
15
+ { [ArchitectureLevel.INFRASTRUCTURE]: { "web-rest": { className: LoggerMiddleware.name } } },
16
+ { requestId },
17
+ );
18
+ next();
19
+ });
20
+ }
21
+ }
@@ -0,0 +1 @@
1
+ export * from "./swagger";
@@ -0,0 +1,44 @@
1
+ import { INestApplication } from "@nestjs/common";
2
+ import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
3
+ import basicAuth from "express-basic-auth";
4
+
5
+ import { ConfigService } from "../../../core/infrastructure/config";
6
+
7
+ export function setupSwagger(app: INestApplication) {
8
+ if (process.env.NODE_ENV !== "development") {
9
+ setupAuthorization(app);
10
+ }
11
+ setupDocumentation(app);
12
+ }
13
+
14
+ function setupDocumentation(app: INestApplication) {
15
+ const options = new DocumentBuilder().setTitle("Harbor API").setVersion("1.0").build();
16
+
17
+ const document = SwaggerModule.createDocument(app, options);
18
+ SwaggerModule.setup("docs", app, document, {
19
+ swaggerOptions: {
20
+ tagsSorter: "alpha",
21
+ operationsSorter: "alpha",
22
+ },
23
+ });
24
+ }
25
+
26
+ function setupAuthorization(app: INestApplication) {
27
+ const configService = app.get(ConfigService);
28
+
29
+ app.use(
30
+ ["/docs"],
31
+ basicAuth({
32
+ challenge: true,
33
+ authorizeAsync: true,
34
+ authorizer: (email, password, authorize) => {
35
+ const valid =
36
+ !!email &&
37
+ !!password &&
38
+ email === configService.get("SWAGGER_EMAIL_SWAGGER") &&
39
+ password === configService.get("SWAGGER_PASSWORD_SWAGGER");
40
+ authorize(null, valid);
41
+ },
42
+ }),
43
+ );
44
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./errors";
2
+ export * from "./http";
@@ -0,0 +1,33 @@
1
+ import { ModuleRef } from "@nestjs/core";
2
+ import { CommandBus, ICommand } from "@nestjs/cqrs";
3
+
4
+ export class StubCommandBus extends CommandBus {
5
+ stubCommandData = new Map<string, unknown>();
6
+ executedCommand: ICommand[] = [];
7
+ error?: string;
8
+
9
+ constructor() {
10
+ super({} as ModuleRef);
11
+ }
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ override execute(command: ICommand): Promise<any> {
15
+ if (this.error) {throw new Error(this.error);}
16
+ this.executedCommand.push(command);
17
+ return Promise.resolve(this.stubCommandData.get(command.constructor.name));
18
+ }
19
+
20
+ setCommandResponse(commandType: { name: string }, commandResponse: unknown) {
21
+ this.stubCommandData.set(commandType.name, commandResponse);
22
+ }
23
+
24
+ setError(error: string) {
25
+ this.error = error;
26
+ }
27
+
28
+ clear() {
29
+ this.stubCommandData.clear();
30
+ this.executedCommand = [];
31
+ this.error = undefined;
32
+ }
33
+ }
@@ -0,0 +1,56 @@
1
+ import { IEvent, IEventBus, IEventPublisher, ObservableBus } from "@nestjs/cqrs";
2
+ import { Observable } from "rxjs";
3
+
4
+ export class StubEventBus<EventBase extends IEvent = IEvent> extends ObservableBus<EventBase> implements IEventBus<EventBase> {
5
+ stubEventData: Map<unknown, IEvent> = new Map();
6
+ error?: string;
7
+
8
+ get publisher() {
9
+ return this;
10
+ }
11
+
12
+ set publisher(_publisher: IEventPublisher<EventBase>) {}
13
+
14
+ publishAll(events: IEvent[]): unknown {
15
+ if (this.error) {
16
+ throw new Error(this.error);
17
+ }
18
+ return events.map((event) => this.stubEventData.set(event, event));
19
+ }
20
+
21
+ publish(event: IEvent): unknown {
22
+ return this.stubEventData.set(event.constructor.name, event);
23
+ }
24
+
25
+ getEvents() {
26
+ return this.stubEventData;
27
+ }
28
+
29
+ clear() {
30
+ this.stubEventData.clear();
31
+ }
32
+
33
+ registerSagas() {
34
+ return undefined;
35
+ }
36
+
37
+ register() {
38
+ return undefined;
39
+ }
40
+
41
+ bind() {
42
+ return undefined;
43
+ }
44
+
45
+ protected registerHandler() {
46
+ return undefined;
47
+ }
48
+
49
+ protected ofEventId(): Observable<EventBase> {
50
+ return new Observable();
51
+ }
52
+
53
+ protected registerSaga() {
54
+ return undefined;
55
+ }
56
+ }
@@ -0,0 +1,24 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
+ import { AggregateRoot, Constructor, IEvent } from "@nestjs/cqrs";
3
+
4
+ import { StubEventBus } from "./event-bus.stub";
5
+
6
+ export class StubEventPublisher<EventBase extends IEvent = IEvent> extends StubEventBus<EventBase> {
7
+ mergeObjectContext<T extends AggregateRoot<EventBase>>(object: T): T {
8
+ Object.assign(object, {
9
+ publish: this.publish.bind(this),
10
+ publishAll: this.publishAll.bind(this),
11
+ });
12
+
13
+ return object;
14
+ }
15
+
16
+ mergeClassContext<T extends Constructor<AggregateRoot<EventBase>>>(metatype: T): T {
17
+ Object.assign(metatype.prototype, {
18
+ publish: this.publish.bind(this),
19
+ publishAll: this.publishAll.bind(this),
20
+ });
21
+
22
+ return metatype;
23
+ }
24
+ }
@@ -0,0 +1,20 @@
1
+ import { LoggerService } from "../modules/logger/logger.service";
2
+ import { FakeTransport } from "../modules/logger/transports/fake.transport";
3
+
4
+ export class FakeLogger extends LoggerService {
5
+ private transport: FakeTransport;
6
+
7
+ constructor() {
8
+ const transport = new FakeTransport();
9
+ super({ service: "test", transports: [transport] });
10
+ this.transport = transport;
11
+ }
12
+
13
+ get logs() {
14
+ return this.transport.logs;
15
+ }
16
+
17
+ clear() {
18
+ this.transport.logs = [];
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./fake-logger";
2
+ export * from "./stubs";
@@ -0,0 +1,27 @@
1
+ import { IQuery } from "@nestjs/cqrs";
2
+
3
+ export class StubQueryBus {
4
+ stubQueryData: Map<string, unknown> = new Map();
5
+ error?: string;
6
+
7
+ execute(query: IQuery) {
8
+ if (this.error) {
9
+ throw new Error(this.error);
10
+ }
11
+
12
+ return this.stubQueryData.get(query.constructor.name);
13
+ }
14
+
15
+ setQueryResponse(queryType: IQuery, queryResponse: unknown) {
16
+ this.stubQueryData.set(queryType.constructor.name, queryResponse);
17
+ }
18
+
19
+ setError(error: string) {
20
+ this.error = error;
21
+ }
22
+
23
+ clear() {
24
+ this.stubQueryData.clear();
25
+ this.error = undefined;
26
+ }
27
+ }
@@ -0,0 +1,250 @@
1
+ import { FakeImplementationError } from "../core/errors";
2
+ import { Stub } from "./stub";
3
+
4
+ interface UserService {
5
+ fetchUser(id: string): Promise<User>;
6
+ createUser(name: string, email: string): Promise<User>;
7
+ deleteUser(id: string): Promise<void>;
8
+ }
9
+
10
+ interface User {
11
+ id: string;
12
+ name: string;
13
+ email: string;
14
+ }
15
+
16
+ class FakeUserService extends Stub<FakeUserService> implements UserService {
17
+ async fetchUser(id: string): Promise<User> {
18
+ return this.onMethod("fetchUser").withArgs(id).execute();
19
+ }
20
+
21
+ async createUser(name: string, email: string): Promise<User> {
22
+ return this.onMethod("createUser").withArgs(name, email).execute();
23
+ }
24
+
25
+ async deleteUser(id: string): Promise<void> {
26
+ return this.onMethod("deleteUser").withArgs(id).execute();
27
+ }
28
+ }
29
+
30
+ describe(Stub.name, () => {
31
+ let fake: FakeUserService;
32
+
33
+ beforeEach(() => {
34
+ fake = new FakeUserService();
35
+ });
36
+
37
+ describe("onMethod().withArgs().returnValue()", () => {
38
+ it("should return the stubbed value when executed", async () => {
39
+ const user: User = { id: "1", name: "John", email: "john@example.com" };
40
+
41
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user);
42
+
43
+ const result = await fake.fetchUser("1");
44
+
45
+ expect(result).toEqual(user);
46
+ });
47
+
48
+ it("should return different values for different arguments", async () => {
49
+ const user1: User = { id: "1", name: "John", email: "john@example.com" };
50
+ const user2: User = { id: "2", name: "Jane", email: "jane@example.com" };
51
+
52
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user1);
53
+ fake.onMethod("fetchUser").withArgs("2").returnValue(user2);
54
+
55
+ expect(await fake.fetchUser("1")).toEqual(user1);
56
+ expect(await fake.fetchUser("2")).toEqual(user2);
57
+ });
58
+
59
+ it("should handle methods with multiple arguments", async () => {
60
+ const user: User = { id: "1", name: "John", email: "john@example.com" };
61
+
62
+ fake.onMethod("createUser").withArgs("John", "john@example.com").returnValue(user);
63
+
64
+ const result = await fake.createUser("John", "john@example.com");
65
+
66
+ expect(result).toEqual(user);
67
+ });
68
+
69
+ it("should handle void return type", async () => {
70
+ fake.onMethod("deleteUser").withArgs("1").returnValue(undefined);
71
+
72
+ await expect(fake.deleteUser("1")).resolves.toBeUndefined();
73
+ });
74
+
75
+ it("should override previous stub when called again with same args", async () => {
76
+ const user1: User = { id: "1", name: "John", email: "john@example.com" };
77
+ const user2: User = { id: "1", name: "John Updated", email: "john.updated@example.com" };
78
+
79
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user1);
80
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user2);
81
+
82
+ const result = await fake.fetchUser("1");
83
+
84
+ expect(result).toEqual(user2);
85
+ });
86
+ });
87
+
88
+ describe("onMethod().withArgs().throwError()", () => {
89
+ it("should throw the stubbed error when executed", async () => {
90
+ const error = new Error("User not found");
91
+
92
+ fake.onMethod("fetchUser").withArgs("invalid").throwError(error);
93
+
94
+ await expect(fake.fetchUser("invalid")).rejects.toThrow("User not found");
95
+ });
96
+
97
+ it("should throw different errors for different arguments", async () => {
98
+ fake.onMethod("fetchUser").withArgs("not-found").throwError(new Error("User not found"));
99
+ fake.onMethod("fetchUser").withArgs("forbidden").throwError(new Error("Access denied"));
100
+
101
+ await expect(fake.fetchUser("not-found")).rejects.toThrow("User not found");
102
+ await expect(fake.fetchUser("forbidden")).rejects.toThrow("Access denied");
103
+ });
104
+
105
+ it("should override returnValue when throwError is called with same args", async () => {
106
+ const user: User = { id: "1", name: "John", email: "john@example.com" };
107
+
108
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user);
109
+ fake.onMethod("fetchUser").withArgs("1").throwError(new Error("Changed to error"));
110
+
111
+ await expect(fake.fetchUser("1")).rejects.toThrow("Changed to error");
112
+ });
113
+
114
+ it("should override throwError when returnValue is called with same args", async () => {
115
+ const user: User = { id: "1", name: "John", email: "john@example.com" };
116
+
117
+ fake.onMethod("fetchUser").withArgs("1").throwError(new Error("Error first"));
118
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user);
119
+
120
+ const result = await fake.fetchUser("1");
121
+
122
+ expect(result).toEqual(user);
123
+ });
124
+ });
125
+
126
+ describe("onMethod().returnValue()", () => {
127
+ it("should return the stubbed value for any arguments", async () => {
128
+ const user: User = { id: "default", name: "Default", email: "default@example.com" };
129
+
130
+ fake.onMethod("fetchUser").returnValue(user);
131
+
132
+ expect(await fake.fetchUser("any-id")).toEqual(user);
133
+ expect(await fake.fetchUser("another-id")).toEqual(user);
134
+ });
135
+
136
+ it("should be overridden by arg-specific stub", async () => {
137
+ const defaultUser: User = { id: "default", name: "Default", email: "default@example.com" };
138
+ const specificUser: User = { id: "1", name: "John", email: "john@example.com" };
139
+
140
+ fake.onMethod("fetchUser").returnValue(defaultUser);
141
+ fake.onMethod("fetchUser").withArgs("1").returnValue(specificUser);
142
+
143
+ expect(await fake.fetchUser("1")).toEqual(specificUser);
144
+ expect(await fake.fetchUser("other")).toEqual(defaultUser);
145
+ });
146
+
147
+ it("should be used as fallback when arg-specific stub is not found", async () => {
148
+ const defaultUser: User = { id: "default", name: "Default", email: "default@example.com" };
149
+ const specificUser: User = { id: "1", name: "John", email: "john@example.com" };
150
+
151
+ fake.onMethod("fetchUser").withArgs("1").returnValue(specificUser);
152
+ fake.onMethod("fetchUser").returnValue(defaultUser);
153
+
154
+ expect(await fake.fetchUser("1")).toEqual(specificUser);
155
+ expect(await fake.fetchUser("unknown")).toEqual(defaultUser);
156
+ });
157
+
158
+ it("should be cleared when clear() is called", async () => {
159
+ const user: User = { id: "default", name: "Default", email: "default@example.com" };
160
+
161
+ fake.onMethod("fetchUser").returnValue(user);
162
+
163
+ fake.clear();
164
+
165
+ await expect(fake.fetchUser("any-id")).rejects.toThrow(FakeImplementationError);
166
+ });
167
+ });
168
+
169
+ describe("onMethod().throwError()", () => {
170
+ it("should throw the stubbed error for any arguments", async () => {
171
+ fake.onMethod("fetchUser").throwError(new Error("Service unavailable"));
172
+
173
+ await expect(fake.fetchUser("any-id")).rejects.toThrow("Service unavailable");
174
+ await expect(fake.fetchUser("another-id")).rejects.toThrow("Service unavailable");
175
+ });
176
+
177
+ it("should be overridden by arg-specific stub", async () => {
178
+ const user: User = { id: "1", name: "John", email: "john@example.com" };
179
+
180
+ fake.onMethod("fetchUser").throwError(new Error("Service unavailable"));
181
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user);
182
+
183
+ expect(await fake.fetchUser("1")).toEqual(user);
184
+ await expect(fake.fetchUser("other")).rejects.toThrow("Service unavailable");
185
+ });
186
+
187
+ it("should be used as fallback when arg-specific stub is not found", async () => {
188
+ const user: User = { id: "1", name: "John", email: "john@example.com" };
189
+
190
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user);
191
+ fake.onMethod("fetchUser").throwError(new Error("Not found"));
192
+
193
+ expect(await fake.fetchUser("1")).toEqual(user);
194
+ await expect(fake.fetchUser("unknown")).rejects.toThrow("Not found");
195
+ });
196
+
197
+ it("should be cleared when clear() is called", async () => {
198
+ fake.onMethod("fetchUser").throwError(new Error("Service unavailable"));
199
+
200
+ fake.clear();
201
+
202
+ await expect(fake.fetchUser("any-id")).rejects.toThrow(FakeImplementationError);
203
+ });
204
+ });
205
+
206
+ describe("onMethod().withArgs().execute()", () => {
207
+ it("should throw FakeImplementationError when no stub is configured", async () => {
208
+ await expect(fake.fetchUser("unstubbed")).rejects.toThrow(FakeImplementationError);
209
+ });
210
+
211
+ it("should throw FakeImplementationError with class name in message", async () => {
212
+ await expect(fake.fetchUser("unstubbed")).rejects.toThrow("fake");
213
+ });
214
+
215
+ it("should throw FakeImplementationError when args do not match and no global stub", async () => {
216
+ const user: User = { id: "1", name: "John", email: "john@example.com" };
217
+
218
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user);
219
+
220
+ await expect(fake.fetchUser("2")).rejects.toThrow(FakeImplementationError);
221
+ });
222
+ });
223
+
224
+ describe("clear()", () => {
225
+ it("should remove all stubbed configurations", async () => {
226
+ const user: User = { id: "1", name: "John", email: "john@example.com" };
227
+
228
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user);
229
+
230
+ expect(await fake.fetchUser("1")).toEqual(user);
231
+
232
+ fake.clear();
233
+
234
+ await expect(fake.fetchUser("1")).rejects.toThrow(FakeImplementationError);
235
+ });
236
+
237
+ it("should allow re-stubbing after clear", async () => {
238
+ const user1: User = { id: "1", name: "John", email: "john@example.com" };
239
+ const user2: User = { id: "1", name: "Jane", email: "jane@example.com" };
240
+
241
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user1);
242
+ fake.clear();
243
+ fake.onMethod("fetchUser").withArgs("1").returnValue(user2);
244
+
245
+ const result = await fake.fetchUser("1");
246
+
247
+ expect(result).toEqual(user2);
248
+ });
249
+ });
250
+ });