@objectql/core 1.7.3 → 1.8.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 +11 -0
- package/LICENSE +21 -118
- package/dist/repository.js +29 -16
- package/dist/repository.js.map +1 -1
- package/package.json +3 -3
- package/src/repository.ts +29 -14
- package/test/validator.test.ts +432 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { Validator } from '../src/validator';
|
|
2
|
+
import {
|
|
3
|
+
ValidationContext,
|
|
4
|
+
CrossFieldValidationRule,
|
|
5
|
+
StateMachineValidationRule,
|
|
6
|
+
UniquenessValidationRule,
|
|
7
|
+
BusinessRuleValidationRule,
|
|
8
|
+
CustomValidationRule,
|
|
9
|
+
FieldConfig,
|
|
10
|
+
} from '@objectql/types';
|
|
11
|
+
|
|
12
|
+
describe('Validator', () => {
|
|
13
|
+
let validator: Validator;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
validator = new Validator();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('Constructor', () => {
|
|
20
|
+
it('should create validator with default options', () => {
|
|
21
|
+
expect(validator).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should accept custom language option', () => {
|
|
25
|
+
const customValidator = new Validator({ language: 'zh-CN' });
|
|
26
|
+
expect(customValidator).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should accept language fallback option', () => {
|
|
30
|
+
const customValidator = new Validator({
|
|
31
|
+
language: 'fr',
|
|
32
|
+
languageFallback: ['en', 'zh-CN']
|
|
33
|
+
});
|
|
34
|
+
expect(customValidator).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Field Validation', () => {
|
|
39
|
+
describe('Required Fields', () => {
|
|
40
|
+
it('should validate required field with value', async () => {
|
|
41
|
+
const fieldConfig: FieldConfig = {
|
|
42
|
+
type: 'text',
|
|
43
|
+
required: true,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const context: ValidationContext = {
|
|
47
|
+
operation: 'create',
|
|
48
|
+
data: { name: 'John' },
|
|
49
|
+
objectName: 'user',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const results = await validator.validateField('name', fieldConfig, 'John', context);
|
|
53
|
+
expect(results).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should fail validation for missing required field', async () => {
|
|
57
|
+
const fieldConfig: FieldConfig = {
|
|
58
|
+
type: 'text',
|
|
59
|
+
required: true,
|
|
60
|
+
label: 'Name',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const context: ValidationContext = {
|
|
64
|
+
operation: 'create',
|
|
65
|
+
data: {},
|
|
66
|
+
objectName: 'user',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const results = await validator.validateField('name', fieldConfig, undefined, context);
|
|
70
|
+
expect(results.length).toBeGreaterThan(0);
|
|
71
|
+
expect(results[0].valid).toBe(false);
|
|
72
|
+
expect(results[0].message).toContain('Name is required');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should fail validation for empty string in required field', async () => {
|
|
76
|
+
const fieldConfig: FieldConfig = {
|
|
77
|
+
type: 'text',
|
|
78
|
+
required: true,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const context: ValidationContext = {
|
|
82
|
+
operation: 'create',
|
|
83
|
+
data: { name: '' },
|
|
84
|
+
objectName: 'user',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const results = await validator.validateField('name', fieldConfig, '', context);
|
|
88
|
+
expect(results.length).toBeGreaterThan(0);
|
|
89
|
+
expect(results[0].valid).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should fail validation for null in required field', async () => {
|
|
93
|
+
const fieldConfig: FieldConfig = {
|
|
94
|
+
type: 'text',
|
|
95
|
+
required: true,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const context: ValidationContext = {
|
|
99
|
+
operation: 'create',
|
|
100
|
+
data: { name: null },
|
|
101
|
+
objectName: 'user',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const results = await validator.validateField('name', fieldConfig, null, context);
|
|
105
|
+
expect(results.length).toBeGreaterThan(0);
|
|
106
|
+
expect(results[0].valid).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Email Validation', () => {
|
|
111
|
+
it('should validate correct email format', async () => {
|
|
112
|
+
const fieldConfig: FieldConfig = {
|
|
113
|
+
type: 'email',
|
|
114
|
+
validation: { format: 'email' },
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const context: ValidationContext = {
|
|
118
|
+
operation: 'create',
|
|
119
|
+
data: { email: 'test@example.com' },
|
|
120
|
+
objectName: 'user',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const results = await validator.validateField('email', fieldConfig, 'test@example.com', context);
|
|
124
|
+
const errors = results.filter(r => !r.valid);
|
|
125
|
+
expect(errors.length).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should reject invalid email format', async () => {
|
|
129
|
+
const fieldConfig: FieldConfig = {
|
|
130
|
+
type: 'email',
|
|
131
|
+
validation: { format: 'email' },
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const context: ValidationContext = {
|
|
135
|
+
operation: 'create',
|
|
136
|
+
data: { email: 'invalid-email' },
|
|
137
|
+
objectName: 'user',
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const results = await validator.validateField('email', fieldConfig, 'invalid-email', context);
|
|
141
|
+
const errors = results.filter(r => !r.valid);
|
|
142
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should reject email without @', async () => {
|
|
146
|
+
const fieldConfig: FieldConfig = {
|
|
147
|
+
type: 'email',
|
|
148
|
+
validation: { format: 'email' },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const context: ValidationContext = {
|
|
152
|
+
operation: 'create',
|
|
153
|
+
data: { email: 'testexample.com' },
|
|
154
|
+
objectName: 'user',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const results = await validator.validateField('email', fieldConfig, 'testexample.com', context);
|
|
158
|
+
const errors = results.filter(r => !r.valid);
|
|
159
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should reject email without domain', async () => {
|
|
163
|
+
const fieldConfig: FieldConfig = {
|
|
164
|
+
type: 'email',
|
|
165
|
+
validation: { format: 'email' },
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const context: ValidationContext = {
|
|
169
|
+
operation: 'create',
|
|
170
|
+
data: { email: 'test@' },
|
|
171
|
+
objectName: 'user',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const results = await validator.validateField('email', fieldConfig, 'test@', context);
|
|
175
|
+
const errors = results.filter(r => !r.valid);
|
|
176
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('Length Validation', () => {
|
|
181
|
+
it('should validate string within min and max length', async () => {
|
|
182
|
+
const fieldConfig: FieldConfig = {
|
|
183
|
+
type: 'text',
|
|
184
|
+
validation: {
|
|
185
|
+
min_length: 3,
|
|
186
|
+
max_length: 10,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const context: ValidationContext = {
|
|
191
|
+
operation: 'create',
|
|
192
|
+
data: { name: 'John' },
|
|
193
|
+
objectName: 'user',
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const results = await validator.validateField('name', fieldConfig, 'John', context);
|
|
197
|
+
const errors = results.filter(r => !r.valid);
|
|
198
|
+
expect(errors.length).toBe(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should reject string shorter than min_length', async () => {
|
|
202
|
+
const fieldConfig: FieldConfig = {
|
|
203
|
+
type: 'text',
|
|
204
|
+
validation: {
|
|
205
|
+
min_length: 5,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const context: ValidationContext = {
|
|
210
|
+
operation: 'create',
|
|
211
|
+
data: { name: 'Bob' },
|
|
212
|
+
objectName: 'user',
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const results = await validator.validateField('name', fieldConfig, 'Bob', context);
|
|
216
|
+
const errors = results.filter(r => !r.valid);
|
|
217
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should reject string longer than max_length', async () => {
|
|
221
|
+
const fieldConfig: FieldConfig = {
|
|
222
|
+
type: 'text',
|
|
223
|
+
validation: {
|
|
224
|
+
max_length: 5,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const context: ValidationContext = {
|
|
229
|
+
operation: 'create',
|
|
230
|
+
data: { name: 'Alexander' },
|
|
231
|
+
objectName: 'user',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const results = await validator.validateField('name', fieldConfig, 'Alexander', context);
|
|
235
|
+
const errors = results.filter(r => !r.valid);
|
|
236
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('Numeric Validation', () => {
|
|
241
|
+
it('should validate number within min and max range', async () => {
|
|
242
|
+
const fieldConfig: FieldConfig = {
|
|
243
|
+
type: 'number',
|
|
244
|
+
validation: {
|
|
245
|
+
min: 0,
|
|
246
|
+
max: 100,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const context: ValidationContext = {
|
|
251
|
+
operation: 'create',
|
|
252
|
+
data: { age: 25 },
|
|
253
|
+
objectName: 'user',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const results = await validator.validateField('age', fieldConfig, 25, context);
|
|
257
|
+
const errors = results.filter(r => !r.valid);
|
|
258
|
+
expect(errors.length).toBe(0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should reject number below min', async () => {
|
|
262
|
+
const fieldConfig: FieldConfig = {
|
|
263
|
+
type: 'number',
|
|
264
|
+
validation: {
|
|
265
|
+
min: 18,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const context: ValidationContext = {
|
|
270
|
+
operation: 'create',
|
|
271
|
+
data: { age: 15 },
|
|
272
|
+
objectName: 'user',
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const results = await validator.validateField('age', fieldConfig, 15, context);
|
|
276
|
+
const errors = results.filter(r => !r.valid);
|
|
277
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should reject number above max', async () => {
|
|
281
|
+
const fieldConfig: FieldConfig = {
|
|
282
|
+
type: 'number',
|
|
283
|
+
validation: {
|
|
284
|
+
max: 120,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const context: ValidationContext = {
|
|
289
|
+
operation: 'create',
|
|
290
|
+
data: { age: 150 },
|
|
291
|
+
objectName: 'user',
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const results = await validator.validateField('age', fieldConfig, 150, context);
|
|
295
|
+
const errors = results.filter(r => !r.valid);
|
|
296
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('Pattern Validation', () => {
|
|
301
|
+
it('should validate value matching pattern', async () => {
|
|
302
|
+
const fieldConfig: FieldConfig = {
|
|
303
|
+
type: 'text',
|
|
304
|
+
validation: {
|
|
305
|
+
pattern: '^[A-Z][0-9]{3}$', // e.g., A123
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const context: ValidationContext = {
|
|
310
|
+
operation: 'create',
|
|
311
|
+
data: { code: 'A123' },
|
|
312
|
+
objectName: 'product',
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const results = await validator.validateField('code', fieldConfig, 'A123', context);
|
|
316
|
+
const errors = results.filter(r => !r.valid);
|
|
317
|
+
expect(errors.length).toBe(0);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should reject value not matching pattern', async () => {
|
|
321
|
+
const fieldConfig: FieldConfig = {
|
|
322
|
+
type: 'text',
|
|
323
|
+
validation: {
|
|
324
|
+
pattern: '^[A-Z][0-9]{3}$',
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const context: ValidationContext = {
|
|
329
|
+
operation: 'create',
|
|
330
|
+
data: { code: 'a123' },
|
|
331
|
+
objectName: 'product',
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const results = await validator.validateField('code', fieldConfig, 'a123', context);
|
|
335
|
+
const errors = results.filter(r => !r.valid);
|
|
336
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('URL Validation', () => {
|
|
341
|
+
it('should validate correct URL format', async () => {
|
|
342
|
+
const fieldConfig: FieldConfig = {
|
|
343
|
+
type: 'url',
|
|
344
|
+
validation: { format: 'url' },
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const context: ValidationContext = {
|
|
348
|
+
operation: 'create',
|
|
349
|
+
data: { website: 'https://example.com' },
|
|
350
|
+
objectName: 'user',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const results = await validator.validateField('website', fieldConfig, 'https://example.com', context);
|
|
354
|
+
const errors = results.filter(r => !r.valid);
|
|
355
|
+
expect(errors.length).toBe(0);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should reject invalid URL format', async () => {
|
|
359
|
+
const fieldConfig: FieldConfig = {
|
|
360
|
+
type: 'url',
|
|
361
|
+
validation: { format: 'url' },
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const context: ValidationContext = {
|
|
365
|
+
operation: 'create',
|
|
366
|
+
data: { website: 'not-a-url' },
|
|
367
|
+
objectName: 'user',
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const results = await validator.validateField('website', fieldConfig, 'not-a-url', context);
|
|
371
|
+
const errors = results.filter(r => !r.valid);
|
|
372
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('Empty Value Handling', () => {
|
|
378
|
+
it('should skip validation for empty optional fields', async () => {
|
|
379
|
+
const fieldConfig: FieldConfig = {
|
|
380
|
+
type: 'text',
|
|
381
|
+
validation: {
|
|
382
|
+
min_length: 5,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const context: ValidationContext = {
|
|
387
|
+
operation: 'create',
|
|
388
|
+
data: {},
|
|
389
|
+
objectName: 'user',
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const results = await validator.validateField('nickname', fieldConfig, undefined, context);
|
|
393
|
+
expect(results.length).toBe(0);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should skip validation for null optional fields', async () => {
|
|
397
|
+
const fieldConfig: FieldConfig = {
|
|
398
|
+
type: 'text',
|
|
399
|
+
validation: {
|
|
400
|
+
min_length: 5,
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const context: ValidationContext = {
|
|
405
|
+
operation: 'create',
|
|
406
|
+
data: { nickname: null },
|
|
407
|
+
objectName: 'user',
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const results = await validator.validateField('nickname', fieldConfig, null, context);
|
|
411
|
+
expect(results.length).toBe(0);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should skip validation for empty string optional fields', async () => {
|
|
415
|
+
const fieldConfig: FieldConfig = {
|
|
416
|
+
type: 'text',
|
|
417
|
+
validation: {
|
|
418
|
+
min_length: 5,
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const context: ValidationContext = {
|
|
423
|
+
operation: 'create',
|
|
424
|
+
data: { nickname: '' },
|
|
425
|
+
objectName: 'user',
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const results = await validator.validateField('nickname', fieldConfig, '', context);
|
|
429
|
+
expect(results.length).toBe(0);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|