@inixiative/json-rules 2.21.1 → 2.23.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/README.md CHANGED
@@ -46,7 +46,7 @@ check(rule, { age: 16 }); // "Must be 18 or older"
46
46
  - ordered windowing — first/last `N` with `orderBy` / `take` / `skip` (check-only)
47
47
  - date comparisons with timezone-aware runtime evaluation
48
48
  - relative & calendar date expressions — "last 30 days", "this month" — via `within` and `ago`/`ahead`/`this`/`last`/`next`
49
- - relative value references via `path`
49
+ - relative value references via `path`, and `$$.` scope refs up through nested arrays
50
50
  - custom error messages on every rule
51
51
  - compilation to Prisma and PostgreSQL for supported subsets
52
52
 
@@ -332,22 +332,43 @@ In runtime validation, a plain path is resolved from the root context:
332
332
  }
333
333
  ```
334
334
 
335
- ### Current Array Element Reference
335
+ ### Scope References
336
336
 
337
- Inside array conditions, `$.` means "read from the current element":
337
+ Inside an array operator's `condition` or `filter`, `$.` reads from the current element.
338
+ Each additional `$` reaches one enclosing element further out: `$$.` is the element of the
339
+ enclosing array operator, `$$$.` the one above that, up to the root row. Logical
340
+ combinators (`all` / `any` / `if`) never add a level — only array and aggregate rules do.
341
+
342
+ Both `field` and `path` take the prefix. A bare `field` is always the current element; a
343
+ bare `path` is always the root context (`options.context`, defaulting to the root row).
338
344
 
339
345
  ```ts
340
346
  {
341
347
  field: 'orders',
342
348
  arrayOperator: ArrayOperator.all,
343
349
  condition: {
344
- field: 'total',
345
- operator: Operator.lessThanEquals,
346
- path: '$.maxBudget'
347
- }
350
+ field: 'lineItems',
351
+ arrayOperator: ArrayOperator.all,
352
+ condition: {
353
+ all: [
354
+ // line item qty against its order's cap
355
+ { field: 'qty', operator: Operator.lessThanEquals, path: '$$.maxQty' },
356
+ // order cap against the root row's limit — neither side is the line item
357
+ { field: '$$.maxQty', operator: Operator.lessThanEquals, path: '$$$.orgLimit' },
358
+ ],
359
+ },
360
+ },
348
361
  }
349
362
  ```
350
363
 
364
+ A ref deeper than the nesting (`$$.` at the top level, `$$$.` one array deep) throws in
365
+ `check()`, is a `scope_out_of_bounds` issue from `validateRule`, and a violation from
366
+ `checkRuleAgainstLens`. A reachable ancestor that lacks the named key fails the comparison
367
+ like any absent field.
368
+
369
+ `toSql()` keeps `path: '$.x'` as a same-row column comparison. Every other scope ref — a
370
+ `$$.` path or any prefixed `field` — is check-only; both compilers throw.
371
+
351
372
  ## Rule Introspection
352
373
 
353
374
  Reading a stored rule's own content — which values it names, which bindings it needs — is
@@ -356,9 +377,17 @@ format grows a node type, and it goes blind silently.
356
377
 
357
378
  | Function | Purpose |
358
379
  | --- | --- |
359
- | `requiredBindings(rule)` | Names of every `{ bind }` token in the tree — the set a bindings map must cover. |
380
+ | `requiredBindings(rule)` | Names a bindings map must cover — every `{ bind }` token not marked `bindOptional`. A name optional at one leaf and required at another is required. |
381
+ | `bindingNames(rule)` | Every `{ bind }` name in the tree, optional or not — what a lens declares. |
360
382
  | `resolveBindings(rule, bindings)` | Substitutes covered binds with their values, leaving uncovered tokens in place (partial resolution). |
361
383
 
384
+ A leaf may mark its bind optional: `{ field, operator, bind: 'region', bindOptional: true }`. An
385
+ unsupplied required bind is a caller bug — `check()` throws, and both compilers refuse a
386
+ surviving token. An unsupplied *optional* bind is `null` wherever absence is final: `check()`
387
+ compares against `null`, `toPrisma` / `toSql` compile the token as `null`. The rule is evaluated
388
+ as written — the leaf is never pruned, so `in {{bind}}` with nothing bound matches nothing rather
389
+ than everything.
390
+
362
391
 
363
392
  ## Runtime Validation
364
393
 
@@ -517,6 +546,7 @@ Not every backend supports every rule shape.
517
546
  | `dayIn` / `dayNotIn` | Yes | No | Yes |
518
547
  | Windowing (`orderBy` / `take` / `skip`) | Yes | Extremal (`take:1`, aligned) | No |
519
548
  | `path: '$.field'` current-element / same-row refs | Yes | No | Yes |
549
+ | `$$.` scope refs and `$`-prefixed `field` | Yes | No | No |
520
550
 
521
551
  ### NULL Semantics
522
552
 
@@ -572,7 +602,7 @@ positive operator, ask for them:
572
602
 
573
603
  - `matches` and `notMatches` are not supported by Prisma output
574
604
  - `dayIn` and `dayNotIn` are not supported by Prisma output
575
- - `path: '$.field'` column-to-column comparisons are not supported by Prisma `WHERE`
605
+ - `path: '$.field'` column-to-column comparisons are not supported by Prisma `WHERE`; no scope ref (`$$.` path, prefixed `field`) compiles
576
606
  - count-based and aggregate relation operators require `{ map, model }`
577
607
  - aggregate rules with `notBetween` are not supported by Prisma output
578
608
  - aggregate rules on JSON/native stored arrays are not supported by Prisma — use `toSql()` or `check()` for those