@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
package/src/http/exceptions.ts
CHANGED
|
@@ -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
|
+
|
package/src/http/factory.ts
CHANGED
package/src/http/router.ts
CHANGED
|
@@ -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')
|
|
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')
|
|
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';
|
|
@@ -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
|
|
154
|
-
|
|
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_${
|
|
221
|
+
provide: `Queue_${name}`,
|
|
157
222
|
useFactory: () => {
|
|
158
|
-
return QueueManager.getOrCreateQueue(
|
|
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
|
+
|
package/src/terminus/terminus.ts
CHANGED
|
@@ -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: [
|
|
59
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|