@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,170 @@
|
|
|
1
|
+
import { FakeImplementationError } from "../core/errors";
|
|
2
|
+
import { hashKey } from "../utils";
|
|
3
|
+
|
|
4
|
+
/** Extracts method names from a type as string literal union */
|
|
5
|
+
type MethodKeys<T> = { [K in keyof T]: T[K] extends (...args: never[]) => unknown ? K : never }[keyof T] & string;
|
|
6
|
+
|
|
7
|
+
/** Extracts the argument types of a method */
|
|
8
|
+
type MethodArgs<T, K extends keyof T> = T[K] extends (...args: infer P) => unknown ? P : never;
|
|
9
|
+
|
|
10
|
+
/** Extracts the awaited return value of a method (unwraps Promise) */
|
|
11
|
+
type MethodReturn<T, K extends keyof T> = T[K] extends (...args: never[]) => infer R ? Awaited<R> : never;
|
|
12
|
+
|
|
13
|
+
/** Extracts the raw return type of a method (preserves Promise) */
|
|
14
|
+
type MethodReturnType<T, K extends keyof T> = T[K] extends (...args: never[]) => infer R ? R : never;
|
|
15
|
+
|
|
16
|
+
type StubEntry<TResult> = { type: "result"; value: TResult } | { type: "error"; error: Error };
|
|
17
|
+
type StubStore<TSelf> = Map<string, StubEntry<MethodReturn<TSelf, MethodKeys<TSelf>>>>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Factory for generating stub keys.
|
|
21
|
+
* Centralizes the logic for global (method-level) and specific (args-level) keys.
|
|
22
|
+
*/
|
|
23
|
+
class StubKeyFactory<TSelf, K extends MethodKeys<TSelf>> {
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly methodName: K,
|
|
26
|
+
private readonly args?: MethodArgs<TSelf, K>,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
/** Global key for method-level stubs (applies to any args) */
|
|
30
|
+
global(): string {
|
|
31
|
+
return this.methodName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Specific key for arg-specific stubs */
|
|
35
|
+
specific(): string {
|
|
36
|
+
return hashKey({ methodName: this.methodName, params: this.args });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builder for specifying method arguments or global method behavior.
|
|
42
|
+
* Part of the fluent API: `stub.onMethod("name").withArgs(...)` or `stub.onMethod("name").throwError(...)`
|
|
43
|
+
*/
|
|
44
|
+
class StubArgsBuilder<TSelf, K extends MethodKeys<TSelf>> {
|
|
45
|
+
private readonly keyFactory: StubKeyFactory<TSelf, K>;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly store: StubStore<TSelf>,
|
|
49
|
+
private readonly methodName: K,
|
|
50
|
+
private readonly className: string,
|
|
51
|
+
) {
|
|
52
|
+
this.keyFactory = new StubKeyFactory(methodName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Specifies the arguments for the stubbed method call.
|
|
57
|
+
* @param args - The arguments to match when the method is called
|
|
58
|
+
*/
|
|
59
|
+
withArgs(...args: MethodArgs<TSelf, K>): StubResultBuilder<TSelf, K> {
|
|
60
|
+
return new StubResultBuilder(this.store, this.methodName, args, this.className);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stubs the method to return a value for any arguments.
|
|
65
|
+
* @param result - The value to return regardless of arguments
|
|
66
|
+
*/
|
|
67
|
+
returnValue(result: MethodReturn<TSelf, K>): void {
|
|
68
|
+
this.store.set(this.keyFactory.global(), { type: "result", value: result });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Stubs the method to throw an error for any arguments.
|
|
73
|
+
* @param error - The error to throw regardless of arguments
|
|
74
|
+
*/
|
|
75
|
+
throwError(error: Error): void {
|
|
76
|
+
this.store.set(this.keyFactory.global(), { type: "error", error });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builder for defining stub behavior or executing the stub.
|
|
82
|
+
* Part of the fluent API: `stub.onMethod("name").withArgs(...).returnValue(...)`
|
|
83
|
+
*/
|
|
84
|
+
class StubResultBuilder<TSelf, K extends MethodKeys<TSelf>> {
|
|
85
|
+
private readonly keyFactory: StubKeyFactory<TSelf, K>;
|
|
86
|
+
|
|
87
|
+
constructor(
|
|
88
|
+
private readonly store: StubStore<TSelf>,
|
|
89
|
+
methodName: K,
|
|
90
|
+
args: MethodArgs<TSelf, K>,
|
|
91
|
+
private readonly className: string,
|
|
92
|
+
) {
|
|
93
|
+
this.keyFactory = new StubKeyFactory(methodName, args);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Stubs the method to return a value when called with the specified arguments.
|
|
98
|
+
* @param result - The value to return
|
|
99
|
+
*/
|
|
100
|
+
returnValue(result: MethodReturn<TSelf, K>): void {
|
|
101
|
+
this.store.set(this.keyFactory.specific(), { type: "result", value: result });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stubs the method to throw an error when called with the specified arguments.
|
|
106
|
+
* @param error - The error to throw
|
|
107
|
+
*/
|
|
108
|
+
throwError(error: Error): void {
|
|
109
|
+
this.store.set(this.keyFactory.specific(), { type: "error", error });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Executes the stub and returns the configured result or throws the configured error.
|
|
114
|
+
* @throws {FakeImplementationError} If no stub is configured for the given method and arguments
|
|
115
|
+
* @throws The configured error if `throwError` was used (with or without args)
|
|
116
|
+
*/
|
|
117
|
+
execute(): MethodReturnType<TSelf, K> {
|
|
118
|
+
const entry = this.store.get(this.keyFactory.specific()) ?? this.store.get(this.keyFactory.global());
|
|
119
|
+
|
|
120
|
+
if (!entry) {
|
|
121
|
+
throw new FakeImplementationError(this.className, this.keyFactory.global());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (entry.type === "error") {
|
|
125
|
+
throw entry.error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return entry.value as MethodReturnType<TSelf, K>;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Abstract base class for creating type-safe method stubs.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* class FakeUserService extends Stub<FakeUserService> implements UserService {
|
|
138
|
+
* async fetchUser(id: string): Promise<User> {
|
|
139
|
+
* return this.onMethod("fetchUser").withArgs(id).execute();
|
|
140
|
+
* }
|
|
141
|
+
* }
|
|
142
|
+
*
|
|
143
|
+
* // In tests:
|
|
144
|
+
* const fake = new FakeUserService();
|
|
145
|
+
*
|
|
146
|
+
* // Stub specific args
|
|
147
|
+
* fake.onMethod("fetchUser").withArgs("123").returnValue({ id: "123", name: "John" });
|
|
148
|
+
* fake.onMethod("fetchUser").withArgs("invalid").throwError(new NotFoundError());
|
|
149
|
+
*
|
|
150
|
+
* // Stub for any args (global fallback)
|
|
151
|
+
* fake.onMethod("fetchUser").returnValue({ id: "default", name: "Default" });
|
|
152
|
+
* fake.onMethod("fetchUser").throwError(new ServiceUnavailableError());
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export abstract class Stub<TSelf = unknown> {
|
|
156
|
+
protected store: StubStore<TSelf> = new Map();
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Starts the stub configuration for a method.
|
|
160
|
+
* @param methodName - The name of the method to stub
|
|
161
|
+
*/
|
|
162
|
+
onMethod<K extends MethodKeys<TSelf>>(methodName: K): StubArgsBuilder<TSelf, K> {
|
|
163
|
+
return new StubArgsBuilder(this.store, methodName, this.constructor.name);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Clears all stubbed configurations. */
|
|
167
|
+
clear(): void {
|
|
168
|
+
this.store.clear();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -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,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,170 @@
|
|
|
1
|
+
import { FakeImplementationError } from "../../core/errors";
|
|
2
|
+
import { hashKey } from "../../utils";
|
|
3
|
+
|
|
4
|
+
/** Extracts method names from a type as string literal union */
|
|
5
|
+
type MethodKeys<T> = { [K in keyof T]: T[K] extends (...args: never[]) => unknown ? K : never }[keyof T] & string;
|
|
6
|
+
|
|
7
|
+
/** Extracts the argument types of a method */
|
|
8
|
+
type MethodArgs<T, K extends keyof T> = T[K] extends (...args: infer P) => unknown ? P : never;
|
|
9
|
+
|
|
10
|
+
/** Extracts the awaited return value of a method (unwraps Promise) */
|
|
11
|
+
type MethodReturn<T, K extends keyof T> = T[K] extends (...args: never[]) => infer R ? Awaited<R> : never;
|
|
12
|
+
|
|
13
|
+
/** Extracts the raw return type of a method (preserves Promise) */
|
|
14
|
+
type MethodReturnType<T, K extends keyof T> = T[K] extends (...args: never[]) => infer R ? R : never;
|
|
15
|
+
|
|
16
|
+
type StubEntry<TResult> = { type: "result"; value: TResult } | { type: "error"; error: Error };
|
|
17
|
+
type StubStore<TSelf> = Map<string, StubEntry<MethodReturn<TSelf, MethodKeys<TSelf>>>>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Factory for generating stub keys.
|
|
21
|
+
* Centralizes the logic for global (method-level) and specific (args-level) keys.
|
|
22
|
+
*/
|
|
23
|
+
class StubKeyFactory<TSelf, K extends MethodKeys<TSelf>> {
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly methodName: K,
|
|
26
|
+
private readonly args?: MethodArgs<TSelf, K>,
|
|
27
|
+
) {}
|
|
28
|
+
|
|
29
|
+
/** Global key for method-level stubs (applies to any args) */
|
|
30
|
+
global(): string {
|
|
31
|
+
return this.methodName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Specific key for arg-specific stubs */
|
|
35
|
+
specific(): string {
|
|
36
|
+
return hashKey({ methodName: this.methodName, params: this.args });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builder for specifying method arguments or global method behavior.
|
|
42
|
+
* Part of the fluent API: `stub.onMethod("name").withArgs(...)` or `stub.onMethod("name").throwError(...)`
|
|
43
|
+
*/
|
|
44
|
+
class StubArgsBuilder<TSelf, K extends MethodKeys<TSelf>> {
|
|
45
|
+
private readonly keyFactory: StubKeyFactory<TSelf, K>;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
private readonly store: StubStore<TSelf>,
|
|
49
|
+
private readonly methodName: K,
|
|
50
|
+
private readonly className: string,
|
|
51
|
+
) {
|
|
52
|
+
this.keyFactory = new StubKeyFactory(methodName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Specifies the arguments for the stubbed method call.
|
|
57
|
+
* @param args - The arguments to match when the method is called
|
|
58
|
+
*/
|
|
59
|
+
withArgs(...args: MethodArgs<TSelf, K>): StubResultBuilder<TSelf, K> {
|
|
60
|
+
return new StubResultBuilder(this.store, this.methodName, args, this.className);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stubs the method to return a value for any arguments.
|
|
65
|
+
* @param result - The value to return regardless of arguments
|
|
66
|
+
*/
|
|
67
|
+
returnValue(result: MethodReturn<TSelf, K>): void {
|
|
68
|
+
this.store.set(this.keyFactory.global(), { type: "result", value: result });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Stubs the method to throw an error for any arguments.
|
|
73
|
+
* @param error - The error to throw regardless of arguments
|
|
74
|
+
*/
|
|
75
|
+
throwError(error: Error): void {
|
|
76
|
+
this.store.set(this.keyFactory.global(), { type: "error", error });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builder for defining stub behavior or executing the stub.
|
|
82
|
+
* Part of the fluent API: `stub.onMethod("name").withArgs(...).returnValue(...)`
|
|
83
|
+
*/
|
|
84
|
+
class StubResultBuilder<TSelf, K extends MethodKeys<TSelf>> {
|
|
85
|
+
private readonly keyFactory: StubKeyFactory<TSelf, K>;
|
|
86
|
+
|
|
87
|
+
constructor(
|
|
88
|
+
private readonly store: StubStore<TSelf>,
|
|
89
|
+
methodName: K,
|
|
90
|
+
args: MethodArgs<TSelf, K>,
|
|
91
|
+
private readonly className: string,
|
|
92
|
+
) {
|
|
93
|
+
this.keyFactory = new StubKeyFactory(methodName, args);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Stubs the method to return a value when called with the specified arguments.
|
|
98
|
+
* @param result - The value to return
|
|
99
|
+
*/
|
|
100
|
+
returnValue(result: MethodReturn<TSelf, K>): void {
|
|
101
|
+
this.store.set(this.keyFactory.specific(), { type: "result", value: result });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stubs the method to throw an error when called with the specified arguments.
|
|
106
|
+
* @param error - The error to throw
|
|
107
|
+
*/
|
|
108
|
+
throwError(error: Error): void {
|
|
109
|
+
this.store.set(this.keyFactory.specific(), { type: "error", error });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Executes the stub and returns the configured result or throws the configured error.
|
|
114
|
+
* @throws {FakeImplementationError} If no stub is configured for the given method and arguments
|
|
115
|
+
* @throws The configured error if `throwError` was used (with or without args)
|
|
116
|
+
*/
|
|
117
|
+
execute(): MethodReturnType<TSelf, K> {
|
|
118
|
+
const entry = this.store.get(this.keyFactory.specific()) ?? this.store.get(this.keyFactory.global());
|
|
119
|
+
|
|
120
|
+
if (!entry) {
|
|
121
|
+
throw new FakeImplementationError(this.className, this.keyFactory.global());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (entry.type === "error") {
|
|
125
|
+
throw entry.error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return entry.value as MethodReturnType<TSelf, K>;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Abstract base class for creating type-safe method stubs.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* class FakeUserService extends Stub<FakeUserService> implements UserService {
|
|
138
|
+
* async fetchUser(id: string): Promise<User> {
|
|
139
|
+
* return this.onMethod("fetchUser").withArgs(id).execute();
|
|
140
|
+
* }
|
|
141
|
+
* }
|
|
142
|
+
*
|
|
143
|
+
* // In tests:
|
|
144
|
+
* const fake = new FakeUserService();
|
|
145
|
+
*
|
|
146
|
+
* // Stub specific args
|
|
147
|
+
* fake.onMethod("fetchUser").withArgs("123").returnValue({ id: "123", name: "John" });
|
|
148
|
+
* fake.onMethod("fetchUser").withArgs("invalid").throwError(new NotFoundError());
|
|
149
|
+
*
|
|
150
|
+
* // Stub for any args (global fallback)
|
|
151
|
+
* fake.onMethod("fetchUser").returnValue({ id: "default", name: "Default" });
|
|
152
|
+
* fake.onMethod("fetchUser").throwError(new ServiceUnavailableError());
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export abstract class Stub<TSelf = unknown> {
|
|
156
|
+
protected store: StubStore<TSelf> = new Map();
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Starts the stub configuration for a method.
|
|
160
|
+
* @param methodName - The name of the method to stub
|
|
161
|
+
*/
|
|
162
|
+
onMethod<K extends MethodKeys<TSelf>>(methodName: K): StubArgsBuilder<TSelf, K> {
|
|
163
|
+
return new StubArgsBuilder(this.store, methodName, this.constructor.name);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Clears all stubbed configurations. */
|
|
167
|
+
clear(): void {
|
|
168
|
+
this.store.clear();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { chunk } from "./array";
|
|
2
|
+
|
|
3
|
+
const generateArray = (length: number): number[] => new Array<number>(length).fill(0);
|
|
4
|
+
|
|
5
|
+
describe("chunk", () => {
|
|
6
|
+
it("should split 1 array 100 items into 10 array 10 items", () => {
|
|
7
|
+
const ARRAY = generateArray(100);
|
|
8
|
+
|
|
9
|
+
const chunks = chunk(ARRAY, 10);
|
|
10
|
+
|
|
11
|
+
expect(chunks.length).toEqual(10);
|
|
12
|
+
chunks.forEach((chunk) => expect(chunk.length).toEqual(10));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should split 1 array 95 items into 9 array 10 items and 1 array 5 items", () => {
|
|
16
|
+
const ARRAY = generateArray(95);
|
|
17
|
+
|
|
18
|
+
const chunks = chunk(ARRAY, 10);
|
|
19
|
+
|
|
20
|
+
expect(chunks.length).toEqual(10);
|
|
21
|
+
chunks.forEach((chunk, index) => {
|
|
22
|
+
if (index !== 9) {
|
|
23
|
+
expect(chunk.length).toEqual(10);
|
|
24
|
+
} else {
|
|
25
|
+
expect(chunk.length).toEqual(5);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
|
|
4
|
+
export const uniqBy = _.uniqBy;
|
|
5
|
+
export const maxBy = _.maxBy;
|
|
6
|
+
export const minBy = _.minBy;
|
|
7
|
+
export const chunk = _.chunk;
|
|
8
|
+
export const shuffle = _.shuffle;
|
|
9
|
+
export const sum = _.sum;
|
|
10
|
+
export const sortBy = _.sortBy;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { base64ToString, stringToBase64 } from "./base64";
|
|
2
|
+
|
|
3
|
+
const UNICODE_STRING = "foo © bar 𝌆 baz";
|
|
4
|
+
const ENCODED_BASE64 = "Zm9vIMKpIGJhciDwnYyGIGJheg==";
|
|
5
|
+
|
|
6
|
+
describe("base64", () => {
|
|
7
|
+
describe("base64ToString", () => {
|
|
8
|
+
it("should decoded base64 to string", () => {
|
|
9
|
+
expect(base64ToString(ENCODED_BASE64)).toEqual(UNICODE_STRING);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("stringToBase64", () => {
|
|
14
|
+
it("should encoded string to base64", () => {
|
|
15
|
+
expect(stringToBase64(UNICODE_STRING)).toEqual(ENCODED_BASE64);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { decode as decodeBase64, encode as encodeBase64 } from "base-64";
|
|
2
|
+
import { decode as decodeUtf8, encode as encodeUtf8 } from "utf8";
|
|
3
|
+
|
|
4
|
+
export const stringToBase64 = (str: string) => encodeBase64(encodeUtf8(str));
|
|
5
|
+
|
|
6
|
+
export const base64ToString = (base64: string) => decodeUtf8(decodeBase64(base64));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type RecursiveKeyOf<TObj extends object> = {
|
|
2
|
+
[TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<TObj[TKey], `${TKey}`>;
|
|
3
|
+
}[keyof TObj & (string | number)];
|
|
4
|
+
|
|
5
|
+
type RecursiveKeyOfInner<TObj extends object> = {
|
|
6
|
+
[TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<TObj[TKey], `['${TKey}']` | `.${TKey}`>;
|
|
7
|
+
}[keyof TObj & (string | number)];
|
|
8
|
+
|
|
9
|
+
type RecursiveKeyOfHandleValue<TValue, Text extends string> = TValue extends unknown[]
|
|
10
|
+
? Text
|
|
11
|
+
: TValue extends object
|
|
12
|
+
? Text | `${Text}${RecursiveKeyOfInner<TValue>}`
|
|
13
|
+
: Text;
|
package/src/utils/csv.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
import "reflect-metadata";
|
|
3
|
+
import { Transform } from "stream";
|
|
4
|
+
|
|
5
|
+
const META_KEY = Symbol("csv:columns");
|
|
6
|
+
|
|
7
|
+
type ColumnMeta = { propertyKey: string; displayName: string };
|
|
8
|
+
|
|
9
|
+
export function CSVFileRecord(): ClassDecorator {
|
|
10
|
+
return function (target: object) {
|
|
11
|
+
Reflect.defineMetadata(META_KEY, [], target);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Column(displayName: string): PropertyDecorator {
|
|
16
|
+
return function (target: object, propertyKey: string | symbol) {
|
|
17
|
+
const columns: ColumnMeta[] = Reflect.getMetadata(META_KEY, target) ?? [];
|
|
18
|
+
columns.push({ propertyKey: propertyKey as string, displayName });
|
|
19
|
+
Reflect.defineMetadata(META_KEY, columns, target);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class CSVStream extends Transform {
|
|
24
|
+
private headerWritten = false;
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super({ objectMode: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_transform(data: Record<string, string>, _enc: string, done: () => void) {
|
|
31
|
+
const columns = Reflect.getMetadata(META_KEY, data) as ColumnMeta[] | undefined;
|
|
32
|
+
if (!columns) {throw new Error("data is not a valid record");}
|
|
33
|
+
|
|
34
|
+
if (!this.headerWritten) {
|
|
35
|
+
this.push(columns.map((c) => c.displayName).join(",") + "\r\n");
|
|
36
|
+
this.headerWritten = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const row = columns.map((c) => (data[c.propertyKey] ?? "").toString().replace(/"/g, '""'));
|
|
40
|
+
this.push(`"${row.join('","')}"\r\n`);
|
|
41
|
+
|
|
42
|
+
done();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_flush(done: () => void) {
|
|
46
|
+
this.headerWritten = false;
|
|
47
|
+
done();
|
|
48
|
+
}
|
|
49
|
+
}
|