@inixiative/json-rules 3.7.0 → 3.9.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
@@ -703,7 +703,7 @@ engine's `sql.dialect` — `SqlDialect.postgres` by default, `SqlDialect.mysql`
703
703
  are not a second `toSql`: `toSql` and `toPrisma` are unchanged.
704
704
 
705
705
  ```ts
706
- import { engineGlobals, Operator, SqlDialect, toLensWhereSql, toWhereSql } from '@inixiative/json-rules';
706
+ import { ArrayOperator, engineGlobals, Operator, SqlDialect, toLensWhereSql, toWhereSql } from '@inixiative/json-rules';
707
707
 
708
708
  engineGlobals.set('sql.dialect', SqlDialect.mysql);
709
709
 
@@ -713,17 +713,48 @@ toWhereSql(
713
713
  );
714
714
  // => "${fan_user.affiliation} IN ('advocate', 'consultant')"
715
715
 
716
- toLensWhereSql(lens, { columns });
716
+ toWhereSql(
717
+ {
718
+ field: 'affiliations',
719
+ arrayOperator: ArrayOperator.any,
720
+ condition: { field: 'affiliation', operator: Operator.equals, value: 'advocate' },
721
+ },
722
+ {
723
+ columns: {
724
+ uuid: '${fan_users.uuid}',
725
+ 'affiliations.fanUserUuid': 'a.fanUserUuid',
726
+ 'affiliations.affiliation': 'a.affiliation',
727
+ },
728
+ tables: { affiliations: 'FanUserAffiliation AS a' },
729
+ map,
730
+ mapName: 'prisma',
731
+ model: 'FanUser',
732
+ },
733
+ );
734
+ // => "EXISTS (SELECT 1 FROM FanUserAffiliation AS a WHERE a.fanUserUuid = ${fan_users.uuid} AND (a.affiliation = 'advocate'))"
735
+
736
+ toLensWhereSql(lens, { columns, tables });
717
737
  // => { root: '…' | null, relations: {}, mapDefaults: { prisma: { models: { BrandReferenceRequestFanUser: "${brand_reference_request_fan_user.affiliation} = 'advocate'" } } } }
718
738
  ```
719
739
 
720
740
  - **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.
741
+ (and `true` / `false`), and `any` / `all` / `none` / `empty` / `notEmpty` over a to-many relation
742
+ (below). Anything else — another operator, a date rule, a count or windowed array rule, an
743
+ aggregate, `if` / `then` / `else` — throws, naming it. So do binds, `path` (column-to-column),
744
+ offsets, `coerceType`, case-insensitivity, a path across a bridge, a field rule through a to-many
745
+ relation, anything Json (a stored JSON null is a value to SQL and null to `check()`), and a
746
+ DateTime or list *value*. A DateTime's or list's existence compiles: `deletedAt notExists`, a
747
+ lens's soft-delete `where`, is `… IS NULL`. A rule with no `value` throws, as on every rail.
748
+ - **To-many relations:** an array rule on a to-many relation of the model is one subquery on the
749
+ relation's table, keyed to the row: `any` / `notEmpty` is `EXISTS`, `none` / `empty` is
750
+ `NOT EXISTS`, and `all` is `NOT EXISTS` of a row whose condition `IS NOT TRUE` — a NULL result
751
+ fails, as in `check()`. The map gives the keys (`relationKeys`; a composite key ANDs every pair):
752
+ the row's side reads `columns['<key>']`, the related side `columns['<relation>.<key>']`.
753
+ `tables['<relation>']` is the table, written after `FROM` as given, with the alias its columns
754
+ read. The condition compiles as above and reads `columns['<relation>.<field>']`; a to-many
755
+ relation of the related rows nests (`tables['<relation>.<relation>']`). A relation behind
756
+ another one (`profile.affiliations`), a relation missing from `tables`, `atLeast` / `atMost` /
757
+ `exactly`, a window (`filter` / `orderBy` / `take` / `skip`) and a fieldless array rule throw.
727
758
  - **Map:** `map` / `mapName` / `model` are required. A value is checked against its column's
728
759
  kind (`leafFitViolations`, as `validateNarrowing` checks it) and an undeclared path is refused —
729
760
  MySQL casts a mistyped literal instead of erroring, so `score = TRUE` would match 1 where
@@ -743,7 +774,7 @@ toLensWhereSql(lens, { columns });
743
774
  exactly, so a `_ci` column (MySQL's default) also matches `'Advocate'` for `'advocate'`; compare a
744
775
  binary-collated column where that matters.
745
776
 
746
- `toLensWhereSql(lens, { columns })` mirrors the lens: the root path's `where`s under `root` (null
777
+ `toLensWhereSql(lens, { columns, tables? })` mirrors the lens: the root path's `where`s under `root` (null
747
778
  when it has none), each relation path's own under `relations`, keyed by the path, and each model
748
779
  default's (`mapDefaults[map].models[model].where`) under `mapDefaults`, keyed the same way — one
749
780
  predicate per model however many paths reach it. Each is the `where` as its layer wrote it: a
@@ -755,6 +786,20 @@ reports every map's defaults; a caller writing one database reads its own map's.
755
786
  (`BrandReferenceRequestFanUser.affiliation`). It does not compile through `toSql`'s `{ lens }`
756
787
  path, which would fold the lens's `where`s into the predicate a second time.
757
788
 
789
+ An array rule on a to-many relation has no join to put that relation's clamps on, so its subquery
790
+ carries them: the relation path's own `where` and the related model's default, ANDed beside the
791
+ key (outside the `IS NOT TRUE` of an `all`, as `check()` filters the rows before it tests them).
792
+ It carries only the clamps of the layers above the layer that wrote the `where`, which is what
793
+ `check()` reads it through: a layer's clamps narrow the layers after it, never its own conditions.
794
+ A clamp in the same layer or a later one stays out, so a later layer can't widen what a `none` or
795
+ an `empty` admitted. Each clamp reads its own relations through the layers above its own. The
796
+ clamps are still reported at their own path and model. `tables` reads like `columns`: by the
797
+ relation's path from the lens model in a path `where` (`affiliations`), by
798
+ `'<Model>.<relation>'` in a model default (`FanUser.affiliations`). An array rule on a relation
799
+ the lens hides throws, and its condition's fields are checked from the relation, not from the
800
+ model it sits on. A model default compiles to one predicate for every path that reaches its
801
+ model, so it throws when a to-many relation it reads carries different clamps on two of them.
802
+
758
803
  **Absent, not hidden.** A row a relation's `where` filters out is absent from the SQL join, so a
759
804
  negation through that relation holds for it; `check()` under the lens treats the row as hidden.
760
805
  The lens is a projection: the difference is accepted and documented.