@objectstack/objectql 7.5.0 → 7.6.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/dist/index.mjs CHANGED
@@ -5279,12 +5279,25 @@ function validateRecord(objectSchema, data, mode) {
5279
5279
 
5280
5280
  // src/validation/rule-validator.ts
5281
5281
  import { ExpressionEngine as ExpressionEngine2 } from "@objectstack/formula";
5282
+ import Ajv from "ajv";
5283
+ var ajv = new Ajv({ allErrors: true, strict: false });
5284
+ var jsonSchemaCache = /* @__PURE__ */ new WeakMap();
5282
5285
  function needsPriorRecord(objectSchema) {
5283
5286
  const rules = objectSchema?.validations;
5284
5287
  if (!Array.isArray(rules)) return false;
5285
- return rules.some(
5286
- (r) => r != null && typeof r === "object" && (r.type === "state_machine" || r.type === "cross_field" || r.type === "script")
5287
- );
5288
+ return rules.some((r) => ruleNeedsPrior(r));
5289
+ }
5290
+ function ruleNeedsPrior(r) {
5291
+ if (r == null || typeof r !== "object") return false;
5292
+ const type = r.type;
5293
+ if (type === "state_machine" || type === "cross_field" || type === "script") {
5294
+ return true;
5295
+ }
5296
+ if (type === "conditional") {
5297
+ const c = r;
5298
+ return ruleNeedsPrior(c.then) || ruleNeedsPrior(c.otherwise);
5299
+ }
5300
+ return false;
5288
5301
  }
5289
5302
  function toExpression(cond) {
5290
5303
  return typeof cond === "string" ? { dialect: "cel", source: cond } : cond;
@@ -5294,6 +5307,7 @@ function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
5294
5307
  if (!Array.isArray(rules) || rules.length === 0 || !data) return;
5295
5308
  const previous = opts.previous ?? void 0;
5296
5309
  const merged = { ...previous ?? {}, ...data };
5310
+ const ctx = { data, merged, previous, mode, logger: opts.logger };
5297
5311
  const errors = [];
5298
5312
  const ordered = rules.filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
5299
5313
  const events = r.events ?? ["insert", "update"];
@@ -5302,11 +5316,7 @@ function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
5302
5316
  for (const rule of ordered) {
5303
5317
  let violation = null;
5304
5318
  try {
5305
- if (rule.type === "state_machine") {
5306
- violation = checkStateMachine(rule, mode, data, previous);
5307
- } else if (rule.type === "script" || rule.type === "cross_field") {
5308
- violation = checkPredicate(rule, merged, previous, opts.logger);
5309
- }
5319
+ violation = evaluateRule(rule, ctx);
5310
5320
  } catch (err) {
5311
5321
  opts.logger?.warn?.(`Validation rule '${rule.name}' threw \u2014 skipped`, err);
5312
5322
  continue;
@@ -5323,6 +5333,23 @@ function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
5323
5333
  }
5324
5334
  if (errors.length > 0) throw new ValidationError(errors);
5325
5335
  }
5336
+ function evaluateRule(rule, ctx) {
5337
+ switch (rule.type) {
5338
+ case "state_machine":
5339
+ return checkStateMachine(rule, ctx.mode, ctx.data, ctx.previous);
5340
+ case "script":
5341
+ case "cross_field":
5342
+ return checkPredicate(rule, ctx.merged, ctx.previous, ctx.logger);
5343
+ case "format":
5344
+ return checkFormat(rule, ctx.data, ctx.logger);
5345
+ case "json_schema":
5346
+ return checkJsonSchema(rule, ctx.data, ctx.logger);
5347
+ case "conditional":
5348
+ return checkConditional(rule, ctx);
5349
+ default:
5350
+ return null;
5351
+ }
5352
+ }
5326
5353
  function checkStateMachine(rule, mode, data, previous) {
5327
5354
  if (mode === "insert" || !previous) return null;
5328
5355
  if (!(rule.field in data)) return null;
@@ -5362,6 +5389,99 @@ function checkPredicate(rule, record, previous, logger) {
5362
5389
  }
5363
5390
  return null;
5364
5391
  }
5392
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5393
+ var PHONE_RE2 = /^\+?[\d\s().-]{7,20}$/;
5394
+ function checkFormat(rule, data, logger) {
5395
+ if (!(rule.field in data)) return null;
5396
+ const value = data[rule.field];
5397
+ if (value === null || value === void 0 || value === "") return null;
5398
+ const str = String(value);
5399
+ if (rule.regex) {
5400
+ let re;
5401
+ try {
5402
+ re = new RegExp(rule.regex);
5403
+ } catch {
5404
+ logger?.warn?.(`Validation rule '${rule.name}' has an invalid regex \u2014 skipped`);
5405
+ return null;
5406
+ }
5407
+ if (!re.test(str)) return formatViolation(rule);
5408
+ }
5409
+ if (rule.format && !matchesNamedFormat(rule.format, str)) {
5410
+ return formatViolation(rule);
5411
+ }
5412
+ return null;
5413
+ }
5414
+ function matchesNamedFormat(format, str) {
5415
+ switch (format) {
5416
+ case "email":
5417
+ return EMAIL_RE2.test(str);
5418
+ case "phone":
5419
+ return PHONE_RE2.test(str);
5420
+ case "url":
5421
+ try {
5422
+ new URL(str);
5423
+ return true;
5424
+ } catch {
5425
+ return false;
5426
+ }
5427
+ case "json":
5428
+ try {
5429
+ JSON.parse(str);
5430
+ return true;
5431
+ } catch {
5432
+ return false;
5433
+ }
5434
+ default:
5435
+ return true;
5436
+ }
5437
+ }
5438
+ function formatViolation(rule) {
5439
+ return { field: rule.field, code: "invalid_format", message: rule.message };
5440
+ }
5441
+ function checkJsonSchema(rule, data, logger) {
5442
+ if (!(rule.field in data)) return null;
5443
+ let value = data[rule.field];
5444
+ if (value === null || value === void 0) return null;
5445
+ if (typeof value === "string") {
5446
+ try {
5447
+ value = JSON.parse(value);
5448
+ } catch {
5449
+ return { field: rule.field, code: "invalid_json", message: rule.message };
5450
+ }
5451
+ }
5452
+ let validate = jsonSchemaCache.get(rule.schema);
5453
+ if (!validate) {
5454
+ try {
5455
+ validate = ajv.compile(rule.schema);
5456
+ } catch (err) {
5457
+ logger?.warn?.(
5458
+ `Validation rule '${rule.name}' has an uncompilable JSON Schema \u2014 skipped`,
5459
+ err
5460
+ );
5461
+ return null;
5462
+ }
5463
+ jsonSchemaCache.set(rule.schema, validate);
5464
+ }
5465
+ if (!validate(value)) {
5466
+ return { field: rule.field, code: "json_schema_violation", message: rule.message };
5467
+ }
5468
+ return null;
5469
+ }
5470
+ function checkConditional(rule, ctx) {
5471
+ const result = ExpressionEngine2.evaluate(toExpression(rule.when), {
5472
+ record: ctx.merged,
5473
+ previous: ctx.previous ?? void 0
5474
+ });
5475
+ if (!result.ok) {
5476
+ ctx.logger?.warn?.(
5477
+ `Validation rule '${rule.name}' when-predicate failed to evaluate (${result.error.kind}: ${result.error.message}) \u2014 skipped`
5478
+ );
5479
+ return null;
5480
+ }
5481
+ const branch = result.value === true ? rule.then : rule.otherwise;
5482
+ if (!branch || branch.active === false) return null;
5483
+ return evaluateRule(branch, ctx);
5484
+ }
5365
5485
  function legalNextStates(objectSchema, field, currentState) {
5366
5486
  const rules = objectSchema?.validations;
5367
5487
  if (!Array.isArray(rules)) return null;
@@ -6322,7 +6442,7 @@ var _ObjectQL = class _ObjectQL {
6322
6442
  * **fail-closed** — the write throws rather than persist cleartext.
6323
6443
  *
6324
6444
  * Mirrors the Settings subsystem's ICryptoProvider wiring; the host (e.g.
6325
- * `serve`) injects `InMemoryCryptoProvider` in dev and a KMS/Vault-backed
6445
+ * `serve`) injects `LocalCryptoProvider` in dev and a KMS/Vault-backed
6326
6446
  * provider in production.
6327
6447
  */
6328
6448
  setCryptoProvider(provider) {
@@ -6360,7 +6480,7 @@ var _ObjectQL = class _ObjectQL {
6360
6480
  }
6361
6481
  if (!this.cryptoProvider) {
6362
6482
  throw new Error(
6363
- `Cannot persist secret field "${object}.${field}": no CryptoProvider is registered. Wire one via engine.setCryptoProvider(...) (e.g. InMemoryCryptoProvider in dev, a KMS/Vault provider in production). Refusing to store cleartext (fail-closed).`
6483
+ `Cannot persist secret field "${object}.${field}": no CryptoProvider is registered. Wire one via engine.setCryptoProvider(...) (e.g. LocalCryptoProvider in dev, a KMS/Vault provider in production). Refusing to store cleartext (fail-closed).`
6364
6484
  );
6365
6485
  }
6366
6486
  const plain = typeof value === "string" ? value : JSON.stringify(value);