@object-ui/core 0.3.0 → 0.5.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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +8 -0
- package/dist/actions/ActionRunner.d.ts +40 -0
- package/dist/actions/ActionRunner.js +160 -0
- package/dist/actions/index.d.ts +8 -0
- package/dist/actions/index.js +8 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.js +10 -0
- package/dist/builder/schema-builder.d.ts +7 -0
- package/dist/builder/schema-builder.js +4 -6
- package/dist/evaluator/ExpressionCache.d.ts +101 -0
- package/dist/evaluator/ExpressionCache.js +135 -0
- package/dist/evaluator/ExpressionContext.d.ts +51 -0
- package/dist/evaluator/ExpressionContext.js +110 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +117 -0
- package/dist/evaluator/ExpressionEvaluator.js +220 -0
- package/dist/evaluator/index.d.ts +10 -0
- package/dist/evaluator/index.js +10 -0
- package/dist/index.d.ts +17 -4
- package/dist/index.js +16 -5
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +6 -0
- package/dist/query/query-ast.d.ts +32 -0
- package/dist/query/query-ast.js +268 -0
- package/dist/registry/PluginScopeImpl.d.ts +80 -0
- package/dist/registry/PluginScopeImpl.js +243 -0
- package/dist/registry/PluginSystem.d.ts +66 -0
- package/dist/registry/PluginSystem.js +142 -0
- package/dist/registry/Registry.d.ts +80 -4
- package/dist/registry/Registry.js +119 -7
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/filter-converter.d.ts +57 -0
- package/dist/utils/filter-converter.js +100 -0
- package/dist/validation/index.d.ts +9 -0
- package/dist/validation/index.js +9 -0
- package/dist/validation/schema-validator.d.ts +7 -0
- package/dist/validation/schema-validator.js +4 -6
- package/dist/validation/validation-engine.d.ts +70 -0
- package/dist/validation/validation-engine.js +363 -0
- package/dist/validation/validators/index.d.ts +16 -0
- package/dist/validation/validators/index.js +16 -0
- package/dist/validation/validators/object-validation-engine.d.ts +118 -0
- package/dist/validation/validators/object-validation-engine.js +538 -0
- package/package.json +26 -7
- package/src/actions/ActionRunner.ts +195 -0
- package/src/actions/index.ts +9 -0
- package/src/adapters/README.md +180 -0
- package/src/adapters/index.ts +10 -0
- package/src/builder/schema-builder.ts +8 -0
- package/src/evaluator/ExpressionCache.ts +192 -0
- package/src/evaluator/ExpressionContext.ts +118 -0
- package/src/evaluator/ExpressionEvaluator.ts +267 -0
- package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
- package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +101 -0
- package/src/evaluator/index.ts +11 -0
- package/src/index.test.ts +8 -0
- package/src/index.ts +18 -5
- package/src/query/__tests__/query-ast.test.ts +211 -0
- package/src/query/__tests__/window-functions.test.ts +275 -0
- package/src/query/index.ts +7 -0
- package/src/query/query-ast.ts +341 -0
- package/src/registry/PluginScopeImpl.ts +259 -0
- package/src/registry/PluginSystem.ts +161 -0
- package/src/registry/Registry.ts +133 -8
- package/src/registry/__tests__/PluginSystem.test.ts +226 -0
- package/src/registry/__tests__/Registry.test.ts +293 -0
- package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
- package/src/types/index.ts +8 -0
- package/src/utils/__tests__/filter-converter.test.ts +118 -0
- package/src/utils/filter-converter.ts +133 -0
- package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
- package/src/validation/__tests__/validation-engine.test.ts +102 -0
- package/src/validation/index.ts +10 -0
- package/src/validation/schema-validator.ts +8 -0
- package/src/validation/validation-engine.ts +461 -0
- package/src/validation/validators/index.ts +25 -0
- package/src/validation/validators/object-validation-engine.ts +722 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +2 -0
- package/src/builder/schema-builder.d.ts +0 -287
- package/src/builder/schema-builder.js +0 -505
- package/src/index.d.ts +0 -4
- package/src/index.js +0 -7
- package/src/registry/Registry.d.ts +0 -49
- package/src/registry/Registry.js +0 -36
- package/src/types/index.d.ts +0 -12
- package/src/types/index.js +0 -1
- package/src/validation/schema-validator.d.ts +0 -87
- package/src/validation/schema-validator.js +0 -280
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-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
|
+
* Simple expression evaluator using a simple parser (no dynamic code execution)
|
|
10
|
+
*
|
|
11
|
+
* SECURITY: This implementation parses expressions into an AST and evaluates them
|
|
12
|
+
* without using eval() or new Function(). It supports:
|
|
13
|
+
* - Comparison operators: ==, !=, >, <, >=, <=
|
|
14
|
+
* - Logical operators: &&, ||, !
|
|
15
|
+
* - Property access: record.field, record['field']
|
|
16
|
+
* - Literals: true, false, null, numbers, strings
|
|
17
|
+
*
|
|
18
|
+
* LIMITATIONS:
|
|
19
|
+
* - Single comparison operator per expression (no chaining like a > b > c)
|
|
20
|
+
* - Simple escape sequence handling (doesn't handle escaped backslashes)
|
|
21
|
+
* - Field names in bracket notation cannot contain escaped quotes
|
|
22
|
+
*
|
|
23
|
+
* For more complex expressions, integrate a dedicated library like:
|
|
24
|
+
* - JSONLogic (jsonlogic.com)
|
|
25
|
+
* - filtrex
|
|
26
|
+
*
|
|
27
|
+
* @see https://github.com/objectstack-ai/objectui/blob/main/SECURITY_FIX_SUMMARY.md
|
|
28
|
+
*/
|
|
29
|
+
class SimpleExpressionEvaluator {
|
|
30
|
+
evaluate(expression, context) {
|
|
31
|
+
try {
|
|
32
|
+
return this.evaluateSafeExpression(expression.trim(), context);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('Expression evaluation error:', error);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Safely evaluate an expression without using dynamic code execution
|
|
41
|
+
*/
|
|
42
|
+
evaluateSafeExpression(expr, context) {
|
|
43
|
+
// Handle boolean literals
|
|
44
|
+
if (expr === 'true')
|
|
45
|
+
return true;
|
|
46
|
+
if (expr === 'false')
|
|
47
|
+
return false;
|
|
48
|
+
if (expr === 'null')
|
|
49
|
+
return null;
|
|
50
|
+
// Handle string literals
|
|
51
|
+
if ((expr.startsWith('"') && expr.endsWith('"')) ||
|
|
52
|
+
(expr.startsWith("'") && expr.endsWith("'"))) {
|
|
53
|
+
return expr.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
// Handle numeric literals
|
|
56
|
+
if (/^-?\d+(\.\d+)?$/.test(expr)) {
|
|
57
|
+
return parseFloat(expr);
|
|
58
|
+
}
|
|
59
|
+
// Handle logical NOT
|
|
60
|
+
if (expr.startsWith('!')) {
|
|
61
|
+
return !this.evaluateSafeExpression(expr.slice(1).trim(), context);
|
|
62
|
+
}
|
|
63
|
+
// Handle logical AND
|
|
64
|
+
if (expr.includes('&&')) {
|
|
65
|
+
const parts = this.splitOnOperator(expr, '&&');
|
|
66
|
+
return parts.every(part => this.evaluateSafeExpression(part, context));
|
|
67
|
+
}
|
|
68
|
+
// Handle logical OR
|
|
69
|
+
if (expr.includes('||')) {
|
|
70
|
+
const parts = this.splitOnOperator(expr, '||');
|
|
71
|
+
return parts.some(part => this.evaluateSafeExpression(part, context));
|
|
72
|
+
}
|
|
73
|
+
// Handle comparison operators
|
|
74
|
+
const comparisonMatch = expr.match(/^(.+?)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/);
|
|
75
|
+
if (comparisonMatch) {
|
|
76
|
+
const [, left, op, right] = comparisonMatch;
|
|
77
|
+
const leftVal = this.evaluateSafeExpression(left.trim(), context);
|
|
78
|
+
const rightVal = this.evaluateSafeExpression(right.trim(), context);
|
|
79
|
+
switch (op) {
|
|
80
|
+
case '===':
|
|
81
|
+
return leftVal === rightVal;
|
|
82
|
+
case '==':
|
|
83
|
+
// Use loose equality for backward compatibility with existing expressions
|
|
84
|
+
// eslint-disable-next-line eqeqeq
|
|
85
|
+
return leftVal == rightVal;
|
|
86
|
+
case '!==':
|
|
87
|
+
return leftVal !== rightVal;
|
|
88
|
+
case '!=':
|
|
89
|
+
// Use loose inequality for backward compatibility with existing expressions
|
|
90
|
+
// eslint-disable-next-line eqeqeq
|
|
91
|
+
return leftVal != rightVal;
|
|
92
|
+
case '>': return leftVal > rightVal;
|
|
93
|
+
case '<': return leftVal < rightVal;
|
|
94
|
+
case '>=': return leftVal >= rightVal;
|
|
95
|
+
case '<=': return leftVal <= rightVal;
|
|
96
|
+
default: return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Handle property access (e.g., record.field or context.field)
|
|
100
|
+
return this.getValueFromContext(expr, context);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Split expression on operator, respecting parentheses and quotes
|
|
104
|
+
*/
|
|
105
|
+
splitOnOperator(expr, operator) {
|
|
106
|
+
const parts = [];
|
|
107
|
+
let current = '';
|
|
108
|
+
let depth = 0;
|
|
109
|
+
let inString = false;
|
|
110
|
+
let stringChar = '';
|
|
111
|
+
for (let i = 0; i < expr.length; i++) {
|
|
112
|
+
const char = expr[i];
|
|
113
|
+
const nextChar = expr[i + 1];
|
|
114
|
+
const prevChar = i > 0 ? expr[i - 1] : '';
|
|
115
|
+
// Handle string quotes, checking for escape sequences
|
|
116
|
+
if ((char === '"' || char === "'") && !inString) {
|
|
117
|
+
inString = true;
|
|
118
|
+
stringChar = char;
|
|
119
|
+
}
|
|
120
|
+
else if (char === stringChar && inString && prevChar !== '\\') {
|
|
121
|
+
// Only close string if quote is not escaped
|
|
122
|
+
inString = false;
|
|
123
|
+
}
|
|
124
|
+
if (!inString) {
|
|
125
|
+
if (char === '(')
|
|
126
|
+
depth++;
|
|
127
|
+
if (char === ')')
|
|
128
|
+
depth--;
|
|
129
|
+
if (depth === 0 && char === operator[0] && nextChar === operator[1]) {
|
|
130
|
+
parts.push(current.trim());
|
|
131
|
+
current = '';
|
|
132
|
+
i++; // Skip next character
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
current += char;
|
|
137
|
+
}
|
|
138
|
+
if (current) {
|
|
139
|
+
parts.push(current.trim());
|
|
140
|
+
}
|
|
141
|
+
return parts;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Get value from context by path (e.g., "record.age" or "age")
|
|
145
|
+
*/
|
|
146
|
+
getValueFromContext(path, context) {
|
|
147
|
+
// Handle bracket notation: record['field']
|
|
148
|
+
const bracketMatch = path.match(/^(\w+)\['([^']+)'\]$/);
|
|
149
|
+
if (bracketMatch) {
|
|
150
|
+
const [, obj, field] = bracketMatch;
|
|
151
|
+
return context[obj]?.[field];
|
|
152
|
+
}
|
|
153
|
+
// Handle dot notation: record.field or just field
|
|
154
|
+
const parts = path.split('.');
|
|
155
|
+
let value = context;
|
|
156
|
+
for (const part of parts) {
|
|
157
|
+
if (value && typeof value === 'object' && part in value) {
|
|
158
|
+
value = value[part];
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Try direct context access for simple identifiers
|
|
162
|
+
if (parts.length === 1 && part in context) {
|
|
163
|
+
return context[part];
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Object-Level Validation Engine
|
|
173
|
+
* Implements ObjectStack Spec v0.7.1 validation framework
|
|
174
|
+
*/
|
|
175
|
+
export class ObjectValidationEngine {
|
|
176
|
+
constructor(expressionEvaluator, uniquenessChecker) {
|
|
177
|
+
Object.defineProperty(this, "expressionEvaluator", {
|
|
178
|
+
enumerable: true,
|
|
179
|
+
configurable: true,
|
|
180
|
+
writable: true,
|
|
181
|
+
value: void 0
|
|
182
|
+
});
|
|
183
|
+
Object.defineProperty(this, "uniquenessChecker", {
|
|
184
|
+
enumerable: true,
|
|
185
|
+
configurable: true,
|
|
186
|
+
writable: true,
|
|
187
|
+
value: void 0
|
|
188
|
+
});
|
|
189
|
+
this.expressionEvaluator = expressionEvaluator || new SimpleExpressionEvaluator();
|
|
190
|
+
this.uniquenessChecker = uniquenessChecker;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Validate a record against a set of validation rules
|
|
194
|
+
*/
|
|
195
|
+
async validateRecord(rules, context, event = 'insert') {
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const rule of rules) {
|
|
198
|
+
// Check if rule is active
|
|
199
|
+
if (!rule.active) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// Check if rule applies to this event
|
|
203
|
+
if (!rule.events.includes(event)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const result = await this.validateRule(rule, context);
|
|
207
|
+
if (!result.valid) {
|
|
208
|
+
results.push(result);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Validate a single rule
|
|
215
|
+
*/
|
|
216
|
+
async validateRule(rule, context) {
|
|
217
|
+
switch (rule.type) {
|
|
218
|
+
case 'script':
|
|
219
|
+
return this.validateScript(rule, context);
|
|
220
|
+
case 'unique':
|
|
221
|
+
return this.validateUniqueness(rule, context);
|
|
222
|
+
case 'state_machine':
|
|
223
|
+
return this.validateStateMachine(rule, context);
|
|
224
|
+
case 'cross_field':
|
|
225
|
+
return this.validateCrossField(rule, context);
|
|
226
|
+
case 'async':
|
|
227
|
+
return this.validateAsync(rule, context);
|
|
228
|
+
case 'conditional':
|
|
229
|
+
return this.validateConditional(rule, context);
|
|
230
|
+
case 'format':
|
|
231
|
+
return this.validateFormat(rule, context);
|
|
232
|
+
case 'range':
|
|
233
|
+
return this.validateRange(rule, context);
|
|
234
|
+
default:
|
|
235
|
+
return {
|
|
236
|
+
valid: true,
|
|
237
|
+
message: `Unknown validation type: ${rule.type}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Validate script-based rule
|
|
243
|
+
*/
|
|
244
|
+
validateScript(rule, context) {
|
|
245
|
+
try {
|
|
246
|
+
const result = this.expressionEvaluator.evaluate(rule.condition, context.record);
|
|
247
|
+
if (!result) {
|
|
248
|
+
return {
|
|
249
|
+
valid: false,
|
|
250
|
+
message: rule.message,
|
|
251
|
+
rule: rule.name,
|
|
252
|
+
severity: rule.severity,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return { valid: true };
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
return {
|
|
259
|
+
valid: false,
|
|
260
|
+
message: `Script evaluation error: ${error}`,
|
|
261
|
+
rule: rule.name,
|
|
262
|
+
severity: 'error',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Validate uniqueness constraint
|
|
268
|
+
*/
|
|
269
|
+
async validateUniqueness(rule, context) {
|
|
270
|
+
if (!this.uniquenessChecker) {
|
|
271
|
+
console.warn('Uniqueness checker not configured');
|
|
272
|
+
return { valid: true };
|
|
273
|
+
}
|
|
274
|
+
const values = {};
|
|
275
|
+
for (const field of rule.fields) {
|
|
276
|
+
values[field] = context.record[field];
|
|
277
|
+
}
|
|
278
|
+
const isUnique = await this.uniquenessChecker(rule.fields, values, rule.scope, context);
|
|
279
|
+
if (!isUnique) {
|
|
280
|
+
return {
|
|
281
|
+
valid: false,
|
|
282
|
+
message: rule.message,
|
|
283
|
+
rule: rule.name,
|
|
284
|
+
severity: rule.severity,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return { valid: true };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Validate state machine transitions
|
|
291
|
+
*/
|
|
292
|
+
validateStateMachine(rule, context) {
|
|
293
|
+
const currentState = context.record[rule.stateField];
|
|
294
|
+
const previousState = context.oldRecord?.[rule.stateField];
|
|
295
|
+
// If no previous state (insert), allow any state
|
|
296
|
+
if (!previousState) {
|
|
297
|
+
return { valid: true };
|
|
298
|
+
}
|
|
299
|
+
// Check if transition is allowed
|
|
300
|
+
for (const transition of rule.transitions) {
|
|
301
|
+
const fromStates = Array.isArray(transition.from) ? transition.from : [transition.from];
|
|
302
|
+
if (!fromStates.includes(previousState)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (transition.to !== currentState) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
// Check condition if specified
|
|
309
|
+
if (transition.condition) {
|
|
310
|
+
const conditionMet = this.expressionEvaluator.evaluate(transition.condition, context.record);
|
|
311
|
+
if (!conditionMet) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Valid transition found
|
|
316
|
+
return { valid: true };
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
valid: false,
|
|
320
|
+
message: rule.message || `Invalid state transition from ${previousState} to ${currentState}`,
|
|
321
|
+
rule: rule.name,
|
|
322
|
+
severity: rule.severity,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Validate cross-field constraints
|
|
327
|
+
*/
|
|
328
|
+
validateCrossField(rule, context) {
|
|
329
|
+
try {
|
|
330
|
+
const result = this.expressionEvaluator.evaluate(rule.condition, context.record);
|
|
331
|
+
if (!result) {
|
|
332
|
+
return {
|
|
333
|
+
valid: false,
|
|
334
|
+
message: rule.message,
|
|
335
|
+
rule: rule.name,
|
|
336
|
+
severity: rule.severity,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return { valid: true };
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
return {
|
|
343
|
+
valid: false,
|
|
344
|
+
message: `Cross-field validation error: ${error}`,
|
|
345
|
+
rule: rule.name,
|
|
346
|
+
severity: 'error',
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Validate async/remote validation
|
|
352
|
+
*/
|
|
353
|
+
async validateAsync(rule, context) {
|
|
354
|
+
try {
|
|
355
|
+
const method = rule.method || 'POST';
|
|
356
|
+
const response = await fetch(rule.endpoint, {
|
|
357
|
+
method,
|
|
358
|
+
headers: {
|
|
359
|
+
'Content-Type': 'application/json',
|
|
360
|
+
},
|
|
361
|
+
body: method !== 'GET' ? JSON.stringify(context.record) : undefined,
|
|
362
|
+
});
|
|
363
|
+
const data = await response.json();
|
|
364
|
+
if (!data.valid) {
|
|
365
|
+
return {
|
|
366
|
+
valid: false,
|
|
367
|
+
message: data.message || rule.message,
|
|
368
|
+
rule: rule.name,
|
|
369
|
+
severity: rule.severity,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return { valid: true };
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
return {
|
|
376
|
+
valid: false,
|
|
377
|
+
message: `Async validation error: ${error}`,
|
|
378
|
+
rule: rule.name,
|
|
379
|
+
severity: 'error',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Validate conditional rules
|
|
385
|
+
*/
|
|
386
|
+
async validateConditional(rule, context) {
|
|
387
|
+
try {
|
|
388
|
+
const conditionMet = this.expressionEvaluator.evaluate(rule.condition, context.record);
|
|
389
|
+
if (!conditionMet) {
|
|
390
|
+
// Condition not met, validation passes
|
|
391
|
+
return { valid: true };
|
|
392
|
+
}
|
|
393
|
+
// Condition met, validate nested rules
|
|
394
|
+
for (const nestedRule of rule.rules) {
|
|
395
|
+
const result = await this.validateRule(nestedRule, context);
|
|
396
|
+
if (!result.valid) {
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { valid: true };
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
return {
|
|
404
|
+
valid: false,
|
|
405
|
+
message: `Conditional validation error: ${error}`,
|
|
406
|
+
rule: rule.name,
|
|
407
|
+
severity: 'error',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Validate format/pattern
|
|
413
|
+
*/
|
|
414
|
+
validateFormat(rule, context) {
|
|
415
|
+
const value = context.record[rule.field];
|
|
416
|
+
if (value === null || value === undefined || value === '') {
|
|
417
|
+
return { valid: true };
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
let pattern;
|
|
421
|
+
if (rule.format) {
|
|
422
|
+
// Use predefined format
|
|
423
|
+
pattern = this.getPredefinedPattern(rule.format);
|
|
424
|
+
}
|
|
425
|
+
else if (typeof rule.pattern === 'string') {
|
|
426
|
+
pattern = new RegExp(rule.pattern, rule.flags);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
pattern = rule.pattern;
|
|
430
|
+
}
|
|
431
|
+
if (!pattern.test(String(value))) {
|
|
432
|
+
return {
|
|
433
|
+
valid: false,
|
|
434
|
+
message: rule.message || `Invalid format for ${rule.field}`,
|
|
435
|
+
rule: rule.name,
|
|
436
|
+
severity: rule.severity,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return { valid: true };
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
return {
|
|
443
|
+
valid: false,
|
|
444
|
+
message: `Format validation error: ${error}`,
|
|
445
|
+
rule: rule.name,
|
|
446
|
+
severity: 'error',
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Validate range constraints
|
|
452
|
+
*/
|
|
453
|
+
validateRange(rule, context) {
|
|
454
|
+
const value = context.record[rule.field];
|
|
455
|
+
if (value === null || value === undefined) {
|
|
456
|
+
return { valid: true };
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
// Convert to comparable values
|
|
460
|
+
let compareValue;
|
|
461
|
+
let minValue;
|
|
462
|
+
let maxValue;
|
|
463
|
+
if (value instanceof Date || typeof value === 'string') {
|
|
464
|
+
compareValue = value instanceof Date ? value : new Date(value);
|
|
465
|
+
minValue = rule.min ? (rule.min instanceof Date ? rule.min : new Date(rule.min)) : undefined;
|
|
466
|
+
maxValue = rule.max ? (rule.max instanceof Date ? rule.max : new Date(rule.max)) : undefined;
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
compareValue = Number(value);
|
|
470
|
+
minValue = rule.min !== undefined ? Number(rule.min) : undefined;
|
|
471
|
+
maxValue = rule.max !== undefined ? Number(rule.max) : undefined;
|
|
472
|
+
}
|
|
473
|
+
// Check minimum
|
|
474
|
+
if (minValue !== undefined) {
|
|
475
|
+
const fails = rule.minExclusive
|
|
476
|
+
? compareValue <= minValue
|
|
477
|
+
: compareValue < minValue;
|
|
478
|
+
if (fails) {
|
|
479
|
+
return {
|
|
480
|
+
valid: false,
|
|
481
|
+
message: rule.message || `Value must be ${rule.minExclusive ? 'greater than' : 'at least'} ${rule.min}`,
|
|
482
|
+
rule: rule.name,
|
|
483
|
+
severity: rule.severity,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Check maximum
|
|
488
|
+
if (maxValue !== undefined) {
|
|
489
|
+
const fails = rule.maxExclusive
|
|
490
|
+
? compareValue >= maxValue
|
|
491
|
+
: compareValue > maxValue;
|
|
492
|
+
if (fails) {
|
|
493
|
+
return {
|
|
494
|
+
valid: false,
|
|
495
|
+
message: rule.message || `Value must be ${rule.maxExclusive ? 'less than' : 'at most'} ${rule.max}`,
|
|
496
|
+
rule: rule.name,
|
|
497
|
+
severity: rule.severity,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { valid: true };
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
return {
|
|
505
|
+
valid: false,
|
|
506
|
+
message: `Range validation error: ${error}`,
|
|
507
|
+
rule: rule.name,
|
|
508
|
+
severity: 'error',
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Get predefined regex pattern
|
|
514
|
+
*/
|
|
515
|
+
getPredefinedPattern(format) {
|
|
516
|
+
const patterns = {
|
|
517
|
+
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
518
|
+
url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)$/,
|
|
519
|
+
phone: /^[\d\s\-+()]+$/,
|
|
520
|
+
ipv4: /^(\d{1,3}\.){3}\d{1,3}$/,
|
|
521
|
+
ipv6: /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i,
|
|
522
|
+
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
523
|
+
iso_date: /^\d{4}-\d{2}-\d{2}$/,
|
|
524
|
+
credit_card: /^\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}$/,
|
|
525
|
+
};
|
|
526
|
+
return patterns[format] || /.*/;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Default instance
|
|
531
|
+
*/
|
|
532
|
+
export const defaultObjectValidationEngine = new ObjectValidationEngine();
|
|
533
|
+
/**
|
|
534
|
+
* Convenience function to validate a record
|
|
535
|
+
*/
|
|
536
|
+
export async function validateRecord(rules, context, event = 'insert') {
|
|
537
|
+
return defaultObjectValidationEngine.validateRecord(rules, context, event);
|
|
538
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"license": "MIT",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
6
|
+
"description": "Core logic, types, and validation for Object UI. Zero React dependencies.",
|
|
7
|
+
"homepage": "https://www.objectui.org",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/objectstack-ai/objectui.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/objectstack-ai/objectui/issues"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
7
25
|
"dependencies": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
26
|
+
"@objectstack/spec": "^0.9.1",
|
|
27
|
+
"lodash": "^4.17.23",
|
|
28
|
+
"zod": "^4.3.6",
|
|
29
|
+
"@object-ui/types": "0.5.0"
|
|
11
30
|
},
|
|
12
31
|
"devDependencies": {
|
|
13
32
|
"typescript": "^5.9.3",
|
|
14
|
-
"vitest": "^4.0.
|
|
33
|
+
"vitest": "^4.0.18"
|
|
15
34
|
},
|
|
16
35
|
"scripts": {
|
|
17
36
|
"build": "tsc",
|