@inixiative/json-rules 3.6.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 +111 -0
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -2
- package/dist/index.d.ts +61 -2
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
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,115 @@ 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 { ArrayOperator, 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
|
+
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 });
|
|
737
|
+
// => { root: '…' | null, relations: {}, mapDefaults: { prisma: { models: { BrandReferenceRequestFanUser: "${brand_reference_request_fan_user.affiliation} = 'advocate'" } } } }
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
- **Operators:** `equals`, `notEquals`, `in`, `notIn`, `exists`, `notExists`, inside `all` / `any`
|
|
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.
|
|
758
|
+
- **Map:** `map` / `mapName` / `model` are required. A value is checked against its column's
|
|
759
|
+
kind (`leafFitViolations`, as `validateNarrowing` checks it) and an undeclared path is refused —
|
|
760
|
+
MySQL casts a mistyped literal instead of erroring, so `score = TRUE` would match 1 where
|
|
761
|
+
`check()` matches nothing. The map also finds a to-one relation's key for its `exists`
|
|
762
|
+
(`columns['<relation>.<key>']`: the related row's key, NULL when there is none), and says which
|
|
763
|
+
values have no form here.
|
|
764
|
+
- **Columns:** `columns` maps a field path to the caller's SQL, used as written; the engine writes
|
|
765
|
+
no joins and no identifiers. A path missing from it throws, naming the path — it never falls back
|
|
766
|
+
to a quoted name, which MySQL's default mode reads as a string literal.
|
|
767
|
+
- **Values** are written inline — there are no placeholders to fill. A string doubles `'`; on
|
|
768
|
+
MySQL it doubles `\` too (MySQL's default mode reads a backslash as an escape;
|
|
769
|
+
`NO_BACKSLASH_ESCAPES` is not supported). `in` is `IN (…)`; an enum compares as written, with no
|
|
770
|
+
cast.
|
|
771
|
+
- **NULL rows:** negations keep them, as on every rail: `notEquals` / `notIn` are ORed with
|
|
772
|
+
`IS NULL`; `in [x, null]` matches NULL, `notIn [x, null]` doesn't.
|
|
773
|
+
- **Collation:** `=` and `IN` compare under the column's collation. check() compares strings
|
|
774
|
+
exactly, so a `_ci` column (MySQL's default) also matches `'Advocate'` for `'advocate'`; compare a
|
|
775
|
+
binary-collated column where that matters.
|
|
776
|
+
|
|
777
|
+
`toLensWhereSql(lens, { columns, tables? })` mirrors the lens: the root path's `where`s under `root` (null
|
|
778
|
+
when it has none), each relation path's own under `relations`, keyed by the path, and each model
|
|
779
|
+
default's (`mapDefaults[map].models[model].where`) under `mapDefaults`, keyed the same way — one
|
|
780
|
+
predicate per model however many paths reach it. Each is the `where` as its layer wrote it: a
|
|
781
|
+
parent layer's clamp on a hop is reported at that hop's path, never folded into what a later
|
|
782
|
+
layer wrote through it. A `where` that reads through a relation the lens hides throws — that
|
|
783
|
+
hop's clamps have no path to report at, and `check()` still carries them. A lens that spans maps
|
|
784
|
+
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,
|
|
785
|
+
`brandReferenceRequestFanUsers.id` below it); a model default by `'<Model>.<field>'`
|
|
786
|
+
(`BrandReferenceRequestFanUser.affiliation`). It does not compile through `toSql`'s `{ lens }`
|
|
787
|
+
path, which would fold the lens's `where`s into the predicate a second time.
|
|
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
|
+
|
|
803
|
+
**Absent, not hidden.** A row a relation's `where` filters out is absent from the SQL join, so a
|
|
804
|
+
negation through that relation holds for it; `check()` under the lens treats the row as hidden.
|
|
805
|
+
The lens is a projection: the difference is accepted and documented.
|
|
806
|
+
|
|
697
807
|
## Backend Support Matrix
|
|
698
808
|
|
|
699
809
|
Not every backend supports every rule shape.
|
|
@@ -845,6 +955,7 @@ Lens & bridges:
|
|
|
845
955
|
- `projectLens`, `walkLensPath`, `readLensValue`, `LensValue`, `describeRule`, `describeRuleSources`
|
|
846
956
|
- `toLensSelect`, `projectRows`, `LensSelect`, `LensRelationSelect`, `LensSelectOptions`, `ProjectRowsOptions`
|
|
847
957
|
- `toSourceQueries`, `materializeSources`, `materializeSourceQuery`
|
|
958
|
+
- `toLensWhereSql`, `LensWhereSql`, `LensWhereSqlOptions`
|
|
848
959
|
- `PathProjection`, `ProjectedVisit`, `ProjectLensOptions`, `LensPathHop`, `LensPathResolution`, `RuleDescription`, `RuleSourceDescription`, `SourceQuery`, `SourcePrismaQuery`, `SourceSqlQuery`, `SourceSelect`, `SourceValues`, `SourceRowShape`, `MaterializeSourceQueryOptions`
|
|
849
960
|
|
|
850
961
|
A lens has three forms, each with its own job:
|