@inixiative/json-rules 3.5.4 → 3.7.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
@@ -645,6 +645,7 @@ import { engineGlobals } from '@inixiative/json-rules';
645
645
  engineGlobals.set('string.caseInsensitive', true);
646
646
  engineGlobals.get('string.caseInsensitive'); // true
647
647
  engineGlobals.set('prismaOptions.datasource.provider', 'mysql');
648
+ engineGlobals.set('sql.dialect', SqlDialect.mysql); // what toWhereSql / toLensWhereSql write
648
649
  engineGlobals.reset(); // back to the defaults
649
650
 
650
651
  // A scoped override: merged over the current state for the duration of a synchronous callback.
@@ -694,6 +695,70 @@ as `toPrisma()` takes it. A set without `mapName` throws.
694
695
  toSql(rule, { map: lens, mapName: 'prisma', model: 'Post' });
695
696
  ```
696
697
 
698
+ ## Inline Predicates — `toWhereSql`, `toLensWhereSql`
699
+
700
+ Two small helpers write a condition, or a lens's row filters, as a SQL predicate for a caller
701
+ that names its own columns and writes its own joins (an Omni topic, for one). They write the
702
+ engine's `sql.dialect` — `SqlDialect.postgres` by default, `SqlDialect.mysql` the other — and
703
+ are not a second `toSql`: `toSql` and `toPrisma` are unchanged.
704
+
705
+ ```ts
706
+ import { engineGlobals, Operator, SqlDialect, toLensWhereSql, toWhereSql } from '@inixiative/json-rules';
707
+
708
+ engineGlobals.set('sql.dialect', SqlDialect.mysql);
709
+
710
+ toWhereSql(
711
+ { field: 'affiliation', operator: Operator.in, value: ['advocate', 'consultant'] },
712
+ { columns: { affiliation: '${fan_user.affiliation}' }, map, mapName: 'prisma', model: 'FanUser' },
713
+ );
714
+ // => "${fan_user.affiliation} IN ('advocate', 'consultant')"
715
+
716
+ toLensWhereSql(lens, { columns });
717
+ // => { root: '…' | null, relations: {}, mapDefaults: { prisma: { models: { BrandReferenceRequestFanUser: "${brand_reference_request_fan_user.affiliation} = 'advocate'" } } } }
718
+ ```
719
+
720
+ - **Operators:** `equals`, `notEquals`, `in`, `notIn`, `exists`, `notExists`, inside `all` / `any`
721
+ (and `true` / `false`). Anything else — another operator, a date, array or aggregate rule,
722
+ `if` / `then` / `else` — throws, naming it. So do binds, `path` (column-to-column), offsets,
723
+ `coerceType`, case-insensitivity, a path across a bridge or through a to-many relation, anything
724
+ Json (a stored JSON null is a value to SQL and null to `check()`), and a DateTime or list
725
+ *value*. A DateTime's or list's existence compiles: `deletedAt notExists`, a lens's soft-delete
726
+ `where`, is `… IS NULL`. A rule with no `value` throws, as on every rail.
727
+ - **Map:** `map` / `mapName` / `model` are required. A value is checked against its column's
728
+ kind (`leafFitViolations`, as `validateNarrowing` checks it) and an undeclared path is refused —
729
+ MySQL casts a mistyped literal instead of erroring, so `score = TRUE` would match 1 where
730
+ `check()` matches nothing. The map also finds a to-one relation's key for its `exists`
731
+ (`columns['<relation>.<key>']`: the related row's key, NULL when there is none), and says which
732
+ values have no form here.
733
+ - **Columns:** `columns` maps a field path to the caller's SQL, used as written; the engine writes
734
+ no joins and no identifiers. A path missing from it throws, naming the path — it never falls back
735
+ to a quoted name, which MySQL's default mode reads as a string literal.
736
+ - **Values** are written inline — there are no placeholders to fill. A string doubles `'`; on
737
+ MySQL it doubles `\` too (MySQL's default mode reads a backslash as an escape;
738
+ `NO_BACKSLASH_ESCAPES` is not supported). `in` is `IN (…)`; an enum compares as written, with no
739
+ cast.
740
+ - **NULL rows:** negations keep them, as on every rail: `notEquals` / `notIn` are ORed with
741
+ `IS NULL`; `in [x, null]` matches NULL, `notIn [x, null]` doesn't.
742
+ - **Collation:** `=` and `IN` compare under the column's collation. check() compares strings
743
+ exactly, so a `_ci` column (MySQL's default) also matches `'Advocate'` for `'advocate'`; compare a
744
+ binary-collated column where that matters.
745
+
746
+ `toLensWhereSql(lens, { columns })` mirrors the lens: the root path's `where`s under `root` (null
747
+ when it has none), each relation path's own under `relations`, keyed by the path, and each model
748
+ default's (`mapDefaults[map].models[model].where`) under `mapDefaults`, keyed the same way — one
749
+ predicate per model however many paths reach it. Each is the `where` as its layer wrote it: a
750
+ parent layer's clamp on a hop is reported at that hop's path, never folded into what a later
751
+ layer wrote through it. A `where` that reads through a relation the lens hides throws — that
752
+ hop's clamps have no path to report at, and `check()` still carries them. A lens that spans maps
753
+ reports every map's defaults; a caller writing one database reads its own map's. A path `where` reads `columns` by its path from the lens model (`status` at the root,
754
+ `brandReferenceRequestFanUsers.id` below it); a model default by `'<Model>.<field>'`
755
+ (`BrandReferenceRequestFanUser.affiliation`). It does not compile through `toSql`'s `{ lens }`
756
+ path, which would fold the lens's `where`s into the predicate a second time.
757
+
758
+ **Absent, not hidden.** A row a relation's `where` filters out is absent from the SQL join, so a
759
+ negation through that relation holds for it; `check()` under the lens treats the row as hidden.
760
+ The lens is a projection: the difference is accepted and documented.
761
+
697
762
  ## Backend Support Matrix
698
763
 
699
764
  Not every backend supports every rule shape.
@@ -845,6 +910,7 @@ Lens & bridges:
845
910
  - `projectLens`, `walkLensPath`, `readLensValue`, `LensValue`, `describeRule`, `describeRuleSources`
846
911
  - `toLensSelect`, `projectRows`, `LensSelect`, `LensRelationSelect`, `LensSelectOptions`, `ProjectRowsOptions`
847
912
  - `toSourceQueries`, `materializeSources`, `materializeSourceQuery`
913
+ - `toLensWhereSql`, `LensWhereSql`, `LensWhereSqlOptions`
848
914
  - `PathProjection`, `ProjectedVisit`, `ProjectLensOptions`, `LensPathHop`, `LensPathResolution`, `RuleDescription`, `RuleSourceDescription`, `SourceQuery`, `SourcePrismaQuery`, `SourceSqlQuery`, `SourceSelect`, `SourceValues`, `SourceRowShape`, `MaterializeSourceQueryOptions`
849
915
 
850
916
  A lens has three forms, each with its own job:
@@ -875,7 +941,7 @@ const lens = composeLens('delegate-7', { user, 'org-acme': orgAcme, 'delegate-7'
875
941
  ### Operator Catalog
876
942
 
877
943
  What a rule builder can offer for a field. The catalog's constants are `FieldKind`,
878
- `RuleTarget`, `ValueShape`, `NUMERIC_KINDS` and `ALL_KINDS`; `getValueShape(operator, family)` takes an
944
+ `RuleTarget`, `ValueShape`, `ConditionShape`, `NUMERIC_KINDS` and `ALL_KINDS`; `getValueShape(operator, family)` takes an
879
945
  `OperatorFamily` (`'field' | 'date' | 'array'`).
880
946
 
881
947
  ```ts
@@ -902,6 +968,7 @@ getValueShape('between', 'date'); // 'dateRange'
902
968
  | `getArrayOperators(target?)` | The array operators, narrowed to one target when given. |
903
969
  | `getAggregateOperators()` | The comparisons an aggregate rule takes. Every target compiles all of them. |
904
970
  | `getValueShape(operator, family)` | The operand an operator takes (`'scalar'`, `'range'`, `'dayList'`, …). `family` is `'field'`, `'date'` or `'array'`, since `between` is both a field and a date operator. Throws on an operator the family doesn't have. |
971
+ | `getConditionShapeKeys(shape)` | `{ required, optional }`: the keys a node of a `ConditionShape` (`'all'`, `'any'`, `'if'`, `'field'`, `'aggregate'`, `'array'`, `'date'`) must have and the keys it may. A literal `true` / `false` is a condition too. |
905
972
 
906
973
  To ask whether a whole rule runs on a target (windows, scope refs and operators together), use
907
974
  `validateRule(rule, { target })`.