@object-ui/core 16.1.0 → 17.2.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +1881 -4
  2. package/dist/actions/ActionEngine.d.ts +11 -1
  3. package/dist/actions/ActionEngine.js +14 -3
  4. package/dist/actions/ActionRunner.d.ts +199 -24
  5. package/dist/actions/ActionRunner.js +189 -84
  6. package/dist/actions/actionKeys.d.ts +135 -0
  7. package/dist/actions/actionKeys.js +283 -0
  8. package/dist/actions/index.d.ts +1 -0
  9. package/dist/actions/index.js +1 -0
  10. package/dist/adapters/ApiDataSource.js +10 -1
  11. package/dist/adapters/ValueDataSource.d.ts +2 -2
  12. package/dist/evaluator/fieldRules.d.ts +0 -2
  13. package/dist/evaluator/fieldRules.js +3 -2
  14. package/dist/evaluator/optionLint.js +5 -2
  15. package/dist/evaluator/optionRules.d.ts +13 -6
  16. package/dist/evaluator/optionRules.js +0 -23
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.js +7 -0
  19. package/dist/query/query-ast.d.ts +6 -6
  20. package/dist/query/query-ast.js +2 -2
  21. package/dist/registry/PluginSystem.d.ts +15 -5
  22. package/dist/registry/PluginSystem.js +1 -1
  23. package/dist/registry/Registry.d.ts +81 -2
  24. package/dist/registry/Registry.js +102 -2
  25. package/dist/registry/WidgetRegistry.d.ts +7 -7
  26. package/dist/registry/WidgetRegistry.js +1 -1
  27. package/dist/registry/public-blocks.js +52 -1
  28. package/dist/styling/scoped-styles.d.ts +14 -8
  29. package/dist/theme/ThemeEngine.js +0 -12
  30. package/dist/utils/chart-series.d.ts +42 -2
  31. package/dist/utils/chart-series.js +55 -0
  32. package/dist/utils/column-identity.d.ts +110 -0
  33. package/dist/utils/column-identity.js +210 -0
  34. package/dist/utils/dashboard-filters.d.ts +11 -2
  35. package/dist/utils/dashboard-filters.js +99 -4
  36. package/dist/utils/dataset-format.d.ts +13 -1
  37. package/dist/utils/dataset-format.js +10 -2
  38. package/dist/utils/expand-fields.js +8 -8
  39. package/dist/utils/filter-converter.d.ts +47 -0
  40. package/dist/utils/filter-converter.js +94 -7
  41. package/dist/utils/filter-tokens.d.ts +108 -0
  42. package/dist/utils/filter-tokens.js +180 -0
  43. package/dist/utils/freeze-schema.d.ts +14 -6
  44. package/dist/utils/freeze-schema.js +14 -6
  45. package/dist/utils/managedBy.d.ts +97 -51
  46. package/dist/utils/managedBy.js +117 -53
  47. package/dist/utils/normalize-list-view.d.ts +109 -0
  48. package/dist/utils/normalize-list-view.js +255 -0
  49. package/dist/utils/sort-values.d.ts +52 -0
  50. package/dist/utils/sort-values.js +125 -0
  51. package/dist/validation/index.d.ts +10 -1
  52. package/dist/validation/index.js +10 -1
  53. package/dist/validation/required-presence.d.ts +31 -0
  54. package/dist/validation/required-presence.js +36 -0
  55. package/dist/validation/schema-validator.d.ts +24 -8
  56. package/dist/validation/validators/index.d.ts +7 -1
  57. package/dist/validation/validators/index.js +7 -1
  58. package/dist/validation/validators/object-validation-engine.d.ts +151 -31
  59. package/dist/validation/validators/object-validation-engine.js +275 -142
  60. package/package.json +4 -4
@@ -5,6 +5,50 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ /** The spec's `events` default, applied here because `/meta` ships it absent. */
9
+ const DEFAULT_EVENTS = ['insert', 'update'];
10
+ /** The spec's `priority` default (lower runs first). */
11
+ const DEFAULT_PRIORITY = 100;
12
+ /**
13
+ * Read a predicate out of an `ExpressionInput` — the spec's wire shape for every
14
+ * expression-valued key, which is `string | { dialect, source?, ast? }`.
15
+ *
16
+ * Returns `null` when there is nothing evaluable (an `ast`-only envelope, an
17
+ * empty source, a non-CEL dialect). `null` means BROKEN, and every caller
18
+ * fails open on it, mirroring the server.
19
+ */
20
+ function expressionSource(input) {
21
+ if (typeof input === 'string')
22
+ return input.trim() === '' ? null : input;
23
+ if (input && typeof input === 'object') {
24
+ const { dialect, source } = input;
25
+ // `cron` / `template` are not predicates, and an absent dialect is not the
26
+ // spec's envelope at all. Either way there is nothing to evaluate here.
27
+ if (dialect !== undefined && dialect !== 'cel')
28
+ return null;
29
+ if (typeof source === 'string' && source.trim() !== '')
30
+ return source;
31
+ }
32
+ return null;
33
+ }
34
+ // The three readers below take `unknown` on purpose. They run against metadata
35
+ // as it arrives over `/meta`, where any of these keys may be absent, and an
36
+ // absent key must resolve to the SPEC's default rather than to JavaScript's
37
+ // falsiness. Narrowing here keeps that decision in one place.
38
+ /** `active` defaults to `true`; only an explicit `false` disables a rule. */
39
+ function isActive(rule) {
40
+ return rule?.active !== false;
41
+ }
42
+ /** `events` defaults to `['insert','update']`. */
43
+ function eventsOf(rule) {
44
+ const events = rule?.events;
45
+ return Array.isArray(events) && events.length > 0 ? events : DEFAULT_EVENTS;
46
+ }
47
+ /** `priority` defaults to 100 (lower runs first). */
48
+ function priorityOf(rule) {
49
+ const priority = rule?.priority;
50
+ return typeof priority === 'number' ? priority : DEFAULT_PRIORITY;
51
+ }
8
52
  /**
9
53
  * Simple expression evaluator using a simple parser (no dynamic code execution)
10
54
  *
@@ -20,9 +64,13 @@
20
64
  * - Simple escape sequence handling (doesn't handle escaped backslashes)
21
65
  * - Field names in bracket notation cannot contain escaped quotes
22
66
  *
23
- * For more complex expressions, integrate a dedicated library like:
24
- * - JSONLogic (jsonlogic.com)
25
- * - filtrex
67
+ * NOT CEL. The spec's `dialect: 'cel'` predicates are evaluated server-side by
68
+ * `@objectstack/formula`; this parser understands the common comparison shape
69
+ * and nothing more. Anything it cannot parse yields a falsy value, which under
70
+ * this engine's polarity (TRUE ⇒ violated) means the rule fails OPEN — the same
71
+ * outcome the server produces for an un-evaluable predicate. To make the
72
+ * pre-check agree with the server on richer predicates, pass a CEL-backed
73
+ * `ValidationExpressionEvaluator` to the constructor.
26
74
  *
27
75
  * @see https://github.com/objectstack-ai/objectui/blob/main/SECURITY_FIX_SUMMARY.md
28
76
  */
@@ -169,8 +217,15 @@ class SimpleExpressionEvaluator {
169
217
  }
170
218
  }
171
219
  /**
172
- * Object-Level Validation Engine
173
- * Implements ObjectStack Spec v2.0.1 validation framework
220
+ * Object-Level Validation Engine — the client pre-check described at the top of
221
+ * this module.
222
+ *
223
+ * @deprecated (#3110) Validation rules are enforcement, and enforcement is
224
+ * single-implementation on the server (`objectql`'s rule-validator). Render the
225
+ * server's rejection instead; for pre-submit feedback, use a validate-only
226
+ * (dry-run) write. Still functional and still correct as of #3103 — but it
227
+ * cannot stay in step with the server without a conformance-fixture mechanism
228
+ * that does not exist yet. See the module header.
174
229
  */
175
230
  export class ObjectValidationEngine {
176
231
  constructor(expressionEvaluator, uniquenessChecker) {
@@ -190,20 +245,21 @@ export class ObjectValidationEngine {
190
245
  this.uniquenessChecker = uniquenessChecker;
191
246
  }
192
247
  /**
193
- * Validate a record against a set of validation rules
248
+ * Validate a record against a set of validation rules.
249
+ *
250
+ * Rules run in `priority` order (lower first, stable within a priority), and
251
+ * only when `active` is not explicitly `false` and `events` covers this write
252
+ * context — reading the spec's defaults, not the falsiness of an absent key.
194
253
  */
195
254
  async validateRecord(rules, context, event = 'insert') {
196
255
  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);
256
+ const applicable = rules
257
+ .filter((rule) => isActive(rule) && eventsOf(rule).includes(event))
258
+ // `sort` is stable in every engine objectui targets (ES2019+), so equal
259
+ // priorities keep their authored order.
260
+ .sort((a, b) => priorityOf(a) - priorityOf(b));
261
+ for (const rule of applicable) {
262
+ const result = await this.validateRule(rule, context, event);
207
263
  if (!result.valid) {
208
264
  results.push(result);
209
265
  }
@@ -211,40 +267,76 @@ export class ObjectValidationEngine {
211
267
  return results;
212
268
  }
213
269
  /**
214
- * Validate a single rule
270
+ * Validate a single rule.
271
+ *
272
+ * `rule` is widened past `ObjectValidationRule` because a `conditional`'s
273
+ * `then` / `otherwise` branch may be any rule the SPEC's union admits —
274
+ * including `json_schema`, which this engine does not implement.
215
275
  */
216
- async validateRule(rule, context) {
276
+ async validateRule(rule, context, event) {
217
277
  switch (rule.type) {
218
278
  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);
279
+ return this.validatePredicate(rule, context, 'Script');
224
280
  case 'cross_field':
225
- return this.validateCrossField(rule, context);
226
- case 'async':
227
- return this.validateAsync(rule, context);
281
+ return this.validatePredicate(rule, context, 'Cross-field');
282
+ case 'state_machine':
283
+ return this.validateStateMachine(rule, context, event);
228
284
  case 'conditional':
229
- return this.validateConditional(rule, context);
285
+ return this.validateConditional(rule, context, event);
230
286
  case 'format':
231
287
  return this.validateFormat(rule, context);
288
+ // Deprecated objectui-local variants — see @object-ui/types. The spec's
289
+ // union rejects these, so they cannot arrive from `/meta`.
290
+ case 'unique':
291
+ return this.validateUniqueness(rule, context);
292
+ case 'async':
293
+ return this.validateAsync(rule, context);
232
294
  case 'range':
233
295
  return this.validateRange(rule, context);
234
296
  default:
235
- return {
236
- valid: true,
237
- message: `Unknown validation type: ${rule.type}`,
238
- };
297
+ // Loud, not silent. `json_schema` is a real spec variant this engine
298
+ // does not implement; reporting it as valid would let the pre-check
299
+ // green-light a record the server is about to reject, with nothing in
300
+ // the console to explain the disagreement.
301
+ console.warn(`[ObjectValidationEngine] rule '${rule.name ?? '(unnamed)'}' has ` +
302
+ `type '${rule.type}', which this client engine does not evaluate — ` +
303
+ `not checked here; the server remains the authority.`);
304
+ return { valid: true };
239
305
  }
240
306
  }
241
307
  /**
242
- * Validate script-based rule
308
+ * The evaluation scope for a predicate.
309
+ *
310
+ * The server binds `{ record, previous }`, so a spec-authored predicate reads
311
+ * `record.amount`. objectui's own rules have always been written against the
312
+ * bare field name (`amount`). Both are bound — the roots last so a field
313
+ * literally called `record` cannot shadow them.
243
314
  */
244
- validateScript(rule, context) {
315
+ scopeFor(context) {
316
+ return {
317
+ ...context.record,
318
+ record: context.record,
319
+ previous: context.oldRecord,
320
+ user: context.user,
321
+ };
322
+ }
323
+ /**
324
+ * Evaluate a `script` / `cross_field` predicate.
325
+ *
326
+ * Both variants share one path on the server, and so do they here. The
327
+ * predicate is the FAILURE condition: TRUE means the rule is violated. A
328
+ * predicate that cannot be evaluated is a BROKEN rule and fails open.
329
+ */
330
+ validatePredicate(rule, context, label) {
331
+ const source = expressionSource(rule.condition);
332
+ if (source === null) {
333
+ console.warn(`[ObjectValidationEngine] ${label.toLowerCase()} rule '${rule.name}' has no evaluable ` +
334
+ `condition — skipped (the server remains the authority).`);
335
+ return { valid: true };
336
+ }
245
337
  try {
246
- const result = this.expressionEvaluator.evaluate(rule.condition, context.record);
247
- if (!result) {
338
+ const violated = this.expressionEvaluator.evaluate(source, this.scopeFor(context));
339
+ if (violated === true) {
248
340
  return {
249
341
  valid: false,
250
342
  message: rule.message,
@@ -255,12 +347,10 @@ export class ObjectValidationEngine {
255
347
  return { valid: true };
256
348
  }
257
349
  catch (error) {
258
- return {
259
- valid: false,
260
- message: `Script evaluation error: ${error}`,
261
- rule: rule.name,
262
- severity: 'error',
263
- };
350
+ // Broken, not violated — mirror the server and fail open.
351
+ console.warn(`[ObjectValidationEngine] ${label.toLowerCase()} rule '${rule.name}' failed to ` +
352
+ `evaluate (${error}) — skipped.`);
353
+ return { valid: true };
264
354
  }
265
355
  }
266
356
  /**
@@ -287,31 +377,65 @@ export class ObjectValidationEngine {
287
377
  return { valid: true };
288
378
  }
289
379
  /**
290
- * Validate state machine transitions
380
+ * Validate state machine transitions (ADR-0020).
381
+ *
382
+ * A flat `field` plus a `{ from: [allowedTo] }` map. Semantics mirror the
383
+ * server-side rule-validator:
384
+ *
385
+ * INSERT
386
+ * - no `initialStates` declared → allow (legacy no-op)
387
+ * - field absent from the write → allow (requiredness is not our job)
388
+ * - value empty → allow (ditto)
389
+ * - value ∉ initialStates → reject (the FSM entry point, #3165)
390
+ *
391
+ * UPDATE
392
+ * - field absent from the write → allow (no transition to police)
393
+ * - no prior record → allow (cannot reason)
394
+ * - field unchanged, or cleared → allow (no transition)
395
+ * - `from` not declared in the map → lenient allow (unconstrained)
396
+ * - `to` ∉ transitions[from] → reject
291
397
  */
292
- validateStateMachine(rule, context) {
293
- // ADR-0020 flat FSM: a `field` plus a `{ from: [allowedTo] }` map.
294
- // Semantics mirror the server-side rule-validator:
295
- // - insert / no prior state → allow (initial assignment)
296
- // - field unchanged (from === to) → allow (no transition)
297
- // - `from` not declared in the map → lenient allow (unconstrained)
298
- // - `to` ∉ transitions[from] → reject
398
+ validateStateMachine(rule, context, event) {
399
+ const touched = rule.field in context.record;
400
+ if (event === 'insert') {
401
+ const initial = rule.initialStates;
402
+ // No entry-point contract — insert is unconstrained (legacy behavior).
403
+ if (!Array.isArray(initial) || initial.length === 0)
404
+ return { valid: true };
405
+ if (!touched)
406
+ return { valid: true };
407
+ const value = context.record[rule.field];
408
+ // Empty is requiredness's problem, not the FSM's.
409
+ if (value === undefined || value === null || value === '')
410
+ return { valid: true };
411
+ if (initial.includes(String(value)))
412
+ return { valid: true };
413
+ return {
414
+ valid: false,
415
+ message: rule.message || `'${String(value)}' is not a valid initial state for ${rule.field}`,
416
+ rule: rule.name,
417
+ severity: rule.severity,
418
+ };
419
+ }
420
+ // The state field wasn't part of this write — nothing to police.
421
+ if (!touched)
422
+ return { valid: true };
299
423
  const currentState = context.record[rule.field];
300
424
  const previousState = context.oldRecord?.[rule.field];
301
- // No prior state (insert) — any initial value is allowed.
425
+ // No prior state — cannot reason about a transition.
302
426
  if (previousState === undefined || previousState === null) {
303
427
  return { valid: true };
304
428
  }
305
- // The state field wasn't part of this change — nothing to police.
306
- if (previousState === currentState) {
429
+ // Unchanged, or cleared — no transition to enforce.
430
+ if (previousState === currentState || currentState === undefined || currentState === null) {
307
431
  return { valid: true };
308
432
  }
309
- const allowed = rule.transitions[previousState];
433
+ const allowed = rule.transitions[String(previousState)];
310
434
  // `from` state not declared — leave it unconstrained (lenient).
311
- if (allowed === undefined) {
435
+ if (!Array.isArray(allowed)) {
312
436
  return { valid: true };
313
437
  }
314
- if (allowed.includes(currentState)) {
438
+ if (allowed.includes(String(currentState))) {
315
439
  return { valid: true };
316
440
  }
317
441
  return {
@@ -321,31 +445,6 @@ export class ObjectValidationEngine {
321
445
  severity: rule.severity,
322
446
  };
323
447
  }
324
- /**
325
- * Validate cross-field constraints
326
- */
327
- validateCrossField(rule, context) {
328
- try {
329
- const result = this.expressionEvaluator.evaluate(rule.condition, context.record);
330
- if (!result) {
331
- return {
332
- valid: false,
333
- message: rule.message,
334
- rule: rule.name,
335
- severity: rule.severity,
336
- };
337
- }
338
- return { valid: true };
339
- }
340
- catch (error) {
341
- return {
342
- valid: false,
343
- message: `Cross-field validation error: ${error}`,
344
- rule: rule.name,
345
- severity: 'error',
346
- };
347
- }
348
- }
349
448
  /**
350
449
  * Validate async/remote validation
351
450
  */
@@ -380,71 +479,75 @@ export class ObjectValidationEngine {
380
479
  }
381
480
  }
382
481
  /**
383
- * Validate conditional rules
482
+ * Validate a conditional rule.
483
+ *
484
+ * Evaluates `when`, then recurses into `then` (TRUE) or `otherwise` (FALSE).
485
+ * The nested rule supplies the violation; an un-evaluable `when` is a broken
486
+ * rule and fails open. A branch whose own `active` is explicitly `false` is
487
+ * skipped, matching the server.
384
488
  */
385
- async validateConditional(rule, context) {
386
- try {
387
- const conditionMet = this.expressionEvaluator.evaluate(rule.condition, context.record);
388
- if (!conditionMet) {
389
- // Condition not met, validation passes
390
- return { valid: true };
391
- }
392
- // Condition met, validate nested rules
393
- for (const nestedRule of rule.rules) {
394
- const result = await this.validateRule(nestedRule, context);
395
- if (!result.valid) {
396
- return result;
397
- }
398
- }
489
+ async validateConditional(rule, context, event) {
490
+ const source = expressionSource(rule.when);
491
+ if (source === null) {
492
+ console.warn(`[ObjectValidationEngine] conditional rule '${rule.name}' has no evaluable ` +
493
+ `\`when\` predicate — skipped (the server remains the authority).`);
399
494
  return { valid: true };
400
495
  }
496
+ let conditionMet;
497
+ try {
498
+ conditionMet = this.expressionEvaluator.evaluate(source, this.scopeFor(context));
499
+ }
401
500
  catch (error) {
402
- return {
403
- valid: false,
404
- message: `Conditional validation error: ${error}`,
405
- rule: rule.name,
406
- severity: 'error',
407
- };
501
+ console.warn(`[ObjectValidationEngine] conditional rule '${rule.name}' \`when\` predicate failed ` +
502
+ `to evaluate (${error}) — skipped.`);
503
+ return { valid: true };
504
+ }
505
+ const branch = conditionMet === true ? rule.then : rule.otherwise;
506
+ if (!branch || !isActive(branch)) {
507
+ return { valid: true };
408
508
  }
509
+ return this.validateRule(branch, context, event);
409
510
  }
410
511
  /**
411
- * Validate format/pattern
512
+ * Validate a field's format against `regex` and/or a named `format`.
513
+ *
514
+ * Mirrors the server: the rule only fires when the write TOUCHES the field and
515
+ * the value is non-empty (requiredness and type-shape belong to the
516
+ * field-level validator, so an absent or blank value is not a format
517
+ * violation). Both checks apply when both are declared. A `regex` that will
518
+ * not compile is a BROKEN rule — logged and skipped, never a violation.
412
519
  */
413
520
  validateFormat(rule, context) {
521
+ if (!(rule.field in context.record)) {
522
+ return { valid: true };
523
+ }
414
524
  const value = context.record[rule.field];
415
525
  if (value === null || value === undefined || value === '') {
416
526
  return { valid: true };
417
527
  }
418
- try {
528
+ const str = String(value);
529
+ const violation = {
530
+ valid: false,
531
+ message: rule.message || `Invalid format for ${rule.field}`,
532
+ rule: rule.name,
533
+ severity: rule.severity,
534
+ };
535
+ if (rule.regex) {
419
536
  let pattern;
420
- if (rule.format) {
421
- // Use predefined format
422
- pattern = this.getPredefinedPattern(rule.format);
423
- }
424
- else if (typeof rule.pattern === 'string') {
425
- pattern = new RegExp(rule.pattern, rule.flags);
537
+ try {
538
+ pattern = new RegExp(rule.regex);
426
539
  }
427
- else {
428
- pattern = rule.pattern;
429
- }
430
- if (!pattern.test(String(value))) {
431
- return {
432
- valid: false,
433
- message: rule.message || `Invalid format for ${rule.field}`,
434
- rule: rule.name,
435
- severity: rule.severity,
436
- };
540
+ catch {
541
+ console.warn(`[ObjectValidationEngine] format rule '${rule.name}' has an invalid regex — skipped.`);
542
+ return { valid: true };
437
543
  }
438
- return { valid: true };
544
+ if (!pattern.test(str))
545
+ return violation;
439
546
  }
440
- catch (error) {
441
- return {
442
- valid: false,
443
- message: `Format validation error: ${error}`,
444
- rule: rule.name,
445
- severity: 'error',
446
- };
547
+ if (rule.format && !matchesNamedFormat(rule.format, str)) {
548
+ return violation;
447
549
  }
550
+ return { valid: true };
448
551
  }
449
552
  /**
450
553
  * Validate range constraints
@@ -508,29 +611,59 @@ export class ObjectValidationEngine {
508
611
  };
509
612
  }
510
613
  }
511
- /**
512
- * Get predefined regex pattern
513
- */
514
- getPredefinedPattern(format) {
515
- const patterns = {
516
- email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
517
- url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*)$/,
518
- phone: /^[\d\s\-+()]+$/,
519
- ipv4: /^(\d{1,3}\.){3}\d{1,3}$/,
520
- ipv6: /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i,
521
- uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
522
- iso_date: /^\d{4}-\d{2}-\d{2}$/,
523
- credit_card: /^\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}$/,
524
- };
525
- return patterns[format] || /.*/;
614
+ }
615
+ // Linear-time email check, copied from the server's rule-validator: domain
616
+ // labels exclude '.', so the quantifiers either side of each '.' cannot overlap
617
+ // — avoiding the polynomial backtracking (ReDoS) of the naive
618
+ // `[^\s@]+\.[^\s@]+` shape while still requiring a local part, an '@' and a
619
+ // dotted domain.
620
+ const EMAIL_RE = /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/;
621
+ // Lenient phone matcher: optional leading +, then 7–20 digits with spaces,
622
+ // dashes, dots and parens allowed as separators. Intentionally permissive —
623
+ // strict national formats belong in a `regex`.
624
+ const PHONE_RE = /^\+?[\d\s().-]{7,20}$/;
625
+ /**
626
+ * The spec's four named formats, matched the way the server matches them.
627
+ *
628
+ * An unrecognised name returns `true` (no violation): the client must not reject
629
+ * a value over a format only the server knows how to check.
630
+ */
631
+ function matchesNamedFormat(format, str) {
632
+ switch (format) {
633
+ case 'email':
634
+ return EMAIL_RE.test(str);
635
+ case 'phone':
636
+ return PHONE_RE.test(str);
637
+ case 'url':
638
+ try {
639
+ new URL(str);
640
+ return true;
641
+ }
642
+ catch {
643
+ return false;
644
+ }
645
+ case 'json':
646
+ try {
647
+ JSON.parse(str);
648
+ return true;
649
+ }
650
+ catch {
651
+ return false;
652
+ }
653
+ default:
654
+ return true;
526
655
  }
527
656
  }
528
657
  /**
529
658
  * Default instance
659
+ *
660
+ * @deprecated (#3110) See `ObjectValidationEngine`.
530
661
  */
531
662
  export const defaultObjectValidationEngine = new ObjectValidationEngine();
532
663
  /**
533
664
  * Convenience function to validate a record
665
+ *
666
+ * @deprecated (#3110) See `ObjectValidationEngine`.
534
667
  */
535
668
  export async function validateRecord(rules, context, event = 'insert') {
536
669
  return defaultObjectValidationEngine.validateRecord(rules, context, event);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/core",
3
- "version": "16.1.0",
3
+ "version": "17.2.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "license": "MIT",
@@ -25,11 +25,11 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "@objectstack/formula": "^16.0.0-rc.0",
29
- "@objectstack/spec": "^16.0.0-rc.0",
28
+ "@objectstack/formula": "^17.0.0-rc.1",
29
+ "@objectstack/spec": "^17.0.0-rc.1",
30
30
  "lodash": "^4.18.1",
31
31
  "zod": "^4.4.3",
32
- "@object-ui/types": "16.1.0"
32
+ "@object-ui/types": "17.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "typescript": "^6.0.3",