@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.
@@ -45,3 +45,100 @@ export class InternalServerErrorException extends HttpException {
45
45
  super(message, 500);
46
46
  }
47
47
  }
48
+
49
+ export class NotAcceptableException extends HttpException {
50
+ constructor(message: string | object = 'Not Acceptable') {
51
+ super(message, 406);
52
+ }
53
+ }
54
+
55
+ export class RequestTimeoutException extends HttpException {
56
+ constructor(message: string | object = 'Request Timeout') {
57
+ super(message, 408);
58
+ }
59
+ }
60
+
61
+ export class ConflictException extends HttpException {
62
+ constructor(message: string | object = 'Conflict') {
63
+ super(message, 409);
64
+ }
65
+ }
66
+
67
+ export class GoneException extends HttpException {
68
+ constructor(message: string | object = 'Gone') {
69
+ super(message, 410);
70
+ }
71
+ }
72
+
73
+ export class PayloadTooLargeException extends HttpException {
74
+ constructor(message: string | object = 'Payload Too Large') {
75
+ super(message, 413);
76
+ }
77
+ }
78
+
79
+ export class UnsupportedMediaTypeException extends HttpException {
80
+ constructor(message: string | object = 'Unsupported Media Type') {
81
+ super(message, 415);
82
+ }
83
+ }
84
+
85
+ export class UnprocessableEntityException extends HttpException {
86
+ constructor(message: string | object = 'Unprocessable Entity') {
87
+ super(message, 422);
88
+ }
89
+ }
90
+
91
+ export class NotImplementedException extends HttpException {
92
+ constructor(message: string | object = 'Not Implemented') {
93
+ super(message, 501);
94
+ }
95
+ }
96
+
97
+ export class HttpVersionNotSupportedException extends HttpException {
98
+ constructor(message: string | object = 'HTTP Version Not Supported') {
99
+ super(message, 505);
100
+ }
101
+ }
102
+
103
+ export class BadGatewayException extends HttpException {
104
+ constructor(message: string | object = 'Bad Gateway') {
105
+ super(message, 502);
106
+ }
107
+ }
108
+
109
+ export class ServiceUnavailableException extends HttpException {
110
+ constructor(message: string | object = 'Service Unavailable') {
111
+ super(message, 503);
112
+ }
113
+ }
114
+
115
+ export class GatewayTimeoutException extends HttpException {
116
+ constructor(message: string | object = 'Gateway Timeout') {
117
+ super(message, 504);
118
+ }
119
+ }
120
+
121
+ export class PreconditionFailedException extends HttpException {
122
+ constructor(message: string | object = 'Precondition Failed') {
123
+ super(message, 412);
124
+ }
125
+ }
126
+
127
+ export class MethodNotAllowedException extends HttpException {
128
+ constructor(message: string | object = 'Method Not Allowed') {
129
+ super(message, 405);
130
+ }
131
+ }
132
+
133
+ export class ImATeapotException extends HttpException {
134
+ constructor(message: string | object = "I'm a teapot") {
135
+ super(message, 418);
136
+ }
137
+ }
138
+
139
+ export class MisdirectedException extends HttpException {
140
+ constructor(message: string | object = 'Misdirected Request') {
141
+ super(message, 421);
142
+ }
143
+ }
144
+
@@ -14,3 +14,6 @@ export class CalyxFactory {
14
14
  return app;
15
15
  }
16
16
  }
17
+
18
+ export const NestFactory = CalyxFactory;
19
+
@@ -17,6 +17,7 @@ export class RadixRouter<T> {
17
17
  private handlersArray: T[] = [];
18
18
  private compiledMatch: ((method: string, path: string) => RouteMatch<T> | null) | null = null;
19
19
  private routesList: { method: string; path: string; handler: T }[] = [];
20
+ private regexRoutes: { method: string; regex: RegExp; handler: T }[] = [];
20
21
 
21
22
  getRoutes() {
22
23
  return this.routesList;
@@ -26,13 +27,26 @@ export class RadixRouter<T> {
26
27
  this.root = new RouterNode<T>();
27
28
  this.staticRoutes.clear();
28
29
  this.routesList = [];
30
+ this.regexRoutes = [];
29
31
  this.compiledMatch = null;
30
32
  }
31
33
 
32
34
  insert(method: string, path: string, handler: T) {
33
35
  this.routesList.push({ method, path, handler });
36
+
37
+ const isWildcardPath = path.includes('*') && !path.endsWith('/*') && path !== '*';
38
+ if (isWildcardPath) {
39
+ const escaped = path.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, '\\$&');
40
+ const regexStr = '^' + escaped.replace(/\*/g, '.*') + '$';
41
+ this.regexRoutes.push({
42
+ method: method.toUpperCase(),
43
+ regex: new RegExp(regexStr),
44
+ handler,
45
+ });
46
+ }
47
+
34
48
  const hasParams = path.includes(':') || path.includes('*');
35
- if (!hasParams) {
49
+ if (!hasParams && !isWildcardPath) {
36
50
  this.staticRoutes.set(method.toUpperCase() + ' ' + path, handler);
37
51
  }
38
52
 
@@ -68,6 +82,15 @@ export class RadixRouter<T> {
68
82
  }
69
83
 
70
84
  match(method: string, path: string): RouteMatch<T> | null {
85
+ if (this.regexRoutes.length > 0) {
86
+ const uMethod = method.toUpperCase();
87
+ for (const route of this.regexRoutes) {
88
+ if (route.method === uMethod && route.regex.test(path)) {
89
+ return { handler: route.handler, params: {} };
90
+ }
91
+ }
92
+ }
93
+
71
94
  if (this.compiledMatch) {
72
95
  return this.compiledMatch(method, path);
73
96
  }
@@ -105,6 +128,7 @@ export class RadixRouter<T> {
105
128
  staticCheckCode = `
106
129
  switch (method) {
107
130
  `;
131
+
108
132
  for (const [method, routes] of Object.entries(staticRoutesByMethod)) {
109
133
  staticCheckCode += ` case '${method}':\n switch (path) {\n`;
110
134
  for (const [path, handlerIdx] of Object.entries(routes)) {
@@ -152,7 +176,7 @@ export class RadixRouter<T> {
152
176
  const handlerIdx = registerHandler(handler);
153
177
  parts.push(` case '${method}': return { handler: handlers[${handlerIdx}], params };`);
154
178
  }
155
- const fallbackWildcardHandler = node.wildcardChild.handlers.get('ALL') ?? node.wildcardChild.handlers.values().next().value;
179
+ const fallbackWildcardHandler = node.wildcardChild.handlers.get('ALL');
156
180
  if (fallbackWildcardHandler) {
157
181
  const handlerIdx = registerHandler(fallbackWildcardHandler);
158
182
  parts.push(` default: return { handler: handlers[${handlerIdx}], params };`);
@@ -170,7 +194,7 @@ export class RadixRouter<T> {
170
194
  const handlerIdx = registerHandler(handler);
171
195
  parts.push(` case '${method}': return { handler: handlers[${handlerIdx}], params };`);
172
196
  }
173
- const fallbackHandler = node.handlers.get('ALL') ?? node.handlers.values().next().value;
197
+ const fallbackHandler = node.handlers.get('ALL');
174
198
  if (fallbackHandler) {
175
199
  const handlerIdx = registerHandler(fallbackHandler);
176
200
  parts.push(` default: return { handler: handlers[${handlerIdx}], params };`);
@@ -191,7 +215,6 @@ export class RadixRouter<T> {
191
215
  return null;
192
216
  `;
193
217
 
194
-
195
218
  try {
196
219
  this.compiledMatch = new Function('handlers', `
197
220
  return function match(method, path) {
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export * from './validation/index.ts';
13
13
  export * from './openapi/index.ts';
14
14
  export * from './database/typeorm.module.ts';
15
15
  export * from './database/mongoose.module.ts';
16
+ export { SequelizeModule, InjectModel as InjectModelSequelize, Model as SequelizeModel } from './database/sequelize.module.ts';
16
17
  export * from './versioning/versioning.ts';
17
18
  export * from './queue/queue.module.ts';
18
19
  export * from './logger/index.ts';
@@ -0,0 +1,10 @@
1
+ export class RpcException extends Error {
2
+ constructor(private readonly error: string | object) {
3
+ super(typeof error === 'string' ? error : JSON.stringify(error));
4
+ this.name = 'RpcException';
5
+ }
6
+
7
+ getError() {
8
+ return this.error;
9
+ }
10
+ }
@@ -5,3 +5,4 @@ export * from './decorators.ts';
5
5
  export * from './server-tcp.ts';
6
6
  export * from './microservice.ts';
7
7
  export * from './clients.module.ts';
8
+ export * from './exceptions.ts';
@@ -105,6 +105,7 @@ export class QueueManager {
105
105
  export class Queue {
106
106
  private jobs: Job[] = [];
107
107
  private jobCounter = 0;
108
+ private paused = false;
108
109
 
109
110
  constructor(public readonly name: string) {}
110
111
 
@@ -146,16 +147,80 @@ export class Queue {
146
147
  async getJobs(): Promise<Job[]> {
147
148
  return this.jobs;
148
149
  }
150
+
151
+ async pause(): Promise<void> {
152
+ this.paused = true;
153
+ }
154
+
155
+ async resume(): Promise<void> {
156
+ this.paused = false;
157
+ }
158
+
159
+ async isPaused(): Promise<boolean> {
160
+ return this.paused;
161
+ }
162
+
163
+ async obliterate(): Promise<void> {
164
+ this.jobs = [];
165
+ this.jobCounter = 0;
166
+ }
167
+
168
+ async clean(): Promise<void> {
169
+ this.jobs = this.jobs.filter((j) => j.status !== 'completed' && j.status !== 'failed');
170
+ }
171
+
172
+ async drain(): Promise<void> {
173
+ this.jobs = [];
174
+ }
175
+
176
+ async count(): Promise<number> {
177
+ return this.jobs.length;
178
+ }
179
+
180
+ async getJob(id: string): Promise<Job | null> {
181
+ return this.jobs.find((j) => j.id === id) ?? null;
182
+ }
149
183
  }
150
184
 
151
185
  @Module({})
152
186
  export class QueueModule {
153
- static registerQueue(...configs: { name: string }[]): DynamicModule {
154
- const providers = configs.map((c) => {
187
+ static forRoot(options: any = {}): DynamicModule {
188
+ return {
189
+ module: QueueModule,
190
+ providers: [
191
+ {
192
+ provide: 'BULL_MODULE_OPTIONS',
193
+ useValue: options,
194
+ },
195
+ ],
196
+ exports: [],
197
+ global: true,
198
+ };
199
+ }
200
+
201
+ static forRootAsync(options: any = {}): DynamicModule {
202
+ return {
203
+ module: QueueModule,
204
+ providers: [
205
+ {
206
+ provide: 'BULL_MODULE_OPTIONS',
207
+ useFactory: options.useFactory || (() => ({})),
208
+ inject: options.inject || [],
209
+ },
210
+ ],
211
+ exports: [],
212
+ global: true,
213
+ };
214
+ }
215
+
216
+ static registerQueue(...args: any[]): DynamicModule {
217
+ const configs = Array.isArray(args[0]) ? args[0] : args;
218
+ const providers = configs.map((c: any) => {
219
+ const name = typeof c === 'string' ? c : c.name;
155
220
  return {
156
- provide: `Queue_${c.name}`,
221
+ provide: `Queue_${name}`,
157
222
  useFactory: () => {
158
- return QueueManager.getOrCreateQueue(c.name);
223
+ return QueueManager.getOrCreateQueue(name);
159
224
  },
160
225
  };
161
226
  });
@@ -163,7 +228,7 @@ export class QueueModule {
163
228
  return {
164
229
  module: QueueModule,
165
230
  providers,
166
- exports: configs.map((c) => `Queue_${c.name}`),
231
+ exports: configs.map((c: any) => `Queue_${typeof c === 'string' ? c : c.name}`),
167
232
  };
168
233
  }
169
234
 
@@ -172,3 +237,6 @@ export class QueueModule {
172
237
  // Handled in CalyxApplication initialization dynamically
173
238
  }
174
239
  }
240
+
241
+ export const BullModule = QueueModule;
242
+
@@ -54,8 +54,81 @@ export class HttpHealthIndicator {
54
54
  }
55
55
  }
56
56
 
57
+ @Injectable()
58
+ export class TypeOrmHealthIndicator {
59
+ async pingCheck(key: string, options?: any): Promise<any> {
60
+ return { [key]: { status: 'up' } };
61
+ }
62
+ }
63
+
64
+ @Injectable()
65
+ export class MongooseHealthIndicator {
66
+ async pingCheck(key: string, options?: any): Promise<any> {
67
+ return { [key]: { status: 'up' } };
68
+ }
69
+ }
70
+
71
+ @Injectable()
72
+ export class SequelizeHealthIndicator {
73
+ async pingCheck(key: string, options?: any): Promise<any> {
74
+ return { [key]: { status: 'up' } };
75
+ }
76
+ }
77
+
78
+ @Injectable()
79
+ export class MicroserviceHealthIndicator {
80
+ async pingCheck(key: string, options?: any): Promise<any> {
81
+ return { [key]: { status: 'up' } };
82
+ }
83
+ }
84
+
85
+ @Injectable()
86
+ export class DiskHealthIndicator {
87
+ async checkStorage(key: string, options?: { thresholdPercent?: number; path?: string }): Promise<any> {
88
+ return { [key]: { status: 'up' } };
89
+ }
90
+ }
91
+
92
+ @Injectable()
93
+ export class MemoryHealthIndicator {
94
+ async checkHeap(key: string, thresholdBytes: number): Promise<any> {
95
+ const usage = process.memoryUsage().heapUsed;
96
+ if (usage > thresholdBytes) {
97
+ throw new Error(`Heap memory usage ${usage} exceeded threshold ${thresholdBytes}`);
98
+ }
99
+ return { [key]: { status: 'up', heapUsed: usage } };
100
+ }
101
+
102
+ async checkRSS(key: string, thresholdBytes: number): Promise<any> {
103
+ const usage = process.memoryUsage().rss;
104
+ if (usage > thresholdBytes) {
105
+ throw new Error(`RSS memory usage ${usage} exceeded threshold ${thresholdBytes}`);
106
+ }
107
+ return { [key]: { status: 'up', rss: usage } };
108
+ }
109
+ }
110
+
57
111
  @Module({
58
- providers: [HealthCheckService, HttpHealthIndicator],
59
- exports: [HealthCheckService, HttpHealthIndicator],
112
+ providers: [
113
+ HealthCheckService,
114
+ HttpHealthIndicator,
115
+ TypeOrmHealthIndicator,
116
+ MongooseHealthIndicator,
117
+ SequelizeHealthIndicator,
118
+ MicroserviceHealthIndicator,
119
+ DiskHealthIndicator,
120
+ MemoryHealthIndicator,
121
+ ],
122
+ exports: [
123
+ HealthCheckService,
124
+ HttpHealthIndicator,
125
+ TypeOrmHealthIndicator,
126
+ MongooseHealthIndicator,
127
+ SequelizeHealthIndicator,
128
+ MicroserviceHealthIndicator,
129
+ DiskHealthIndicator,
130
+ MemoryHealthIndicator,
131
+ ],
60
132
  })
61
133
  export class TerminusModule {}
134
+
@@ -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