@martel/calyx 1.12.0 → 1.13.0
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/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/cli/index.ts +7 -1
- package/src/config/config.module.ts +16 -2
- package/src/config/config.service.ts +20 -6
- package/src/core/container.ts +340 -154
- package/src/core/testing-module.ts +4 -0
- package/src/cqrs/cqrs.ts +93 -4
- package/src/database/sequelize.module.ts +239 -0
- package/src/event-emitter/decorators.ts +2 -2
- package/src/event-emitter/event-emitter.ts +3 -0
- package/src/http/application.ts +135 -6
- package/src/http/decorators.ts +21 -1
- package/src/http/exceptions.ts +97 -0
- package/src/http/factory.ts +3 -0
- package/src/http/router.ts +27 -4
- package/src/index.ts +1 -0
- package/src/microservices/exceptions.ts +10 -0
- package/src/microservices/index.ts +1 -0
- package/src/queue/queue.module.ts +73 -5
- package/src/terminus/terminus.ts +75 -2
- package/src/validation/compiler.ts +133 -10
- package/src/validation/decorators.ts +164 -2
- package/src/websockets/exceptions.ts +10 -0
- package/src/websockets/index.ts +1 -0
- package/tests/circular-di.test.ts +151 -0
- package/tests/di.test.ts +10 -2
- package/tests/nestjs-parity.test.ts +255 -0
|
@@ -12,6 +12,48 @@ function registerValidationRule(type: string, target: any, propertyKey: string,
|
|
|
12
12
|
Reflect.defineMetadata('calyx:validation_rules', existing, target.constructor);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
// 1. Conditional & Basic
|
|
16
|
+
export function IsOptional(): PropertyDecorator {
|
|
17
|
+
return (target, propertyKey) => registerValidationRule('optional', target, String(propertyKey));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function IsDefined(): PropertyDecorator {
|
|
21
|
+
return (target, propertyKey) => registerValidationRule('isDefined', target, String(propertyKey));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Equals(value: any): PropertyDecorator {
|
|
25
|
+
return (target, propertyKey) => registerValidationRule('equals', target, String(propertyKey), [value]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function NotEquals(value: any): PropertyDecorator {
|
|
29
|
+
return (target, propertyKey) => registerValidationRule('notEquals', target, String(propertyKey), [value]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function IsEmpty(): PropertyDecorator {
|
|
33
|
+
return (target, propertyKey) => registerValidationRule('isEmpty', target, String(propertyKey));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function IsNotEmpty(): PropertyDecorator {
|
|
37
|
+
return (target, propertyKey) => registerValidationRule('isNotEmpty', target, String(propertyKey));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function IsIn(values: any[]): PropertyDecorator {
|
|
41
|
+
return (target, propertyKey) => registerValidationRule('isIn', target, String(propertyKey), [values]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function IsNotIn(values: any[]): PropertyDecorator {
|
|
45
|
+
return (target, propertyKey) => registerValidationRule('isNotIn', target, String(propertyKey), [values]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Core Types
|
|
49
|
+
export function IsBoolean(): PropertyDecorator {
|
|
50
|
+
return (target, propertyKey) => registerValidationRule('isBoolean', target, String(propertyKey));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function IsDate(): PropertyDecorator {
|
|
54
|
+
return (target, propertyKey) => registerValidationRule('isDate', target, String(propertyKey));
|
|
55
|
+
}
|
|
56
|
+
|
|
15
57
|
export function IsString(): PropertyDecorator {
|
|
16
58
|
return (target, propertyKey) => registerValidationRule('string', target, String(propertyKey));
|
|
17
59
|
}
|
|
@@ -20,14 +62,134 @@ export function IsNumber(): PropertyDecorator {
|
|
|
20
62
|
return (target, propertyKey) => registerValidationRule('number', target, String(propertyKey));
|
|
21
63
|
}
|
|
22
64
|
|
|
23
|
-
export function
|
|
24
|
-
return (target, propertyKey) => registerValidationRule('
|
|
65
|
+
export function IsInt(): PropertyDecorator {
|
|
66
|
+
return (target, propertyKey) => registerValidationRule('isInt', target, String(propertyKey));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function IsArray(): PropertyDecorator {
|
|
70
|
+
return (target, propertyKey) => registerValidationRule('isArray', target, String(propertyKey));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function IsEnum(entity: any): PropertyDecorator {
|
|
74
|
+
return (target, propertyKey) => registerValidationRule('isEnum', target, String(propertyKey), [entity]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function IsObject(): PropertyDecorator {
|
|
78
|
+
return (target, propertyKey) => registerValidationRule('isObject', target, String(propertyKey));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 3. Number Constraints
|
|
82
|
+
export function IsPositive(): PropertyDecorator {
|
|
83
|
+
return (target, propertyKey) => registerValidationRule('isPositive', target, String(propertyKey));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function IsNegative(): PropertyDecorator {
|
|
87
|
+
return (target, propertyKey) => registerValidationRule('isNegative', target, String(propertyKey));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function Min(num: number): PropertyDecorator {
|
|
91
|
+
return (target, propertyKey) => registerValidationRule('min', target, String(propertyKey), [num]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function Max(num: number): PropertyDecorator {
|
|
95
|
+
return (target, propertyKey) => registerValidationRule('max', target, String(propertyKey), [num]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. String Constraints
|
|
99
|
+
export function Contains(str: string): PropertyDecorator {
|
|
100
|
+
return (target, propertyKey) => registerValidationRule('contains', target, String(propertyKey), [str]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function NotContains(str: string): PropertyDecorator {
|
|
104
|
+
return (target, propertyKey) => registerValidationRule('notContains', target, String(propertyKey), [str]);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function IsAlpha(): PropertyDecorator {
|
|
108
|
+
return (target, propertyKey) => registerValidationRule('isAlpha', target, String(propertyKey));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function IsAlphanumeric(): PropertyDecorator {
|
|
112
|
+
return (target, propertyKey) => registerValidationRule('isAlphanumeric', target, String(propertyKey));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function IsDecimal(): PropertyDecorator {
|
|
116
|
+
return (target, propertyKey) => registerValidationRule('isDecimal', target, String(propertyKey));
|
|
25
117
|
}
|
|
26
118
|
|
|
27
119
|
export function IsEmail(): PropertyDecorator {
|
|
28
120
|
return (target, propertyKey) => registerValidationRule('email', target, String(propertyKey));
|
|
29
121
|
}
|
|
30
122
|
|
|
123
|
+
export function IsUrl(): PropertyDecorator {
|
|
124
|
+
return (target, propertyKey) => registerValidationRule('isUrl', target, String(propertyKey));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function IsIP(): PropertyDecorator {
|
|
128
|
+
return (target, propertyKey) => registerValidationRule('isIP', target, String(propertyKey));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function IsPort(): PropertyDecorator {
|
|
132
|
+
return (target, propertyKey) => registerValidationRule('isPort', target, String(propertyKey));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function IsJSON(): PropertyDecorator {
|
|
136
|
+
return (target, propertyKey) => registerValidationRule('isJSON', target, String(propertyKey));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function IsLowercase(): PropertyDecorator {
|
|
140
|
+
return (target, propertyKey) => registerValidationRule('isLowercase', target, String(propertyKey));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function IsUppercase(): PropertyDecorator {
|
|
144
|
+
return (target, propertyKey) => registerValidationRule('isUppercase', target, String(propertyKey));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function IsNumberString(): PropertyDecorator {
|
|
148
|
+
return (target, propertyKey) => registerValidationRule('isNumberString', target, String(propertyKey));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function IsUUID(): PropertyDecorator {
|
|
152
|
+
return (target, propertyKey) => registerValidationRule('isUUID', target, String(propertyKey));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function IsDateString(): PropertyDecorator {
|
|
156
|
+
return (target, propertyKey) => registerValidationRule('isDateString', target, String(propertyKey));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function Length(min: number, max: number): PropertyDecorator {
|
|
160
|
+
return (target, propertyKey) => registerValidationRule('length', target, String(propertyKey), [min, max]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function MinLength(min: number): PropertyDecorator {
|
|
164
|
+
return (target, propertyKey) => registerValidationRule('minLength', target, String(propertyKey), [min]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function MaxLength(max: number): PropertyDecorator {
|
|
168
|
+
return (target, propertyKey) => registerValidationRule('maxLength', target, String(propertyKey), [max]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function Matches(regex: RegExp): PropertyDecorator {
|
|
172
|
+
return (target, propertyKey) => registerValidationRule('matches', target, String(propertyKey), [regex]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 5. Array Constraints
|
|
176
|
+
export function ArrayNotEmpty(): PropertyDecorator {
|
|
177
|
+
return (target, propertyKey) => registerValidationRule('arrayNotEmpty', target, String(propertyKey));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function ArrayMinSize(num: number): PropertyDecorator {
|
|
181
|
+
return (target, propertyKey) => registerValidationRule('arrayMinSize', target, String(propertyKey), [num]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function ArrayMaxSize(num: number): PropertyDecorator {
|
|
185
|
+
return (target, propertyKey) => registerValidationRule('arrayMaxSize', target, String(propertyKey), [num]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function ArrayUnique(): PropertyDecorator {
|
|
189
|
+
return (target, propertyKey) => registerValidationRule('arrayUnique', target, String(propertyKey));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Class-Transformer Aliases/Overrides
|
|
31
193
|
export function Expose(): PropertyDecorator {
|
|
32
194
|
return (target, propertyKey) => {
|
|
33
195
|
const constructor = target.constructor;
|
package/src/websockets/index.ts
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
Module,
|
|
4
|
+
Injectable,
|
|
5
|
+
Inject,
|
|
6
|
+
forwardRef,
|
|
7
|
+
CalyxContainer,
|
|
8
|
+
Scope,
|
|
9
|
+
REQUEST,
|
|
10
|
+
} from '../src/index.ts';
|
|
11
|
+
|
|
12
|
+
describe('Circular DI and Recursive Dynamic Modules', () => {
|
|
13
|
+
test('should resolve circular dependencies with property-based injection', () => {
|
|
14
|
+
@Injectable()
|
|
15
|
+
class ClassA {
|
|
16
|
+
@Inject(forwardRef(() => ClassB))
|
|
17
|
+
public b: any;
|
|
18
|
+
|
|
19
|
+
getName() {
|
|
20
|
+
return 'A';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Injectable()
|
|
25
|
+
class ClassB {
|
|
26
|
+
@Inject(forwardRef(() => ClassA))
|
|
27
|
+
public a: any;
|
|
28
|
+
|
|
29
|
+
getName() {
|
|
30
|
+
return 'B';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Module({
|
|
35
|
+
providers: [ClassA, ClassB],
|
|
36
|
+
})
|
|
37
|
+
class RootModule {}
|
|
38
|
+
|
|
39
|
+
const container = new CalyxContainer();
|
|
40
|
+
container.bootstrap(RootModule);
|
|
41
|
+
|
|
42
|
+
const a = container.getGlobalOrAnyInstance(ClassA);
|
|
43
|
+
const b = container.getGlobalOrAnyInstance(ClassB);
|
|
44
|
+
|
|
45
|
+
expect(a.b.getName()).toBe('B');
|
|
46
|
+
expect(b.a.getName()).toBe('A');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('should resolve recursive dynamic module imports', () => {
|
|
50
|
+
@Injectable()
|
|
51
|
+
class DatabaseConfig {
|
|
52
|
+
getUri() {
|
|
53
|
+
return 'mongodb://localhost';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Module({
|
|
58
|
+
providers: [DatabaseConfig],
|
|
59
|
+
exports: [DatabaseConfig],
|
|
60
|
+
})
|
|
61
|
+
class ConfigModule {}
|
|
62
|
+
|
|
63
|
+
@Module({})
|
|
64
|
+
class DatabaseModule {
|
|
65
|
+
static forRoot() {
|
|
66
|
+
return {
|
|
67
|
+
module: DatabaseModule,
|
|
68
|
+
imports: [ConfigModule],
|
|
69
|
+
providers: [
|
|
70
|
+
{
|
|
71
|
+
provide: 'DATABASE_URI',
|
|
72
|
+
useFactory: (cfg: DatabaseConfig) => cfg.getUri(),
|
|
73
|
+
inject: [DatabaseConfig],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
exports: ['DATABASE_URI'],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Injectable()
|
|
82
|
+
class AppService {
|
|
83
|
+
constructor(@Inject('DATABASE_URI') public uri: string) {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Module({
|
|
87
|
+
imports: [DatabaseModule.forRoot()],
|
|
88
|
+
providers: [AppService],
|
|
89
|
+
})
|
|
90
|
+
class AppModule {}
|
|
91
|
+
|
|
92
|
+
const container = new CalyxContainer();
|
|
93
|
+
container.bootstrap(AppModule);
|
|
94
|
+
|
|
95
|
+
const appService = container.getGlobalOrAnyInstance(AppService);
|
|
96
|
+
expect(appService.uri).toBe('mongodb://localhost');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should support request-scoped circular dependency JIT DI compilation', async () => {
|
|
100
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
101
|
+
class ReqA {
|
|
102
|
+
constructor(
|
|
103
|
+
@Inject(REQUEST) public req: any,
|
|
104
|
+
@Inject(forwardRef(() => ReqB)) public b: any
|
|
105
|
+
) {}
|
|
106
|
+
|
|
107
|
+
sayHi() {
|
|
108
|
+
return 'A';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
113
|
+
class ReqB {
|
|
114
|
+
constructor(
|
|
115
|
+
@Inject(REQUEST) public req: any,
|
|
116
|
+
@Inject(forwardRef(() => ReqA)) public a: any
|
|
117
|
+
) {}
|
|
118
|
+
|
|
119
|
+
sayHi() {
|
|
120
|
+
return 'B';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A controller is needed because JIT compiling factories runs for controllers
|
|
125
|
+
@Injectable()
|
|
126
|
+
class ReqController {
|
|
127
|
+
constructor(
|
|
128
|
+
public a: ReqA,
|
|
129
|
+
public b: ReqB
|
|
130
|
+
) {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Module({
|
|
134
|
+
providers: [ReqA, ReqB],
|
|
135
|
+
controllers: [ReqController],
|
|
136
|
+
})
|
|
137
|
+
class RootModule {}
|
|
138
|
+
|
|
139
|
+
const container = new CalyxContainer();
|
|
140
|
+
container.bootstrap(RootModule);
|
|
141
|
+
|
|
142
|
+
// Get the JIT factory or resolve in a request context
|
|
143
|
+
const reqContext = new Map<any, any>();
|
|
144
|
+
reqContext.set(REQUEST, { url: '/test' });
|
|
145
|
+
|
|
146
|
+
const ctrl = container.resolveControllerInRequestContext(RootModule, ReqController, reqContext);
|
|
147
|
+
expect(ctrl.a.req).toEqual({ url: '/test' });
|
|
148
|
+
expect(ctrl.a.b.sayHi()).toBe('B');
|
|
149
|
+
expect(ctrl.b.a.sayHi()).toBe('A');
|
|
150
|
+
});
|
|
151
|
+
});
|
package/tests/di.test.ts
CHANGED
|
@@ -167,15 +167,17 @@ describe('Dependency Injection System', () => {
|
|
|
167
167
|
expect(() => container.bootstrap(RootModule)).toThrow(/Cannot resolve dependency/);
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
test('should
|
|
170
|
+
test('should resolve circular dependencies using lazy proxies', () => {
|
|
171
171
|
@Injectable()
|
|
172
172
|
class ServiceA {
|
|
173
173
|
constructor(@Inject(forwardRef(() => ServiceB)) public b: any) {}
|
|
174
|
+
getValue() { return 'A'; }
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
@Injectable()
|
|
177
178
|
class ServiceB {
|
|
178
179
|
constructor(@Inject(forwardRef(() => ServiceA)) public a: any) {}
|
|
180
|
+
getValue() { return 'B'; }
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
@Module({
|
|
@@ -184,7 +186,13 @@ describe('Dependency Injection System', () => {
|
|
|
184
186
|
class RootModule {}
|
|
185
187
|
|
|
186
188
|
const container = new CalyxContainer();
|
|
187
|
-
|
|
189
|
+
container.bootstrap(RootModule);
|
|
190
|
+
|
|
191
|
+
const a = container.getGlobalOrAnyInstance(ServiceA);
|
|
192
|
+
const b = container.getGlobalOrAnyInstance(ServiceB);
|
|
193
|
+
|
|
194
|
+
expect(a.b.getValue()).toBe('B');
|
|
195
|
+
expect(b.a.getValue()).toBe('A');
|
|
188
196
|
});
|
|
189
197
|
|
|
190
198
|
test('should isolate module scopes and resolve exported providers', () => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { expect, test, describe } from 'bun:test';
|
|
2
|
+
import { filter, map } from 'rxjs/operators';
|
|
3
|
+
import { Observable } from 'rxjs';
|
|
2
4
|
import {
|
|
3
5
|
Test,
|
|
4
6
|
LazyModuleLoader,
|
|
@@ -30,6 +32,28 @@ import {
|
|
|
30
32
|
ICommandHandler,
|
|
31
33
|
IQueryHandler,
|
|
32
34
|
IEventHandler,
|
|
35
|
+
ConflictException,
|
|
36
|
+
ServiceUnavailableException,
|
|
37
|
+
UnprocessableEntityException,
|
|
38
|
+
WsException,
|
|
39
|
+
RpcException,
|
|
40
|
+
IsUUID,
|
|
41
|
+
Min,
|
|
42
|
+
Max,
|
|
43
|
+
IsBoolean,
|
|
44
|
+
Length,
|
|
45
|
+
ValidationCompiler,
|
|
46
|
+
registerAs,
|
|
47
|
+
ConfigModule,
|
|
48
|
+
SequelizeModule,
|
|
49
|
+
SequelizeModel,
|
|
50
|
+
BullModule,
|
|
51
|
+
UnhandledExceptionBus,
|
|
52
|
+
EventPublisher,
|
|
53
|
+
AggregateRoot,
|
|
54
|
+
Saga,
|
|
55
|
+
MemoryHealthIndicator,
|
|
56
|
+
TypeOrmHealthIndicator,
|
|
33
57
|
} from '../src/index.ts';
|
|
34
58
|
|
|
35
59
|
@Injectable()
|
|
@@ -269,4 +293,235 @@ describe('NestJS Parity Extensions', () => {
|
|
|
269
293
|
|
|
270
294
|
await moduleRef.close();
|
|
271
295
|
});
|
|
296
|
+
|
|
297
|
+
test('HTTP Exceptions: check new status codes', () => {
|
|
298
|
+
const exc1 = new ConflictException();
|
|
299
|
+
expect(exc1.getStatus()).toBe(409);
|
|
300
|
+
|
|
301
|
+
const exc2 = new ServiceUnavailableException();
|
|
302
|
+
expect(exc2.getStatus()).toBe(503);
|
|
303
|
+
|
|
304
|
+
const exc3 = new UnprocessableEntityException();
|
|
305
|
+
expect(exc3.getStatus()).toBe(422);
|
|
306
|
+
|
|
307
|
+
const wsExc = new WsException('error');
|
|
308
|
+
expect(wsExc.getError()).toBe('error');
|
|
309
|
+
|
|
310
|
+
const rpcExc = new RpcException('error');
|
|
311
|
+
expect(rpcExc.getError()).toBe('error');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('JIT Validation: check advanced decorators', () => {
|
|
315
|
+
class DTO {
|
|
316
|
+
@IsUUID()
|
|
317
|
+
uuid!: string;
|
|
318
|
+
|
|
319
|
+
@Min(10)
|
|
320
|
+
minVal!: number;
|
|
321
|
+
|
|
322
|
+
@Max(50)
|
|
323
|
+
maxVal!: number;
|
|
324
|
+
|
|
325
|
+
@IsBoolean()
|
|
326
|
+
boolVal!: boolean;
|
|
327
|
+
|
|
328
|
+
@Length(3, 8)
|
|
329
|
+
strLen!: string;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const validate = ValidationCompiler.compile(DTO);
|
|
333
|
+
|
|
334
|
+
const errs1 = validate({
|
|
335
|
+
uuid: 'abc',
|
|
336
|
+
minVal: 5,
|
|
337
|
+
maxVal: 60,
|
|
338
|
+
boolVal: 'not-bool' as any,
|
|
339
|
+
strLen: 'hi',
|
|
340
|
+
});
|
|
341
|
+
expect(errs1).not.toBeNull();
|
|
342
|
+
expect(errs1!.length).toBe(5);
|
|
343
|
+
|
|
344
|
+
const errs2 = validate({
|
|
345
|
+
uuid: '123e4567-e89b-12d3-a456-426614174000',
|
|
346
|
+
minVal: 15,
|
|
347
|
+
maxVal: 40,
|
|
348
|
+
boolVal: true,
|
|
349
|
+
strLen: 'hello',
|
|
350
|
+
});
|
|
351
|
+
expect(errs2).toBeNull();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('ConfigNamespaces: registerAs and dot-notation', () => {
|
|
355
|
+
const dbConfig = registerAs('database', () => ({
|
|
356
|
+
host: 'my-host',
|
|
357
|
+
port: 5432,
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
const module = ConfigModule.forRoot({
|
|
361
|
+
load: [dbConfig],
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const service = (module.providers[0] as any).useValue;
|
|
365
|
+
expect(service.get('database.host')).toBe('my-host');
|
|
366
|
+
expect(service.get('database.port')).toBe(5432);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('SequelizeModule: check database fallback model', async () => {
|
|
370
|
+
class User extends SequelizeModel {
|
|
371
|
+
name!: string;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const moduleRef = await Test.createTestingModule({
|
|
375
|
+
imports: [
|
|
376
|
+
SequelizeModule.forRoot({ storage: ':memory:' }),
|
|
377
|
+
SequelizeModule.forFeature([User]),
|
|
378
|
+
],
|
|
379
|
+
}).compile();
|
|
380
|
+
|
|
381
|
+
const injectedModel = moduleRef.get('Sequelize_Model_User') as any;
|
|
382
|
+
expect(injectedModel).toBeDefined();
|
|
383
|
+
|
|
384
|
+
const user = await User.create({ name: 'Alice' });
|
|
385
|
+
expect(user.id).toBeDefined();
|
|
386
|
+
expect(user.name).toBe('Alice');
|
|
387
|
+
|
|
388
|
+
const found = await User.findByPk(user.id);
|
|
389
|
+
expect(found).not.toBeNull();
|
|
390
|
+
expect(found.name).toBe('Alice');
|
|
391
|
+
|
|
392
|
+
await user.update({ name: 'Bob' });
|
|
393
|
+
const found2 = await User.findByPk(user.id);
|
|
394
|
+
expect(found2.name).toBe('Bob');
|
|
395
|
+
|
|
396
|
+
await user.destroy();
|
|
397
|
+
const found3 = await User.findByPk(user.id);
|
|
398
|
+
expect(found3).toBeNull();
|
|
399
|
+
|
|
400
|
+
await moduleRef.close();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('BullModule: check root registry and compatibility methods', async () => {
|
|
404
|
+
const moduleRef = await Test.createTestingModule({
|
|
405
|
+
imports: [
|
|
406
|
+
BullModule.forRoot({}),
|
|
407
|
+
BullModule.registerQueue({ name: 'audio' }),
|
|
408
|
+
],
|
|
409
|
+
}).compile();
|
|
410
|
+
|
|
411
|
+
const queue = moduleRef.get('Queue_audio') as any;
|
|
412
|
+
expect(queue).toBeDefined();
|
|
413
|
+
expect(queue.pause).toBeDefined();
|
|
414
|
+
expect(queue.resume).toBeDefined();
|
|
415
|
+
|
|
416
|
+
await queue.pause();
|
|
417
|
+
expect(await queue.isPaused()).toBe(true);
|
|
418
|
+
await queue.resume();
|
|
419
|
+
expect(await queue.isPaused()).toBe(false);
|
|
420
|
+
|
|
421
|
+
const job = await queue.add('test-job', { foo: 'bar' });
|
|
422
|
+
expect(job.id).toBeDefined();
|
|
423
|
+
expect(await queue.count()).toBe(1);
|
|
424
|
+
|
|
425
|
+
await queue.drain();
|
|
426
|
+
expect(await queue.count()).toBe(0);
|
|
427
|
+
|
|
428
|
+
await moduleRef.close();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test('CQRS Saga & AggregateRoot & ExceptionBus', async () => {
|
|
432
|
+
class MyCommand implements ICommand {
|
|
433
|
+
constructor(public readonly val: string) {}
|
|
434
|
+
}
|
|
435
|
+
class MyEvent implements IEvent {
|
|
436
|
+
constructor(public readonly val: string) {}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let commandVal = '';
|
|
440
|
+
|
|
441
|
+
@CommandHandler(MyCommand)
|
|
442
|
+
class MyCommandHandler implements ICommandHandler<MyCommand> {
|
|
443
|
+
async execute(command: MyCommand) {
|
|
444
|
+
commandVal = command.val;
|
|
445
|
+
if (command.val === 'throw') {
|
|
446
|
+
throw new Error('command-failed');
|
|
447
|
+
}
|
|
448
|
+
return 'ok';
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
class OrderAggregate extends AggregateRoot {
|
|
453
|
+
createOrder(val: string) {
|
|
454
|
+
this.apply(new MyEvent(val));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
@Injectable()
|
|
459
|
+
class OrderSagas {
|
|
460
|
+
@Saga()
|
|
461
|
+
orderCreated = (events$: Observable<any>): Observable<ICommand> => {
|
|
462
|
+
return events$.pipe(
|
|
463
|
+
filter((e: any) => e instanceof MyEvent),
|
|
464
|
+
map((e: any) => new MyCommand(e.val))
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const moduleRef = await Test.createTestingModule({
|
|
470
|
+
imports: [CqrsModule],
|
|
471
|
+
providers: [MyCommandHandler, OrderSagas],
|
|
472
|
+
}).compile();
|
|
473
|
+
|
|
474
|
+
const eventBus = moduleRef.get(EventBus);
|
|
475
|
+
const exceptionBus = moduleRef.get(UnhandledExceptionBus);
|
|
476
|
+
const commandBus = moduleRef.get(CommandBus);
|
|
477
|
+
|
|
478
|
+
const cqrs = moduleRef.get(CqrsModule);
|
|
479
|
+
(cqrs as any).onModuleInit();
|
|
480
|
+
|
|
481
|
+
const agg = new OrderAggregate();
|
|
482
|
+
agg.createOrder('hello-saga');
|
|
483
|
+
expect(agg.getUncommittedEvents().length).toBe(1);
|
|
484
|
+
|
|
485
|
+
const publisher = moduleRef.get(EventPublisher);
|
|
486
|
+
const boundAgg = publisher.mergeObjectContext(agg);
|
|
487
|
+
boundAgg.commit();
|
|
488
|
+
expect(agg.getUncommittedEvents().length).toBe(0);
|
|
489
|
+
|
|
490
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
491
|
+
expect(commandVal).toBe('hello-saga');
|
|
492
|
+
|
|
493
|
+
let caughtErr: any = null;
|
|
494
|
+
exceptionBus.subscribe((err) => {
|
|
495
|
+
caughtErr = err;
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
await commandBus.execute(new MyCommand('throw'));
|
|
500
|
+
} catch {
|
|
501
|
+
// expected
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
expect(caughtErr).not.toBeNull();
|
|
505
|
+
expect(caughtErr.error.message).toBe('command-failed');
|
|
506
|
+
|
|
507
|
+
await moduleRef.close();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test('Terminus health check indicators', async () => {
|
|
511
|
+
const moduleRef = await Test.createTestingModule({
|
|
512
|
+
imports: [TerminusModule],
|
|
513
|
+
}).compile();
|
|
514
|
+
|
|
515
|
+
const memIndicator = moduleRef.get(MemoryHealthIndicator);
|
|
516
|
+
const dbIndicator = moduleRef.get(TypeOrmHealthIndicator);
|
|
517
|
+
|
|
518
|
+
const memRes = await memIndicator.checkHeap('heap', 1000 * 1024 * 1024);
|
|
519
|
+
expect(memRes.heap.status).toBe('up');
|
|
520
|
+
|
|
521
|
+
const dbRes = await dbIndicator.pingCheck('db');
|
|
522
|
+
expect(dbRes.db.status).toBe('up');
|
|
523
|
+
|
|
524
|
+
await moduleRef.close();
|
|
525
|
+
});
|
|
272
526
|
});
|
|
527
|
+
|