@martel/calyx 1.11.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +1 -1
  3. package/src/cache/cache.interceptor.ts +4 -2
  4. package/src/cache/decorators.ts +4 -0
  5. package/src/cache/index.ts +1 -0
  6. package/src/cli/index.ts +7 -1
  7. package/src/config/config.module.ts +16 -2
  8. package/src/config/config.service.ts +20 -6
  9. package/src/core/container.ts +559 -140
  10. package/src/core/index.ts +2 -0
  11. package/src/core/lazy-module-loader.ts +29 -0
  12. package/src/core/metadata.ts +6 -1
  13. package/src/core/testing-module.ts +123 -0
  14. package/src/cqrs/cqrs.ts +264 -0
  15. package/src/database/sequelize.module.ts +239 -0
  16. package/src/event-emitter/decorators.ts +2 -2
  17. package/src/event-emitter/event-emitter.ts +3 -0
  18. package/src/graphql/decorators.ts +16 -0
  19. package/src/graphql/graphql.module.ts +16 -0
  20. package/src/http/application.ts +261 -21
  21. package/src/http/decorators.ts +25 -1
  22. package/src/http/exceptions.ts +97 -0
  23. package/src/http/factory.ts +3 -0
  24. package/src/http/router.ts +27 -4
  25. package/src/index.ts +3 -0
  26. package/src/microservices/clients.module.ts +47 -0
  27. package/src/microservices/exceptions.ts +10 -0
  28. package/src/microservices/index.ts +2 -0
  29. package/src/microservices/microservice.ts +1 -1
  30. package/src/queue/queue.module.ts +73 -5
  31. package/src/schedule/decorators.ts +10 -6
  32. package/src/schedule/index.ts +1 -0
  33. package/src/schedule/schedule.module.ts +3 -2
  34. package/src/schedule/scheduler-registry.ts +50 -0
  35. package/src/security/index.ts +1 -0
  36. package/src/security/throttler.module.ts +108 -0
  37. package/src/terminus/terminus.ts +134 -0
  38. package/src/validation/compiler.ts +133 -10
  39. package/src/validation/decorators.ts +164 -2
  40. package/src/validation/http-pipes.ts +128 -0
  41. package/src/validation/index.ts +1 -0
  42. package/src/websockets/decorators.ts +12 -2
  43. package/src/websockets/exceptions.ts +10 -0
  44. package/src/websockets/index.ts +1 -0
  45. package/tests/circular-di.test.ts +151 -0
  46. package/tests/di.test.ts +10 -2
  47. package/tests/nestjs-parity.test.ts +527 -0
@@ -28,16 +28,139 @@ export class ValidationCompiler {
28
28
 
29
29
  const propCode: string[] = [];
30
30
  for (const rule of propRules) {
31
- if (rule.type === 'optional') continue;
32
-
33
- if (rule.type === 'string') {
34
- propCode.push(`if (typeof obj.${prop} !== 'string') errors.push('${prop} must be a string');`);
35
- } else if (rule.type === 'number') {
36
- propCode.push(`if (typeof obj.${prop} !== 'number' || isNaN(obj.${prop})) errors.push('${prop} must be a number');`);
37
- } else if (rule.type === 'email') {
38
- propCode.push(
39
- `if (typeof obj.${prop} !== 'string' || !obj.${prop}.includes('@')) errors.push('${prop} must be a valid email');`
40
- );
31
+ const args = rule.args || [];
32
+ const type = rule.type;
33
+
34
+ if (type === 'optional') continue;
35
+
36
+ switch (type) {
37
+ case 'isDefined':
38
+ propCode.push(`if (obj.${prop} === undefined || obj.${prop} === null) errors.push('${prop} must be defined');`);
39
+ break;
40
+ case 'equals':
41
+ propCode.push(`if (obj.${prop} !== ${JSON.stringify(args[0])}) errors.push('${prop} must be equal to ${args[0]}');`);
42
+ break;
43
+ case 'notEquals':
44
+ propCode.push(`if (obj.${prop} === ${JSON.stringify(args[0])}) errors.push('${prop} must not be equal to ${args[0]}');`);
45
+ break;
46
+ case 'isEmpty':
47
+ propCode.push(`if (obj.${prop} !== '' && obj.${prop} !== undefined && obj.${prop} !== null) errors.push('${prop} must be empty');`);
48
+ break;
49
+ case 'isNotEmpty':
50
+ propCode.push(`if (obj.${prop} === '' || obj.${prop} === undefined || obj.${prop} === null) errors.push('${prop} should not be empty');`);
51
+ break;
52
+ case 'isIn':
53
+ propCode.push(`if (!${JSON.stringify(args[0])}.includes(obj.${prop})) errors.push('${prop} must be one of the following values: ' + ${JSON.stringify(args[0])}.join(', '));`);
54
+ break;
55
+ case 'isNotIn':
56
+ propCode.push(`if (${JSON.stringify(args[0])}.includes(obj.${prop})) errors.push('${prop} must not be one of the following values: ' + ${JSON.stringify(args[0])}.join(', '));`);
57
+ break;
58
+ case 'isBoolean':
59
+ propCode.push(`if (typeof obj.${prop} !== 'boolean') errors.push('${prop} must be a boolean');`);
60
+ break;
61
+ case 'isDate':
62
+ propCode.push(`if (!(obj.${prop} instanceof Date) || isNaN(obj.${prop}.getTime())) errors.push('${prop} must be a Date instance');`);
63
+ break;
64
+ case 'string':
65
+ propCode.push(`if (typeof obj.${prop} !== 'string') errors.push('${prop} must be a string');`);
66
+ break;
67
+ case 'number':
68
+ propCode.push(`if (typeof obj.${prop} !== 'number' || isNaN(obj.${prop})) errors.push('${prop} must be a number');`);
69
+ break;
70
+ case 'isInt':
71
+ propCode.push(`if (typeof obj.${prop} !== 'number' || !Number.isInteger(obj.${prop})) errors.push('${prop} must be an integer');`);
72
+ break;
73
+ case 'isArray':
74
+ propCode.push(`if (!Array.isArray(obj.${prop})) errors.push('${prop} must be an array');`);
75
+ break;
76
+ case 'isEnum':
77
+ const enumValues = Object.values(args[0]);
78
+ propCode.push(`if (!${JSON.stringify(enumValues)}.includes(obj.${prop})) errors.push('${prop} must be a valid enum value');`);
79
+ break;
80
+ case 'isObject':
81
+ propCode.push(`if (typeof obj.${prop} !== 'object' || obj.${prop} === null || Array.isArray(obj.${prop})) errors.push('${prop} must be an object');`);
82
+ break;
83
+ case 'isPositive':
84
+ propCode.push(`if (typeof obj.${prop} !== 'number' || obj.${prop} <= 0) errors.push('${prop} must be a positive number');`);
85
+ break;
86
+ case 'isNegative':
87
+ propCode.push(`if (typeof obj.${prop} !== 'number' || obj.${prop} >= 0) errors.push('${prop} must be a negative number');`);
88
+ break;
89
+ case 'min':
90
+ propCode.push(`if (typeof obj.${prop} !== 'number' || obj.${prop} < ${args[0]}) errors.push('${prop} must not be less than ${args[0]}');`);
91
+ break;
92
+ case 'max':
93
+ propCode.push(`if (typeof obj.${prop} !== 'number' || obj.${prop} > ${args[0]}) errors.push('${prop} must not be greater than ${args[0]}');`);
94
+ break;
95
+ case 'contains':
96
+ propCode.push(`if (typeof obj.${prop} !== 'string' || !obj.${prop}.includes(${JSON.stringify(args[0])})) errors.push('${prop} must contain a ${args[0]} string');`);
97
+ break;
98
+ case 'notContains':
99
+ propCode.push(`if (typeof obj.${prop} !== 'string' || obj.${prop}.includes(${JSON.stringify(args[0])})) errors.push('${prop} must not contain a ${args[0]} string');`);
100
+ break;
101
+ case 'isAlpha':
102
+ propCode.push(`if (typeof obj.${prop} !== 'string' || !/^[a-zA-Z]+$/.test(obj.${prop})) errors.push('${prop} must contain only letters');`);
103
+ break;
104
+ case 'isAlphanumeric':
105
+ propCode.push(`if (typeof obj.${prop} !== 'string' || !/^[a-zA-Z0-9]+$/.test(obj.${prop})) errors.push('${prop} must contain only letters and numbers');`);
106
+ break;
107
+ case 'isDecimal':
108
+ propCode.push(`if (typeof obj.${prop} !== 'string' && typeof obj.${prop} !== 'number' || !/^\\d+\\.\\d+$/.test(String(obj.${prop}))) errors.push('${prop} must be a decimal number');`);
109
+ break;
110
+ case 'email':
111
+ propCode.push(`if (typeof obj.${prop} !== 'string' || !obj.${prop}.includes('@')) errors.push('${prop} must be a valid email');`);
112
+ break;
113
+ case 'isUrl':
114
+ propCode.push(`if (typeof obj.${prop} !== 'string' || !/^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$/.test(obj.${prop})) errors.push('${prop} must be a URL address');`);
115
+ break;
116
+ case 'isIP':
117
+ propCode.push(`if (typeof obj.${prop} !== 'string' || !/^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/.test(obj.${prop})) errors.push('${prop} must be an IP address');`);
118
+ break;
119
+ case 'isPort':
120
+ propCode.push(`if (typeof obj.${prop} !== 'number' && typeof obj.${prop} !== 'string' || Number(obj.${prop}) < 0 || Number(obj.${prop}) > 65535) errors.push('${prop} must be a port');`);
121
+ break;
122
+ case 'isJSON':
123
+ propCode.push(`if (typeof obj.${prop} !== 'string') errors.push('${prop} must be a json string'); else { try { JSON.parse(obj.${prop}); } catch { errors.push('${prop} must be a json string'); } }`);
124
+ break;
125
+ case 'isLowercase':
126
+ propCode.push(`if (typeof obj.${prop} !== 'string' || obj.${prop} !== obj.${prop}.toLowerCase()) errors.push('${prop} must be a lowercase string');`);
127
+ break;
128
+ case 'isUppercase':
129
+ propCode.push(`if (typeof obj.${prop} !== 'string' || obj.${prop} !== obj.${prop}.toUpperCase()) errors.push('${prop} must be a uppercase string');`);
130
+ break;
131
+ case 'isNumberString':
132
+ propCode.push(`if (typeof obj.${prop} !== 'string' || isNaN(Number(obj.${prop}))) errors.push('${prop} must be a number string');`);
133
+ break;
134
+ case 'isUUID':
135
+ propCode.push(`if (typeof obj.${prop} !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(obj.${prop})) errors.push('${prop} must be a UUID');`);
136
+ break;
137
+ case 'isDateString':
138
+ propCode.push(`if (typeof obj.${prop} !== 'string' || isNaN(Date.parse(obj.${prop}))) errors.push('${prop} must be a valid ISO 8601 date string');`);
139
+ break;
140
+ case 'length':
141
+ propCode.push(`if (typeof obj.${prop} !== 'string' || obj.${prop}.length < ${args[0]} || obj.${prop}.length > ${args[1]}) errors.push('${prop} length must be between ${args[0]} and ${args[1]}');`);
142
+ break;
143
+ case 'minLength':
144
+ propCode.push(`if (typeof obj.${prop} !== 'string' || obj.${prop}.length < ${args[0]}) errors.push('${prop} length must be at least ${args[0]}');`);
145
+ break;
146
+ case 'maxLength':
147
+ propCode.push(`if (typeof obj.${prop} !== 'string' || obj.${prop}.length > ${args[0]}) errors.push('${prop} length must not exceed ${args[0]}');`);
148
+ break;
149
+ case 'matches':
150
+ propCode.push(`if (typeof obj.${prop} !== 'string' || !${args[0].toString()}.test(obj.${prop})) errors.push('${prop} must match regular expression');`);
151
+ break;
152
+ case 'arrayNotEmpty':
153
+ propCode.push(`if (!Array.isArray(obj.${prop}) || obj.${prop}.length === 0) errors.push('${prop} should not be empty');`);
154
+ break;
155
+ case 'arrayMinSize':
156
+ propCode.push(`if (!Array.isArray(obj.${prop}) || obj.${prop}.length < ${args[0]}) errors.push('${prop} must contain at least ${args[0]} elements');`);
157
+ break;
158
+ case 'arrayMaxSize':
159
+ propCode.push(`if (!Array.isArray(obj.${prop}) || obj.${prop}.length > ${args[0]}) errors.push('${prop} must contain not more than ${args[0]} elements');`);
160
+ break;
161
+ case 'arrayUnique':
162
+ propCode.push(`if (!Array.isArray(obj.${prop}) || new Set(obj.${prop}).size !== obj.${prop}.length) errors.push('${prop} must contain unique elements');`);
163
+ break;
41
164
  }
42
165
  }
43
166
 
@@ -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 IsOptional(): PropertyDecorator {
24
- return (target, propertyKey) => registerValidationRule('optional', target, String(propertyKey));
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;
@@ -0,0 +1,128 @@
1
+ import { PipeTransform, ArgumentMetadata } from '../lifecycle/interfaces.ts';
2
+ import { Injectable } from '../core/decorators.ts';
3
+ import { HttpException } from '../http/exceptions.ts';
4
+
5
+ @Injectable()
6
+ export class ParseIntPipe implements PipeTransform<string | number, number> {
7
+ transform(value: string | number, metadata: ArgumentMetadata): number {
8
+ const val = typeof value === 'string' ? parseInt(value, 10) : value;
9
+ if (isNaN(val)) {
10
+ throw new HttpException(`Validation failed (numeric string is expected) for parameter "${metadata.data ?? ''}"`, 400);
11
+ }
12
+ return val;
13
+ }
14
+ }
15
+
16
+ @Injectable()
17
+ export class ParseFloatPipe implements PipeTransform<string | number, number> {
18
+ transform(value: string | number, metadata: ArgumentMetadata): number {
19
+ const val = typeof value === 'string' ? parseFloat(value) : value;
20
+ if (isNaN(val)) {
21
+ throw new HttpException(`Validation failed (float string is expected) for parameter "${metadata.data ?? ''}"`, 400);
22
+ }
23
+ return val;
24
+ }
25
+ }
26
+
27
+ @Injectable()
28
+ export class ParseBoolPipe implements PipeTransform<string | boolean, boolean> {
29
+ transform(value: string | boolean, metadata: ArgumentMetadata): boolean {
30
+ if (value === true || value === 'true') return true;
31
+ if (value === false || value === 'false') return false;
32
+ throw new HttpException(`Validation failed (boolean string is expected) for parameter "${metadata.data ?? ''}"`, 400);
33
+ }
34
+ }
35
+
36
+ export interface ParseArrayOptions {
37
+ items?: any;
38
+ separator?: string;
39
+ optional?: boolean;
40
+ }
41
+
42
+ @Injectable()
43
+ export class ParseArrayPipe implements PipeTransform {
44
+ constructor(private readonly options: ParseArrayOptions = {}) {}
45
+
46
+ transform(value: any, metadata: ArgumentMetadata): any[] {
47
+ if (value === undefined || value === null) {
48
+ if (this.options.optional) return [];
49
+ throw new HttpException(`Validation failed (array is expected) for parameter "${metadata.data ?? ''}"`, 400);
50
+ }
51
+ const separator = this.options.separator ?? ',';
52
+ let arr: any[] = [];
53
+ if (typeof value === 'string') {
54
+ arr = value.split(separator);
55
+ } else if (Array.isArray(value)) {
56
+ arr = value;
57
+ } else {
58
+ throw new HttpException(`Validation failed (array is expected) for parameter "${metadata.data ?? ''}"`, 400);
59
+ }
60
+
61
+ if (this.options.items) {
62
+ const type = this.options.items;
63
+ arr = arr.map((item) => {
64
+ if (type === Number) {
65
+ const val = Number(item);
66
+ if (isNaN(val)) throw new HttpException(`Validation failed (array of numbers is expected) for parameter "${metadata.data ?? ''}"`, 400);
67
+ return val;
68
+ }
69
+ if (type === Boolean) {
70
+ if (item === true || item === 'true') return true;
71
+ if (item === false || item === 'false') return false;
72
+ throw new HttpException(`Validation failed (array of booleans is expected) for parameter "${metadata.data ?? ''}"`, 400);
73
+ }
74
+ return item;
75
+ });
76
+ }
77
+ return arr;
78
+ }
79
+ }
80
+
81
+ @Injectable()
82
+ export class ParseUUIDPipe implements PipeTransform<string, string> {
83
+ private static readonly uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
84
+
85
+ transform(value: string, metadata: ArgumentMetadata): string {
86
+ if (typeof value !== 'string' || !ParseUUIDPipe.uuidRegex.test(value)) {
87
+ throw new HttpException(`Validation failed (uuid string is expected) for parameter "${metadata.data ?? ''}"`, 400);
88
+ }
89
+ return value;
90
+ }
91
+ }
92
+
93
+ @Injectable()
94
+ export class ParseEnumPipe implements PipeTransform {
95
+ constructor(private readonly enumType: any) {}
96
+
97
+ transform(value: any, metadata: ArgumentMetadata): any {
98
+ const values = Object.values(this.enumType);
99
+ if (!values.includes(value)) {
100
+ throw new HttpException(`Validation failed (enum value is expected) for parameter "${metadata.data ?? ''}"`, 400);
101
+ }
102
+ return value;
103
+ }
104
+ }
105
+
106
+ @Injectable()
107
+ export class ParseFilePipe implements PipeTransform {
108
+ constructor(private readonly validators: any[] = []) {}
109
+
110
+ transform(value: any, metadata: ArgumentMetadata): any {
111
+ if (!value || (typeof value === 'object' && !value.buffer)) {
112
+ throw new HttpException(`Validation failed (file is expected) for parameter "${metadata.data ?? ''}"`, 400);
113
+ }
114
+ return value;
115
+ }
116
+ }
117
+
118
+ @Injectable()
119
+ export class DefaultValuePipe implements PipeTransform {
120
+ constructor(private readonly defaultValue: any) {}
121
+
122
+ transform(value: any, metadata: ArgumentMetadata): any {
123
+ if (value === undefined || value === null || value === '') {
124
+ return this.defaultValue;
125
+ }
126
+ return value;
127
+ }
128
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './decorators.ts';
2
2
  export * from './compiler.ts';
3
3
  export * from './pipe.ts';
4
+ export * from './http-pipes.ts';
@@ -28,12 +28,22 @@ export function SubscribeMessage(event: string): MethodDecorator {
28
28
  };
29
29
  }
30
30
 
31
- export function MessageBody(): ParameterDecorator {
31
+ export function MessageBody(first?: any, ...pipes: any[]): ParameterDecorator {
32
32
  return (target, propertyKey, parameterIndex) => {
33
33
  if (!propertyKey) return;
34
34
  const constructor = target.constructor;
35
+
36
+ let name: string | undefined = undefined;
37
+ let parsedPipes: any[] = [];
38
+ if (typeof first === 'string') {
39
+ name = first;
40
+ parsedPipes = pipes;
41
+ } else if (first !== undefined) {
42
+ parsedPipes = [first, ...pipes];
43
+ }
44
+
35
45
  const existing = Reflect.getOwnMetadata('calyx:message_body', constructor) || [];
36
- existing.push({ propertyKey, parameterIndex });
46
+ existing.push({ propertyKey, parameterIndex, name, pipes: parsedPipes });
37
47
  Reflect.defineMetadata('calyx:message_body', existing, constructor);
38
48
  };
39
49
  }
@@ -0,0 +1,10 @@
1
+ export class WsException extends Error {
2
+ constructor(private readonly error: string | object) {
3
+ super(typeof error === 'string' ? error : JSON.stringify(error));
4
+ this.name = 'WsException';
5
+ }
6
+
7
+ getError() {
8
+ return this.error;
9
+ }
10
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './decorators.ts';
2
2
  export * from './gateway.ts';
3
+ export * from './exceptions.ts';
@@ -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
+ });