@objectql/core 4.0.2 → 4.0.3
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +18 -0
- package/README.md +4 -4
- package/dist/app.d.ts +9 -6
- package/dist/app.js +151 -29
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.js +2 -5
- package/dist/index.js.map +1 -1
- package/dist/optimizations/CompiledHookManager.d.ts +55 -0
- package/dist/optimizations/CompiledHookManager.js +164 -0
- package/dist/optimizations/CompiledHookManager.js.map +1 -0
- package/dist/optimizations/DependencyGraph.d.ts +82 -0
- package/dist/optimizations/DependencyGraph.js +211 -0
- package/dist/optimizations/DependencyGraph.js.map +1 -0
- package/dist/optimizations/GlobalConnectionPool.d.ts +89 -0
- package/dist/optimizations/GlobalConnectionPool.js +193 -0
- package/dist/optimizations/GlobalConnectionPool.js.map +1 -0
- package/dist/optimizations/LazyMetadataLoader.d.ts +75 -0
- package/dist/optimizations/LazyMetadataLoader.js +149 -0
- package/dist/optimizations/LazyMetadataLoader.js.map +1 -0
- package/dist/optimizations/OptimizedMetadataRegistry.d.ts +26 -0
- package/dist/optimizations/OptimizedMetadataRegistry.js +117 -0
- package/dist/optimizations/OptimizedMetadataRegistry.js.map +1 -0
- package/dist/optimizations/OptimizedValidationEngine.d.ts +73 -0
- package/dist/optimizations/OptimizedValidationEngine.js +141 -0
- package/dist/optimizations/OptimizedValidationEngine.js.map +1 -0
- package/dist/optimizations/QueryCompiler.d.ts +51 -0
- package/dist/optimizations/QueryCompiler.js +216 -0
- package/dist/optimizations/QueryCompiler.js.map +1 -0
- package/dist/optimizations/SQLQueryOptimizer.d.ts +96 -0
- package/dist/optimizations/SQLQueryOptimizer.js +265 -0
- package/dist/optimizations/SQLQueryOptimizer.js.map +1 -0
- package/dist/optimizations/index.d.ts +32 -0
- package/dist/optimizations/index.js +44 -0
- package/dist/optimizations/index.js.map +1 -0
- package/dist/plugin.d.ts +6 -7
- package/dist/plugin.js +39 -22
- package/dist/plugin.js.map +1 -1
- package/dist/query/query-analyzer.js.map +1 -1
- package/dist/query/query-builder.d.ts +6 -1
- package/dist/query/query-builder.js +21 -5
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-service.js.map +1 -1
- package/dist/repository.d.ts +2 -0
- package/dist/repository.js +15 -9
- package/dist/repository.js.map +1 -1
- package/jest.config.js +3 -3
- package/package.json +8 -5
- package/src/app.ts +173 -47
- package/src/index.ts +8 -9
- package/src/optimizations/CompiledHookManager.ts +185 -0
- package/src/optimizations/DependencyGraph.ts +255 -0
- package/src/optimizations/GlobalConnectionPool.ts +251 -0
- package/src/optimizations/LazyMetadataLoader.ts +180 -0
- package/src/optimizations/OptimizedMetadataRegistry.ts +132 -0
- package/src/optimizations/OptimizedValidationEngine.ts +172 -0
- package/src/optimizations/QueryCompiler.ts +242 -0
- package/src/optimizations/SQLQueryOptimizer.ts +329 -0
- package/src/optimizations/index.ts +34 -0
- package/src/plugin.ts +51 -28
- package/src/query/query-analyzer.ts +1 -1
- package/src/query/query-builder.ts +21 -7
- package/src/query/query-service.ts +1 -1
- package/src/repository.ts +25 -13
- package/test/__mocks__/@objectstack/runtime.ts +8 -8
- package/test/app.test.ts +9 -7
- package/test/optimizations.test.ts +440 -0
- package/test/plugin-integration.test.ts +30 -19
- package/tsconfig.json +4 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/ai-agent.d.ts +0 -176
- package/dist/ai-agent.js +0 -722
- package/dist/ai-agent.js.map +0 -1
- package/dist/formula-engine.d.ts +0 -102
- package/dist/formula-engine.js +0 -433
- package/dist/formula-engine.js.map +0 -1
- package/dist/formula-plugin.d.ts +0 -52
- package/dist/formula-plugin.js +0 -107
- package/dist/formula-plugin.js.map +0 -1
- package/dist/validator-plugin.d.ts +0 -56
- package/dist/validator-plugin.js +0 -106
- package/dist/validator-plugin.js.map +0 -1
- package/dist/validator.d.ts +0 -80
- package/dist/validator.js +0 -625
- package/dist/validator.js.map +0 -1
- package/src/ai-agent.ts +0 -868
- package/src/formula-engine.ts +0 -572
- package/src/formula-plugin.ts +0 -141
- package/src/validator-plugin.ts +0 -140
- package/src/validator.ts +0 -743
- package/test/formula-engine.test.ts +0 -725
- package/test/formula-integration.test.ts +0 -286
- package/test/formula-plugin.test.ts +0 -197
- package/test/formula-spec-compliance.test.ts +0 -258
- package/test/validation-spec-compliance.test.ts +0 -440
- package/test/validator-plugin.test.ts +0 -126
- package/test/validator.test.ts +0 -440
package/src/validator.ts
DELETED
|
@@ -1,743 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectQL
|
|
3
|
-
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Validation engine for ObjectQL.
|
|
11
|
-
* Executes validation rules based on metadata configuration.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
AnyValidationRule,
|
|
16
|
-
ValidationContext,
|
|
17
|
-
ValidationResult,
|
|
18
|
-
ValidationRuleResult,
|
|
19
|
-
CrossFieldValidationRule,
|
|
20
|
-
StateMachineValidationRule,
|
|
21
|
-
UniquenessValidationRule,
|
|
22
|
-
BusinessRuleValidationRule,
|
|
23
|
-
CustomValidationRule,
|
|
24
|
-
FieldConfig,
|
|
25
|
-
ObjectDoc,
|
|
26
|
-
ValidationCondition,
|
|
27
|
-
ValidationOperator,
|
|
28
|
-
} from '@objectql/types';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Configuration options for the Validator.
|
|
32
|
-
*/
|
|
33
|
-
export interface ValidatorOptions {
|
|
34
|
-
/** Preferred language for validation messages (default: 'en') */
|
|
35
|
-
language?: string;
|
|
36
|
-
/** Fallback languages if preferred language is not available */
|
|
37
|
-
languageFallback?: string[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Validator class that executes validation rules.
|
|
42
|
-
*/
|
|
43
|
-
export class Validator {
|
|
44
|
-
private options: ValidatorOptions;
|
|
45
|
-
|
|
46
|
-
constructor(options: ValidatorOptions = {}) {
|
|
47
|
-
this.options = {
|
|
48
|
-
language: options.language || 'en',
|
|
49
|
-
languageFallback: options.languageFallback || ['en', 'zh-CN'],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Validate a record against a set of rules.
|
|
55
|
-
*/
|
|
56
|
-
async validate(
|
|
57
|
-
rules: AnyValidationRule[],
|
|
58
|
-
context: ValidationContext
|
|
59
|
-
): Promise<ValidationResult> {
|
|
60
|
-
const results: ValidationRuleResult[] = [];
|
|
61
|
-
|
|
62
|
-
for (const rule of rules) {
|
|
63
|
-
// Check if rule should be applied
|
|
64
|
-
if (!this.shouldApplyRule(rule, context)) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Execute validation based on rule type
|
|
69
|
-
let result: ValidationRuleResult;
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
switch (rule.type) {
|
|
73
|
-
case 'cross_field':
|
|
74
|
-
result = await this.validateCrossField(rule as CrossFieldValidationRule, context);
|
|
75
|
-
break;
|
|
76
|
-
case 'state_machine':
|
|
77
|
-
result = await this.validateStateMachine(rule as StateMachineValidationRule, context);
|
|
78
|
-
break;
|
|
79
|
-
case 'unique':
|
|
80
|
-
result = await this.validateUniqueness(rule as UniquenessValidationRule, context);
|
|
81
|
-
break;
|
|
82
|
-
case 'business_rule':
|
|
83
|
-
result = await this.validateBusinessRule(rule as BusinessRuleValidationRule, context);
|
|
84
|
-
break;
|
|
85
|
-
case 'custom':
|
|
86
|
-
result = await this.validateCustom(rule as CustomValidationRule, context);
|
|
87
|
-
break;
|
|
88
|
-
default:
|
|
89
|
-
// Generic validation
|
|
90
|
-
result = {
|
|
91
|
-
rule: rule.name,
|
|
92
|
-
valid: true,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
} catch (error) {
|
|
96
|
-
result = {
|
|
97
|
-
rule: rule.name,
|
|
98
|
-
valid: false,
|
|
99
|
-
message: error instanceof Error ? error.message : 'Validation error',
|
|
100
|
-
severity: rule.severity || 'error',
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
results.push(result);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Categorize results
|
|
108
|
-
const errors = results.filter(r => !r.valid && r.severity === 'error');
|
|
109
|
-
const warnings = results.filter(r => !r.valid && r.severity === 'warning');
|
|
110
|
-
const info = results.filter(r => !r.valid && r.severity === 'info');
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
valid: errors.length === 0,
|
|
114
|
-
results,
|
|
115
|
-
errors,
|
|
116
|
-
warnings,
|
|
117
|
-
info,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Validate field-level rules.
|
|
123
|
-
*/
|
|
124
|
-
async validateField(
|
|
125
|
-
fieldName: string,
|
|
126
|
-
fieldConfig: FieldConfig,
|
|
127
|
-
value: any,
|
|
128
|
-
context: ValidationContext
|
|
129
|
-
): Promise<ValidationRuleResult[]> {
|
|
130
|
-
const results: ValidationRuleResult[] = [];
|
|
131
|
-
|
|
132
|
-
// Required field validation
|
|
133
|
-
if (fieldConfig.required && (value === null || value === undefined || value === '')) {
|
|
134
|
-
results.push({
|
|
135
|
-
rule: `${fieldName}_required`,
|
|
136
|
-
valid: false,
|
|
137
|
-
message: fieldConfig.validation?.message || `${fieldConfig.label || fieldName} is required`,
|
|
138
|
-
severity: 'error',
|
|
139
|
-
fields: [fieldName],
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Skip further validation if value is empty and not required
|
|
144
|
-
if (value === null || value === undefined || value === '') {
|
|
145
|
-
return results;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Type-specific validation
|
|
149
|
-
if (fieldConfig.validation) {
|
|
150
|
-
const validation = fieldConfig.validation;
|
|
151
|
-
|
|
152
|
-
// Email format
|
|
153
|
-
if (validation.format === 'email') {
|
|
154
|
-
// NOTE: This is a basic email validation regex. For production use,
|
|
155
|
-
// consider using a more comprehensive email validation library or regex
|
|
156
|
-
// that handles international domains, quoted strings, etc.
|
|
157
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
158
|
-
if (!emailRegex.test(value)) {
|
|
159
|
-
results.push({
|
|
160
|
-
rule: `${fieldName}_email_format`,
|
|
161
|
-
valid: false,
|
|
162
|
-
message: validation.message || 'Invalid email format',
|
|
163
|
-
severity: 'error',
|
|
164
|
-
fields: [fieldName],
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// URL format
|
|
170
|
-
if (validation.format === 'url') {
|
|
171
|
-
try {
|
|
172
|
-
const url = new URL(value);
|
|
173
|
-
if (validation.protocols && !validation.protocols.includes(url.protocol.replace(':', ''))) {
|
|
174
|
-
results.push({
|
|
175
|
-
rule: `${fieldName}_url_protocol`,
|
|
176
|
-
valid: false,
|
|
177
|
-
message: validation.message || `URL must use one of: ${validation.protocols.join(', ')}`,
|
|
178
|
-
severity: 'error',
|
|
179
|
-
fields: [fieldName],
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
} catch {
|
|
183
|
-
results.push({
|
|
184
|
-
rule: `${fieldName}_url_format`,
|
|
185
|
-
valid: false,
|
|
186
|
-
message: validation.message || 'Invalid URL format',
|
|
187
|
-
severity: 'error',
|
|
188
|
-
fields: [fieldName],
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Pattern validation
|
|
194
|
-
if (validation.pattern) {
|
|
195
|
-
try {
|
|
196
|
-
const pattern = new RegExp(validation.pattern);
|
|
197
|
-
if (!pattern.test(String(value))) {
|
|
198
|
-
results.push({
|
|
199
|
-
rule: `${fieldName}_pattern`,
|
|
200
|
-
valid: false,
|
|
201
|
-
message: validation.message || 'Value does not match required pattern',
|
|
202
|
-
severity: 'error',
|
|
203
|
-
fields: [fieldName],
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
} catch (error) {
|
|
207
|
-
results.push({
|
|
208
|
-
rule: `${fieldName}_pattern`,
|
|
209
|
-
valid: false,
|
|
210
|
-
message: `Invalid regex pattern: ${validation.pattern}`,
|
|
211
|
-
severity: 'error',
|
|
212
|
-
fields: [fieldName],
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Min/Max validation
|
|
218
|
-
if (validation.min !== undefined && value < validation.min) {
|
|
219
|
-
results.push({
|
|
220
|
-
rule: `${fieldName}_min`,
|
|
221
|
-
valid: false,
|
|
222
|
-
message: validation.message || `Value must be at least ${validation.min}`,
|
|
223
|
-
severity: 'error',
|
|
224
|
-
fields: [fieldName],
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (validation.max !== undefined && value > validation.max) {
|
|
229
|
-
results.push({
|
|
230
|
-
rule: `${fieldName}_max`,
|
|
231
|
-
valid: false,
|
|
232
|
-
message: validation.message || `Value must be at most ${validation.max}`,
|
|
233
|
-
severity: 'error',
|
|
234
|
-
fields: [fieldName],
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Length validation
|
|
239
|
-
const strValue = String(value);
|
|
240
|
-
if (validation.min_length !== undefined && strValue.length < validation.min_length) {
|
|
241
|
-
results.push({
|
|
242
|
-
rule: `${fieldName}_min_length`,
|
|
243
|
-
valid: false,
|
|
244
|
-
message: validation.message || `Must be at least ${validation.min_length} characters`,
|
|
245
|
-
severity: 'error',
|
|
246
|
-
fields: [fieldName],
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (validation.max_length !== undefined && strValue.length > validation.max_length) {
|
|
251
|
-
results.push({
|
|
252
|
-
rule: `${fieldName}_max_length`,
|
|
253
|
-
valid: false,
|
|
254
|
-
message: validation.message || `Must be at most ${validation.max_length} characters`,
|
|
255
|
-
severity: 'error',
|
|
256
|
-
fields: [fieldName],
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Legacy min/max from fieldConfig
|
|
262
|
-
if (fieldConfig.min !== undefined && value < fieldConfig.min) {
|
|
263
|
-
results.push({
|
|
264
|
-
rule: `${fieldName}_min`,
|
|
265
|
-
valid: false,
|
|
266
|
-
message: `Value must be at least ${fieldConfig.min}`,
|
|
267
|
-
severity: 'error',
|
|
268
|
-
fields: [fieldName],
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (fieldConfig.max !== undefined && value > fieldConfig.max) {
|
|
273
|
-
results.push({
|
|
274
|
-
rule: `${fieldName}_max`,
|
|
275
|
-
valid: false,
|
|
276
|
-
message: `Value must be at most ${fieldConfig.max}`,
|
|
277
|
-
severity: 'error',
|
|
278
|
-
fields: [fieldName],
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return results;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Check if a rule should be applied based on triggers and conditions.
|
|
287
|
-
*/
|
|
288
|
-
private shouldApplyRule(rule: AnyValidationRule, context: ValidationContext): boolean {
|
|
289
|
-
// Check trigger
|
|
290
|
-
if (rule.trigger && !rule.trigger.includes(context.operation)) {
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Check fields (for updates)
|
|
295
|
-
if (rule.fields && rule.fields.length > 0 && context.changedFields) {
|
|
296
|
-
const hasChangedField = rule.fields.some(f => context.changedFields!.includes(f));
|
|
297
|
-
if (!hasChangedField) {
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Check apply_when condition
|
|
303
|
-
if (rule.apply_when) {
|
|
304
|
-
return this.evaluateCondition(rule.apply_when, context.record);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Validate cross-field rule.
|
|
312
|
-
*/
|
|
313
|
-
private async validateCrossField(
|
|
314
|
-
rule: CrossFieldValidationRule,
|
|
315
|
-
context: ValidationContext
|
|
316
|
-
): Promise<ValidationRuleResult> {
|
|
317
|
-
if (!rule.rule) {
|
|
318
|
-
return { rule: rule.name, valid: true };
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const valid = this.evaluateCondition(rule.rule, context.record);
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
rule: rule.name,
|
|
325
|
-
valid,
|
|
326
|
-
message: valid ? undefined : this.formatMessage(rule.message, context.record),
|
|
327
|
-
error_code: rule.error_code,
|
|
328
|
-
severity: rule.severity || 'error',
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Validate state machine transitions.
|
|
334
|
-
*/
|
|
335
|
-
private async validateStateMachine(
|
|
336
|
-
rule: StateMachineValidationRule,
|
|
337
|
-
context: ValidationContext
|
|
338
|
-
): Promise<ValidationRuleResult> {
|
|
339
|
-
// Only validate on update
|
|
340
|
-
if (context.operation !== 'update' || !context.previousRecord) {
|
|
341
|
-
return { rule: rule.name, valid: true };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const oldState = context.previousRecord[rule.field];
|
|
345
|
-
const newState = context.record[rule.field];
|
|
346
|
-
|
|
347
|
-
// If state hasn't changed, validation passes
|
|
348
|
-
if (oldState === newState) {
|
|
349
|
-
return { rule: rule.name, valid: true };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Check if transition is allowed
|
|
353
|
-
const transitions = rule.transitions?.[oldState];
|
|
354
|
-
if (!transitions) {
|
|
355
|
-
return {
|
|
356
|
-
rule: rule.name,
|
|
357
|
-
valid: false,
|
|
358
|
-
message: this.formatMessage(rule.message, { old_status: oldState, new_status: newState }),
|
|
359
|
-
error_code: rule.error_code,
|
|
360
|
-
severity: rule.severity || 'error',
|
|
361
|
-
fields: [rule.field],
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Handle both array and object format
|
|
366
|
-
let allowedNext: string[] = [];
|
|
367
|
-
if (Array.isArray(transitions)) {
|
|
368
|
-
allowedNext = transitions;
|
|
369
|
-
} else if (typeof transitions === 'object' && 'allowed_next' in transitions) {
|
|
370
|
-
allowedNext = transitions.allowed_next || [];
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const isAllowed = allowedNext.includes(newState);
|
|
374
|
-
|
|
375
|
-
return {
|
|
376
|
-
rule: rule.name,
|
|
377
|
-
valid: isAllowed,
|
|
378
|
-
message: isAllowed ? undefined : this.formatMessage(rule.message, { old_status: oldState, new_status: newState }),
|
|
379
|
-
error_code: rule.error_code,
|
|
380
|
-
severity: rule.severity || 'error',
|
|
381
|
-
fields: [rule.field],
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Validate uniqueness by checking database for existing values.
|
|
387
|
-
*/
|
|
388
|
-
private async validateUniqueness(
|
|
389
|
-
rule: UniquenessValidationRule,
|
|
390
|
-
context: ValidationContext
|
|
391
|
-
): Promise<ValidationRuleResult> {
|
|
392
|
-
// Check if API is available for database access
|
|
393
|
-
if (!context.api) {
|
|
394
|
-
// If no API provided, we can't validate - pass by default
|
|
395
|
-
return {
|
|
396
|
-
rule: rule.name,
|
|
397
|
-
valid: true,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Get object name from context metadata
|
|
402
|
-
if (!context.metadata?.objectName) {
|
|
403
|
-
return {
|
|
404
|
-
rule: rule.name,
|
|
405
|
-
valid: false,
|
|
406
|
-
message: 'Object name not provided in validation context',
|
|
407
|
-
severity: rule.severity || 'error',
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const objectName = context.metadata.objectName;
|
|
412
|
-
|
|
413
|
-
// Determine fields to check for uniqueness
|
|
414
|
-
const fieldsToCheck: string[] = rule.fields || (rule.field ? [rule.field] : []);
|
|
415
|
-
|
|
416
|
-
if (fieldsToCheck.length === 0) {
|
|
417
|
-
return {
|
|
418
|
-
rule: rule.name,
|
|
419
|
-
valid: false,
|
|
420
|
-
message: 'No fields specified for uniqueness validation',
|
|
421
|
-
severity: rule.severity || 'error',
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Build query filter
|
|
426
|
-
const filters: Record<string, any> = {};
|
|
427
|
-
|
|
428
|
-
// Add field conditions
|
|
429
|
-
for (const field of fieldsToCheck) {
|
|
430
|
-
const fieldValue = context.record[field];
|
|
431
|
-
|
|
432
|
-
// Skip validation if field value is null/undefined (no value to check uniqueness for)
|
|
433
|
-
if (fieldValue === null || fieldValue === undefined) {
|
|
434
|
-
return {
|
|
435
|
-
rule: rule.name,
|
|
436
|
-
valid: true,
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Handle case sensitivity for string values
|
|
441
|
-
if (typeof fieldValue === 'string' && rule.case_sensitive === false) {
|
|
442
|
-
// NOTE: Case-insensitive comparison requires driver-specific implementation
|
|
443
|
-
// Some drivers support regex (MongoDB), others use LOWER() function (SQL)
|
|
444
|
-
// For now, we use exact match - driver adapters should implement case-insensitive logic
|
|
445
|
-
filters[field] = fieldValue;
|
|
446
|
-
} else {
|
|
447
|
-
filters[field] = fieldValue;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Apply scope conditions if specified
|
|
452
|
-
if (rule.scope) {
|
|
453
|
-
// Evaluate scope condition and add to filters
|
|
454
|
-
const scopeFields = this.extractFieldsFromCondition(rule.scope);
|
|
455
|
-
for (const field of scopeFields) {
|
|
456
|
-
if (context.record[field] !== undefined) {
|
|
457
|
-
filters[field] = context.record[field];
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Exclude current record for update operations
|
|
463
|
-
if (context.operation === 'update' && context.previousRecord?._id) {
|
|
464
|
-
filters._id = { $ne: context.previousRecord._id };
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
try {
|
|
468
|
-
// Query database to count existing records with same field values
|
|
469
|
-
const count = await context.api.count(objectName, filters);
|
|
470
|
-
|
|
471
|
-
const valid = count === 0;
|
|
472
|
-
|
|
473
|
-
return {
|
|
474
|
-
rule: rule.name,
|
|
475
|
-
valid,
|
|
476
|
-
message: valid ? undefined : this.formatMessage(rule.message, context.record),
|
|
477
|
-
error_code: rule.error_code,
|
|
478
|
-
severity: rule.severity || 'error',
|
|
479
|
-
fields: fieldsToCheck,
|
|
480
|
-
};
|
|
481
|
-
} catch (error) {
|
|
482
|
-
// If query fails, treat as validation error
|
|
483
|
-
return {
|
|
484
|
-
rule: rule.name,
|
|
485
|
-
valid: false,
|
|
486
|
-
message: `Uniqueness check failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
487
|
-
severity: rule.severity || 'error',
|
|
488
|
-
fields: fieldsToCheck,
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Extract field names from a validation condition.
|
|
495
|
-
*/
|
|
496
|
-
private extractFieldsFromCondition(condition: ValidationCondition): string[] {
|
|
497
|
-
const fields: string[] = [];
|
|
498
|
-
|
|
499
|
-
if (condition.field) {
|
|
500
|
-
fields.push(condition.field);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (condition.all_of) {
|
|
504
|
-
for (const subcondition of condition.all_of) {
|
|
505
|
-
fields.push(...this.extractFieldsFromCondition(subcondition));
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (condition.any_of) {
|
|
510
|
-
for (const subcondition of condition.any_of) {
|
|
511
|
-
fields.push(...this.extractFieldsFromCondition(subcondition));
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return fields;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Validate business rule by evaluating constraint conditions.
|
|
520
|
-
*/
|
|
521
|
-
private async validateBusinessRule(
|
|
522
|
-
rule: BusinessRuleValidationRule,
|
|
523
|
-
context: ValidationContext
|
|
524
|
-
): Promise<ValidationRuleResult> {
|
|
525
|
-
if (!rule.constraint) {
|
|
526
|
-
// No constraint specified, validation passes
|
|
527
|
-
return {
|
|
528
|
-
rule: rule.name,
|
|
529
|
-
valid: true,
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const constraint = rule.constraint;
|
|
534
|
-
let valid = true;
|
|
535
|
-
|
|
536
|
-
// Evaluate all_of conditions (all must be true)
|
|
537
|
-
if (constraint.all_of && constraint.all_of.length > 0) {
|
|
538
|
-
valid = constraint.all_of.every(condition => this.evaluateCondition(condition, context.record));
|
|
539
|
-
|
|
540
|
-
if (!valid) {
|
|
541
|
-
return {
|
|
542
|
-
rule: rule.name,
|
|
543
|
-
valid: false,
|
|
544
|
-
message: this.formatMessage(rule.message, context.record),
|
|
545
|
-
error_code: rule.error_code,
|
|
546
|
-
severity: rule.severity || 'error',
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Evaluate any_of conditions (at least one must be true)
|
|
552
|
-
if (constraint.any_of && constraint.any_of.length > 0) {
|
|
553
|
-
valid = constraint.any_of.some(condition => this.evaluateCondition(condition, context.record));
|
|
554
|
-
|
|
555
|
-
if (!valid) {
|
|
556
|
-
return {
|
|
557
|
-
rule: rule.name,
|
|
558
|
-
valid: false,
|
|
559
|
-
message: this.formatMessage(rule.message, context.record),
|
|
560
|
-
error_code: rule.error_code,
|
|
561
|
-
severity: rule.severity || 'error',
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Evaluate expression if provided (basic implementation)
|
|
567
|
-
if (constraint.expression) {
|
|
568
|
-
// For now, we'll treat expression validation as a stub
|
|
569
|
-
// Full implementation would require safe expression evaluation
|
|
570
|
-
// This could be extended to use a safe expression evaluator in the future
|
|
571
|
-
valid = true;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Evaluate then_require conditions (conditional required fields)
|
|
575
|
-
if (constraint.then_require && constraint.then_require.length > 0) {
|
|
576
|
-
for (const condition of constraint.then_require) {
|
|
577
|
-
const conditionMet = this.evaluateCondition(condition, context.record);
|
|
578
|
-
|
|
579
|
-
if (!conditionMet) {
|
|
580
|
-
return {
|
|
581
|
-
rule: rule.name,
|
|
582
|
-
valid: false,
|
|
583
|
-
message: this.formatMessage(rule.message, context.record),
|
|
584
|
-
error_code: rule.error_code,
|
|
585
|
-
severity: rule.severity || 'error',
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return {
|
|
592
|
-
rule: rule.name,
|
|
593
|
-
valid,
|
|
594
|
-
message: valid ? undefined : this.formatMessage(rule.message, context.record),
|
|
595
|
-
error_code: rule.error_code,
|
|
596
|
-
severity: rule.severity || 'error',
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Validate custom rule (stub - requires function execution).
|
|
602
|
-
*/
|
|
603
|
-
private async validateCustom(
|
|
604
|
-
rule: CustomValidationRule,
|
|
605
|
-
context: ValidationContext
|
|
606
|
-
): Promise<ValidationRuleResult> {
|
|
607
|
-
// TODO: Implement custom validator execution
|
|
608
|
-
// This requires safe function evaluation
|
|
609
|
-
// Stub: Pass silently until implementation is complete
|
|
610
|
-
return {
|
|
611
|
-
rule: rule.name,
|
|
612
|
-
valid: true,
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/**
|
|
617
|
-
* Evaluate a validation condition.
|
|
618
|
-
*/
|
|
619
|
-
private evaluateCondition(condition: ValidationCondition, record: ObjectDoc): boolean {
|
|
620
|
-
// Handle logical operators
|
|
621
|
-
if (condition.all_of) {
|
|
622
|
-
return condition.all_of.every(c => this.evaluateCondition(c, record));
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (condition.any_of) {
|
|
626
|
-
return condition.any_of.some(c => this.evaluateCondition(c, record));
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Handle expression
|
|
630
|
-
if (condition.expression) {
|
|
631
|
-
// TODO: Implement safe expression evaluation
|
|
632
|
-
return true;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Handle field comparison
|
|
636
|
-
if (condition.field && condition.operator !== undefined) {
|
|
637
|
-
const fieldValue = record[condition.field];
|
|
638
|
-
// Use compare_to if specified (cross-field comparison), otherwise use value
|
|
639
|
-
const compareValue = condition.compare_to !== undefined
|
|
640
|
-
? record[condition.compare_to]
|
|
641
|
-
: condition.value;
|
|
642
|
-
return this.compareValues(fieldValue, condition.operator, compareValue);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
return true;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* Compare two values using an operator.
|
|
650
|
-
*/
|
|
651
|
-
private compareValues(a: any, operator: ValidationOperator, b: any): boolean {
|
|
652
|
-
switch (operator) {
|
|
653
|
-
case '=':
|
|
654
|
-
return a === b;
|
|
655
|
-
case '!=':
|
|
656
|
-
return a !== b;
|
|
657
|
-
case '>':
|
|
658
|
-
return a > b;
|
|
659
|
-
case '>=':
|
|
660
|
-
return a >= b;
|
|
661
|
-
case '<':
|
|
662
|
-
return a < b;
|
|
663
|
-
case '<=':
|
|
664
|
-
return a <= b;
|
|
665
|
-
case 'in':
|
|
666
|
-
return Array.isArray(b) && b.includes(a);
|
|
667
|
-
case 'not_in':
|
|
668
|
-
return Array.isArray(b) && !b.includes(a);
|
|
669
|
-
case 'contains': {
|
|
670
|
-
if (a == null || b == null) {
|
|
671
|
-
return false;
|
|
672
|
-
}
|
|
673
|
-
const strA = String(a);
|
|
674
|
-
const strB = String(b);
|
|
675
|
-
return strA.includes(strB);
|
|
676
|
-
}
|
|
677
|
-
case 'not_contains': {
|
|
678
|
-
if (a == null || b == null) {
|
|
679
|
-
return false;
|
|
680
|
-
}
|
|
681
|
-
const strA = String(a);
|
|
682
|
-
const strB = String(b);
|
|
683
|
-
return !strA.includes(strB);
|
|
684
|
-
}
|
|
685
|
-
case 'starts_with': {
|
|
686
|
-
if (a == null || b == null) {
|
|
687
|
-
return false;
|
|
688
|
-
}
|
|
689
|
-
const strA = String(a);
|
|
690
|
-
const strB = String(b);
|
|
691
|
-
return strA.startsWith(strB);
|
|
692
|
-
}
|
|
693
|
-
case 'ends_with': {
|
|
694
|
-
if (a == null || b == null) {
|
|
695
|
-
return false;
|
|
696
|
-
}
|
|
697
|
-
const strA = String(a);
|
|
698
|
-
const strB = String(b);
|
|
699
|
-
return strA.endsWith(strB);
|
|
700
|
-
}
|
|
701
|
-
default:
|
|
702
|
-
return false;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Format validation message with template variables.
|
|
708
|
-
*/
|
|
709
|
-
private formatMessage(message: string | Record<string, string>, context: any): string {
|
|
710
|
-
// Handle i18n messages
|
|
711
|
-
if (typeof message === 'object') {
|
|
712
|
-
// Try preferred language first
|
|
713
|
-
const preferredLanguage = this.options.language ?? 'en';
|
|
714
|
-
let messageText = message[preferredLanguage];
|
|
715
|
-
|
|
716
|
-
// Try fallback languages if preferred not available
|
|
717
|
-
if (!messageText && this.options.languageFallback) {
|
|
718
|
-
for (const lang of this.options.languageFallback) {
|
|
719
|
-
if (message[lang]) {
|
|
720
|
-
messageText = message[lang];
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Fallback to first available message
|
|
727
|
-
message = messageText || Object.values(message)[0];
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Replace template variables
|
|
731
|
-
return message.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, path) => {
|
|
732
|
-
const value = this.getNestedValue(context, path);
|
|
733
|
-
return value !== undefined ? String(value) : match;
|
|
734
|
-
});
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
/**
|
|
738
|
-
* Get nested value from object by path.
|
|
739
|
-
*/
|
|
740
|
-
private getNestedValue(obj: any, path: string): any {
|
|
741
|
-
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
742
|
-
}
|
|
743
|
-
}
|