@inixiative/json-rules 3.1.1 → 3.3.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 +94 -27
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +267 -153
- package/dist/index.d.ts +267 -153
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ check(rule, { age: 16 }); // "Must be 18 or older"
|
|
|
43
43
|
- `if` / `then` / `else`
|
|
44
44
|
- array validation against nested object elements
|
|
45
45
|
- array aggregates — `sum` and `avg` across numeric arrays or relation lists
|
|
46
|
-
- ordered windowing — first/last `N` with `orderBy` / `take` / `skip` (check
|
|
46
|
+
- ordered windowing — first/last `N` with `orderBy` / `take` / `skip` (`check()`; `toPrisma()` compiles the extremal case and a `filter` alone)
|
|
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
49
|
- relative value references via `path`, and `$$.` scope refs up through nested arrays
|
|
@@ -73,7 +73,9 @@ check(rule, { age: 16 }); // "Must be 18 or older"
|
|
|
73
73
|
- `exists`
|
|
74
74
|
- `notExists`
|
|
75
75
|
- `startsWith`
|
|
76
|
+
- `notStartsWith`
|
|
76
77
|
- `endsWith`
|
|
78
|
+
- `notEndsWith`
|
|
77
79
|
|
|
78
80
|
### Array Operators
|
|
79
81
|
|
|
@@ -221,8 +223,9 @@ NULL items are skipped, as SQL's `SUM` / `AVG` skip them.
|
|
|
221
223
|
|
|
222
224
|
A date rule's `value` can be a structured, serializable expression instead of an
|
|
223
225
|
absolute date. Magnitudes are always **positive** — direction lives in the keyword.
|
|
224
|
-
|
|
225
|
-
`hour`, `minute`, `second`.
|
|
226
|
+
A period (`this` / `last` / `next`) names a dayjs unit: `day`, `week`, `isoWeek`, `month`,
|
|
227
|
+
`quarter`, `year`, `hour`, `minute`, `second`. A rolling amount (`ago` / `ahead`) counts
|
|
228
|
+
`years`, `quarters`, `months`, `weeks`, `days`, `hours`, `minutes`, `seconds`.
|
|
226
229
|
|
|
227
230
|
**Point expressions** — pair with `before` / `after` / `onOrBefore` / `onOrAfter` /
|
|
228
231
|
`notBefore` / `notAfter`,
|
|
@@ -313,11 +316,15 @@ grant there under `all`). `orderBy` is a non-empty array of `{ field, dir: 'asc'
|
|
|
313
316
|
> **Compilation.** `toPrisma()` compiles the **extremal** case — `take: 1`, a single
|
|
314
317
|
> `orderBy`, and a monotonic condition on that same field, with the direction aligned so
|
|
315
318
|
> the extremal element is binding (`all` + desc + `before`, `any` + desc + `after`, etc.).
|
|
316
|
-
> It rewrites to
|
|
317
|
-
>
|
|
318
|
-
>
|
|
319
|
-
>
|
|
320
|
-
>
|
|
319
|
+
> It rewrites to relation filters: the rule above, with `completedAt` required, is "no missions,
|
|
320
|
+
> or some and none since the bound" — `{ OR: [{ fanMissions: { none: {} } }, { AND: [{ fanMissions:
|
|
321
|
+
> { some: {} } }, { fanMissions: { none: { completedAt: { gte: <now-30d> } } } }] }] }`. A `filter`
|
|
322
|
+
> alone (no `orderBy` / `take` / `skip`) folds into the rule: `all` through the exact complement
|
|
323
|
+
> of its condition, the rest as `filter AND condition`. Any other windowed rule — `take > 1`,
|
|
324
|
+
> `skip`, multi-key `orderBy`, a different/non-monotonic condition, a misaligned direction, or a
|
|
325
|
+
> `filter` beside an ordered window — throws a clear "unsupported" error. `toSql()` does not
|
|
326
|
+
> compile windowing at all (no relation subqueries in a `WHERE` fragment). Evaluate the
|
|
327
|
+
> unsupported cases in memory with `check()`.
|
|
321
328
|
|
|
322
329
|
## Path Semantics
|
|
323
330
|
|
|
@@ -494,15 +501,18 @@ string literal against a number or Boolean field, stamp the rule's `coerceType`
|
|
|
494
501
|
does it from a lens). The compilers refuse a string literal on a number or Boolean column
|
|
495
502
|
without one.
|
|
496
503
|
|
|
497
|
-
An enum compares against its declared values.
|
|
498
|
-
|
|
504
|
+
An enum compares exactly against its declared values. String, pattern and ordered operators
|
|
505
|
+
don't apply to one, and the compilers refuse them. A case-insensitive equality or membership, or
|
|
506
|
+
one naming a value the enum doesn't declare, compiles to a membership test over the declared
|
|
499
507
|
values `check()` would match (plus the NULL arm for a negation). The field map must list the
|
|
500
508
|
values (prisma-map does).
|
|
501
509
|
|
|
502
510
|
```ts
|
|
503
511
|
// map: { models: { U: { fields: { role: { kind: 'enum', type: 'Role' } } } }, enums: { Role: ['admin', 'member'] } }
|
|
504
|
-
toSql({ field: 'role', operator: Operator.
|
|
512
|
+
toSql({ field: 'role', operator: Operator.equals, value: 'ADMIN', caseInsensitive: true }, { map, model: 'U' });
|
|
505
513
|
// { sql: '"t0"."role"::text = ANY($1)', params: [['admin']], joins: [] }
|
|
514
|
+
toSql({ field: 'role', operator: Operator.startsWith, value: 'adm' }, { map, model: 'U' });
|
|
515
|
+
// throws: 'startsWith' does not apply to the enum 'role'; compare its values with equals / in.
|
|
506
516
|
```
|
|
507
517
|
|
|
508
518
|
### Custom Errors
|
|
@@ -582,6 +592,20 @@ await prisma.user.findMany({ where }); // users whose orders sum to more than 10
|
|
|
582
592
|
A user with no orders sums to 0, as in `check()`: a comparison that holds at 0 selects the
|
|
583
593
|
parents outside the groups where it fails, so childless parents stay in.
|
|
584
594
|
|
|
595
|
+
### Compiling under a lens
|
|
596
|
+
|
|
597
|
+
Pass `lens` instead of `map` / `mapName` / `model` and the rule is gated by the lens
|
|
598
|
+
(`validateRuleInLens`: a rule it refuses throws; a bare value `path` is your `context` on these
|
|
599
|
+
rails, not a column, so it isn't resolved through the lens), narrowed by it, and compiled against
|
|
600
|
+
its base lens. `toSql` takes it the same way. Passing both throws.
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
const plan = toPrisma(rule, { lens: narrowing, now });
|
|
604
|
+
// = toPrisma(narrowRule(rule, narrowing), { map: base, mapName: base.mapName, model: base.model, now }),
|
|
605
|
+
// once validateRuleInLens(rule, narrowing) passes
|
|
606
|
+
const where = await executePrismaPlan(plan, prisma);
|
|
607
|
+
```
|
|
608
|
+
|
|
585
609
|
### Json null checks
|
|
586
610
|
|
|
587
611
|
A Json column holds a DB NULL or a JSON `null`, and a path inside it can be absent — `check()`
|
|
@@ -678,7 +702,7 @@ Not every backend supports every rule shape.
|
|
|
678
702
|
| Date comparisons | Yes | Most | Yes |
|
|
679
703
|
| Date expressions (`ago`/`ahead`/`this`/`last`/`next`/`start`/`end`) + `within` | Yes | Yes | Yes |
|
|
680
704
|
| `dayIn` / `dayNotIn` | Yes | No | Yes |
|
|
681
|
-
| Windowing (`filter` / `orderBy` / `take` / `skip`) | Yes | Extremal (`take:1`, aligned, no `filter`) | No |
|
|
705
|
+
| Windowing (`filter` / `orderBy` / `take` / `skip`) | Yes | Extremal (`take: 1`, aligned, no `filter`), or a `filter` alone | No |
|
|
682
706
|
| `path: '$.field'` current-element / same-row refs | Yes | No | Yes |
|
|
683
707
|
| `offset` and unit amounts — value, bind or context | Yes | Yes | Yes |
|
|
684
708
|
| `offset` and unit amounts — `$.` row refs | Yes | No | Yes (not a date offset's) |
|
|
@@ -693,12 +717,12 @@ compilers carry NULL rows explicitly:
|
|
|
693
717
|
|
|
694
718
|
| Rule | `check()` on `{ col: null }` | `toSql()` | `toPrisma()` (nullable column) |
|
|
695
719
|
| --- | --- | --- | --- |
|
|
696
|
-
| `notEquals 'x'` / `notContains` / `notMatches` / `notBetween` | matches | `(col <> $1 OR col IS NULL)` | `{ OR: [{ col: { not: 'x' } }, { col: { equals: null } }] }` |
|
|
720
|
+
| `notEquals 'x'` / `notContains` / `notMatches` / `notBetween` | matches | `(col <> $1 OR col IS NULL)` | `{ OR: [{ col: { not: 'x' } }, { col: { equals: null } }] }` (`notMatches` has no Prisma form) |
|
|
697
721
|
| `notIn ['x']` | matches | `(col <> ALL($1) OR col IS NULL)` | `{ OR: [{ col: { notIn: ['x'] } }, { col: { equals: null } }] }` |
|
|
698
722
|
| `in ['x', null]` | matches | `(col = ANY($1) OR col IS NULL)` | `{ OR: [{ col: { in: ['x'] } }, { col: { equals: null } }] }` |
|
|
699
723
|
| `notIn ['x', null]` | no match | `(col <> ALL($1) AND col IS NOT NULL)` | `{ AND: [{ col: { notIn: ['x'] } }, { col: { not: null } }] }` |
|
|
700
724
|
| `equals` / `notEquals` with `path: '$.other'` | `null === null` | `IS [NOT] DISTINCT FROM` | — |
|
|
701
|
-
| `exists` / `notExists` | `!= null` / `== null` | `IS NOT NULL` / `IS NULL` | `{ not: null }` / `{ equals: null }` |
|
|
725
|
+
| `exists` / `notExists` | `!= null` / `== null` | `IS NOT NULL` / `IS NULL` | `{ not: null }` / `{ equals: null }`; on a required column, whether its row is there (`{ rel: { is: {} } }` / `NOT`, always / never at the root) |
|
|
702
726
|
|
|
703
727
|
The **absent set** of a path is wider than a NULL leaf: an optional to-one hop can be NULL too,
|
|
704
728
|
and `{ rel: { col: { equals: null } } }` only matches when the relation exists. So every negation
|
|
@@ -757,6 +781,19 @@ positive operator, ask for them:
|
|
|
757
781
|
- `exactly`
|
|
758
782
|
- `toSql()` generates `WHERE` fragments and `LEFT JOIN`s, not complete queries
|
|
759
783
|
|
|
784
|
+
### Where the Rails Differ
|
|
785
|
+
|
|
786
|
+
`check()`, `toSql()` and `toPrisma()` agree on every rule they all compile, except where the
|
|
787
|
+
engines themselves differ:
|
|
788
|
+
|
|
789
|
+
- Case-insensitive comparison follows each engine's case mapping: JavaScript's `toLowerCase` and
|
|
790
|
+
Postgres's `LOWER` under the database collation can differ on letters like `İ`.
|
|
791
|
+
- Ordered string comparisons (`lessThan`, `between` on text) follow each engine's order:
|
|
792
|
+
`check()` compares UTF-16 code units, Postgres the column's collation.
|
|
793
|
+
- An array or aggregate rule on a Json value that isn't an array is a data error. `check()`
|
|
794
|
+
throws on it; SQL can't raise per row, so it reads the value as an empty array — for an
|
|
795
|
+
aggregate and for `empty` / `notEmpty` alike.
|
|
796
|
+
|
|
760
797
|
## TypeScript Types
|
|
761
798
|
|
|
762
799
|
The public rule types are generic over comparison payloads:
|
|
@@ -764,7 +801,7 @@ The public rule types are generic over comparison payloads:
|
|
|
764
801
|
```ts
|
|
765
802
|
type Condition<TRuleValue = RuleValue, TDateValue = DateRuleValue> =
|
|
766
803
|
| Rule<TRuleValue>
|
|
767
|
-
| AggregateRule
|
|
804
|
+
| AggregateRule<TRuleValue, TDateValue>
|
|
768
805
|
| ArrayRule<TRuleValue, TDateValue>
|
|
769
806
|
| DateRule<TDateValue>
|
|
770
807
|
| All<TRuleValue, TDateValue>
|
|
@@ -784,7 +821,9 @@ Rules:
|
|
|
784
821
|
- `Condition`, `StrictCondition`, `Rule`, `AggregateRule`, `AggregateMode`, `ArrayRule`, `DateRule`, `Row`, `CheckData`
|
|
785
822
|
- `GroupByStep`, `WhereStep`, `PrismaStep`, `PrismaWhere`, `StepRef` (a Prisma plan's steps); `ScopeRef`, `ScopedRef`, `ScopeOutOfBounds` (scope refs)
|
|
786
823
|
- `CheckOptions`, `CompileOptions`, `ToPrismaOptions`, `ToSqlOptions`, `ToSqlResult`, `ToPrismaResult`, `ValidateRuleOptions`, `ListBindingsOptions`, `ValidationIssue`, `ValidationResult`
|
|
787
|
-
-
|
|
824
|
+
- rule parts: `All`, `Any`, `IfThenElse`, `RuleValue`, `RuleScalar`, `OrderedRuleValue`, `ValueSourceOf`, `ValueSourceFields`, `NumberOffset`, `Magnitude`, `WindowFields`, `OrderBy`, `SortDir`
|
|
825
|
+
- dates: `DateRuleValue`, `DateInputValue`, `DateInputOrExpr`, `DateExpr`, `RollingExpr`, `PeriodExpr`, `EdgeExpr`, `PeriodUnit`, `RelativeUnits`, `DateOffset`, `DateConfig`, `TimeZoneConfig`, `WeekStart`
|
|
826
|
+
- the strict shapes, which pair each operator with its operand's type: `StrictCondition`, `StrictAll`, `StrictAny`, `StrictIfThenElse`, `StrictRule`, `StrictEqualityRule`, `StrictMembershipRule`, `StrictOrderedComparisonRule`, `StrictRangeRule`, `StrictContainsRule`, `StrictStringBoundaryRule`, `StrictPatternRule`, `StrictPresenceRule`, `StrictDateRule`, `StrictDateComparisonRule`, `StrictDateRangeRule`, `StrictDateDayRule`, `StrictArrayRule`, `StrictArrayPredicateRule`, `StrictArrayCountRule`, `StrictArrayPresenceRule`, `StrictAggregateRule`
|
|
788
827
|
- `engineGlobals`, `EngineGlobalsState`, `PrismaProvider`, `FuzzyConfig`
|
|
789
828
|
|
|
790
829
|
Lens & bridges:
|
|
@@ -793,8 +832,9 @@ Lens & bridges:
|
|
|
793
832
|
- `FieldMap`, `FieldMapEntry`, `ModelEntry`, `SourceOption`, `FieldMapSet`, `Bridge`, `BridgeEndpoint`, `BridgeCardinality`, `BridgeDictionary`
|
|
794
833
|
- `createLens`, `storeLens`, `composeLens`, `StoredLens`, `stitchFieldMaps`, `indexBridges`, `validateFieldMaps`, `assertValidFieldMaps`
|
|
795
834
|
- `validateNarrowing`, `assertValidNarrowing`, `validateRuleInLens`, `narrowRule`, `coerceRule`
|
|
796
|
-
- `bindLens`, `listLensBindings`
|
|
797
|
-
- `projectLens`, `walkLensPath`, `describeRule`, `describeRuleSources`
|
|
835
|
+
- `bindLens`, `listLensBindings`, `getLensRoot`
|
|
836
|
+
- `projectLens`, `walkLensPath`, `readLensValue`, `LensValue`, `describeRule`, `describeRuleSources`
|
|
837
|
+
- `toLensSelect`, `projectRows`, `LensSelect`, `LensRelationSelect`, `LensSelectOptions`, `ProjectRowsOptions`
|
|
798
838
|
- `toSourceQueries`, `materializeSources`, `materializeSourceQuery`
|
|
799
839
|
- `PathProjection`, `ProjectedVisit`, `ProjectLensOptions`, `LensPathHop`, `LensPathResolution`, `RuleDescription`, `RuleSourceDescription`, `SourceQuery`, `SourcePrismaQuery`, `SourceSqlQuery`, `SourceSelect`, `SourceValues`, `SourceRowShape`, `MaterializeSourceQueryOptions`
|
|
800
840
|
|
|
@@ -923,7 +963,7 @@ check(
|
|
|
923
963
|
);
|
|
924
964
|
```
|
|
925
965
|
|
|
926
|
-
`check()` throws if `data` is an array but the rule contains any field-based leaf, or if the rule is a fieldless `ArrayRule` and `data` is not an array. Root-array
|
|
966
|
+
`check()` throws if `data` is an array but the rule contains any field-based leaf, or if the rule is a fieldless `ArrayRule` and `data` is not an array. Root-array rules are `check()`-only: both compilers throw on a fieldless `ArrayRule`.
|
|
927
967
|
|
|
928
968
|
## Lens & Multi-Source Data
|
|
929
969
|
|
|
@@ -1043,7 +1083,9 @@ Composition across chained narrowings is pure intersection. `where` clauses are
|
|
|
1043
1083
|
| `describeRuleSources(rule, lens)` | The values a rule names at each source the lens declares, keyed like `projectLens` (`path` + `field`, with the source's `mapName` / `model`). Resolved through the lens like `walkLensPath`, so `mapDefaults` sources answer wherever their model appears. `dynamic: true` when the set can't be enumerated: a `path` / `bind` leaf, an offset or a read amount that moves the value, a substring / pattern / range / window operator, or an operator the catalog doesn't know — callers fail closed on it. The reverse question for a reference registry ("which rows does this rule name") — join `model` + `values`. |
|
|
1044
1084
|
| `validateRuleInLens(rule, lens)` | Validates a user rule's field paths and enum values against the narrowed lens, path-aware. Returns `{ ok, errors: { path, message, code }[] }` like `validateRule` (codes such as `not_in_lens`, `operator_kind_mismatch`, `invalid_value`, `value_not_allowed`). The security gate. |
|
|
1045
1085
|
| `describeRule(rule, lens)` | `{ sources, bridgesCrossed, supportedTargets, errors }`: the maps a rule reads, whether it crosses a bridge, which of `check` / `toPrisma` / `toSql` can run it (by the rule's shape, as `validateRule` reads it — a compiler may still refuse a field's kind, such as a date rule on a String column on Prisma), and the lens gate's `ValidationIssue`s. For routing and UX; `validateRuleInLens` stays the gate. |
|
|
1086
|
+
| `getLensRoot(lensOrNarrowing)` | The base lens a narrowing chain is rooted at; a lens is its own. Throws on a cyclic chain. |
|
|
1046
1087
|
| `walkLensPath(lens, path)` | Resolves one dotted path through the lens hop by hop: `{ outcome: 'resolved', hops, terminal, jsonSubPath }`, or `hidden` / `missing` / `pastScalar` with the failing `index`. |
|
|
1088
|
+
| `readLensValue(lens, row, path, options?)` | One value off a row, as the lens shows it: `{ ok: true, value }`, or `{ ok: false, reason }` — `hidden` / `missing` / `pastScalar` (the walk `validateRuleInLens` gates a field with), `relation` (the path ends on rows, not a value) or `list` (it crosses a to-many). Each row on the way, the root included, is checked against its visit's grants: one a grant hides, or a missing one, reads `null`. Only own properties are read, into a Json column too. `options` is what each grant is checked with (`now`, `bindings`). For values a template interpolates. |
|
|
1047
1089
|
| `narrowRule(rule, narrowing)` | Composes the user rule with the lens's `where` clauses, injecting each at its anchor in the rule tree. Under an `all`, the grant goes into the rule's window `filter`, which `check()` evaluates and `toPrisma` folds into the rule (`toSql` compiles no relation arrays). Other rules pass to `check` / `toPrisma` / `toSql`. |
|
|
1048
1090
|
| `coerceRule(rule, lens)` | Stamps each field rule with its field's `coerceType` from the lens (`Int`, `Float`, `Decimal`, `BigInt`, `DateTime`, `Boolean`, `String`). Leaves date rules, aggregate comparisons, rules that already carry a `coerceType`, and anything below a Json column alone. |
|
|
1049
1091
|
|
|
@@ -1075,7 +1117,7 @@ const bound = bindLens(narrowing, { tenantId: 't-42' });
|
|
|
1075
1117
|
| Function | Purpose |
|
|
1076
1118
|
| --- | --- |
|
|
1077
1119
|
| `listLensBindings(lensOrNarrowing)` | The bind names the whole narrowing chain requires, sorted. `bindOptional` tokens are left out, and a `parent:name` reference counts as `name`. |
|
|
1078
|
-
| `bindLens(lensOrNarrowing, bindings)` | Returns a new narrowing chain with every covered token replaced by its value; uncovered tokens stay, so binding can happen in stages. `parent:name` draws the value of `name`. A bare lens comes back unchanged. The input is not mutated. |
|
|
1120
|
+
| `bindLens(lensOrNarrowing, bindings)` | Returns a new narrowing chain with every covered token replaced by its value; uncovered tokens stay, so binding can happen in stages. `parent:name` draws the value of `name`. A bare lens comes back unchanged. The result has the input's type (`bindLens<T extends Lens \| LensNarrowing>(…): T`). The input is not mutated. |
|
|
1079
1121
|
|
|
1080
1122
|
A layer may not re-declare a bind name an ancestor declares; it reads the inherited one as
|
|
1081
1123
|
`parent:name`. `validateNarrowing` reports a collision or a dangling `parent:` reference as
|
|
@@ -1125,7 +1167,7 @@ const projection = projectLens(narrowing, { sourceValues: [values] });
|
|
|
1125
1167
|
| --- | --- |
|
|
1126
1168
|
| `toSourceQueries(lensOrNarrowing)` | `SourceQuery[]`, one per sourced field: `{ path, mapName, model, field, label?, groupBy?, composedWhere, prisma, sql }`. `prisma.steps` is present when the where needs `executePrismaPlan`. `sql.sql` is `null` with an `error` when SQL can't express the where. A grouped source drops `distinct`. |
|
|
1127
1169
|
| `materializeSourceQuery(query, rows, { rowShape? })` | One query's fetched rows as `SourceValues`. `rowShape` is `'prisma'` (default: a dotted `label` and each `groupBy` axis come nested) or `'sql'` (they come flat as `__label` / `__group_i`). Options are deduplicated and sorted. |
|
|
1128
|
-
| `materializeSources(lensOrNarrowing, rows, options?)` | `SourceValues[]` for every sourced field, from rows fetched under the lens
|
|
1170
|
+
| `materializeSources(lensOrNarrowing, rows, options?)` | `SourceValues[]` for every sourced field, from rows fetched under the lens (relations inline). Each row at the source's path must pass, through `check()` with `options`, its source `where`, the grants of the visits above it, the guards of the relations it crosses and the values the lens allows; its own visit's `where` is not re-applied, since the rows were fetched under it. A scalar-list field gives one option per element. A `from: 'mapDefaults'` source throws (see below). |
|
|
1129
1171
|
|
|
1130
1172
|
Options never offer a value the lens disallows: `projectLens` drops fetched values outside a
|
|
1131
1173
|
field's allowed set.
|
|
@@ -1164,16 +1206,41 @@ mapDefaults: {
|
|
|
1164
1206
|
|
|
1165
1207
|
`from: 'mapDefaults'` resolves where it sits — `mapDefaults[<this path's map>].models[<this path's
|
|
1166
1208
|
model>].sources[<field>]` — and takes that source's eligibility (tenancy included), label and
|
|
1167
|
-
axes; its own `where`, and child layers, only narrow it.
|
|
1209
|
+
axes; its own `where`, and child layers, only narrow it. A pointer drops the grants the path
|
|
1210
|
+
carries down only in the layer that declares it: every layer before or after it still carries
|
|
1211
|
+
theirs, so a child's pointer can only narrow what its parent gave, and a tenant layer added after a
|
|
1212
|
+
pointer always narrows it. How depends on how it scopes: through `mapDefaults` (the model's own
|
|
1213
|
+
grant or source) the pointer still offers unlinked rows; through a root `where` the grant can only
|
|
1214
|
+
reach the pointer down the path, so it offers just the linked rows — and across a bridge, where no
|
|
1215
|
+
grant crosses, nothing. Scope tenancy through `mapDefaults` to keep a pointer's unlinked rows.
|
|
1168
1216
|
A pointer whose model declares no source fails `validateNarrowing` (`invalid_source`) and throws
|
|
1169
|
-
from `projectLens` (by path) / `toSourceQueries` / `materializeSources
|
|
1170
|
-
'model' })` and `describeRuleSources` read the lens without
|
|
1171
|
-
|
|
1172
|
-
scopes it through `mapDefaults` (the model's own grant or source), not a root `where`. Across a bridge it is how a picker gets options at all: the
|
|
1217
|
+
from `projectLens` (by path) / `toSourceQueries` / `materializeSources`, even where a layer hides
|
|
1218
|
+
its field; `projectLens(…, { by: 'model' })` and `describeRuleSources` read the lens without
|
|
1219
|
+
validating it. Across a bridge it is how a picker gets options at all: the
|
|
1173
1220
|
model source compiles against the far map alone, with that map's own tenancy, where a path source
|
|
1174
|
-
offers nothing. `materializeSources` refuses a pointer — a fetched collection can't hold unlinked
|
|
1221
|
+
offers nothing (until a later layer scopes by a root `where`, as above). `materializeSources` refuses a pointer — a fetched collection can't hold unlinked
|
|
1175
1222
|
rows; query it with `toSourceQueries` and `materializeSourceQuery`.
|
|
1176
1223
|
|
|
1224
|
+
### Fetching Under a Lens
|
|
1225
|
+
|
|
1226
|
+
`toLensSelect` and `projectRows` fetch the rows a lens shows and cut them to it.
|
|
1227
|
+
|
|
1228
|
+
```ts
|
|
1229
|
+
import { check, executePrismaPlan, narrowRule, projectRows, toLensSelect, toPrisma } from '@inixiative/json-rules';
|
|
1230
|
+
|
|
1231
|
+
const where = await executePrismaPlan(toPrisma(true, { lens: narrowing, now }), prisma);
|
|
1232
|
+
const rows = await prisma.user.findMany({ where, ...toLensSelect(narrowing, { now, rules: [rule] }) });
|
|
1233
|
+
const shown = projectRows(narrowing, rows, { now }); // what a viewer may see
|
|
1234
|
+
// To re-test the grants in memory later — never to return to a viewer:
|
|
1235
|
+
const forRecheck = projectRows(narrowing, rows, { keepGrantColumns: true, rules: [rule], now });
|
|
1236
|
+
const holds = forRecheck.filter((row) => check(narrowRule(rule, narrowing), row, { now }) === true);
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
| Function | Purpose |
|
|
1240
|
+
| --- | --- |
|
|
1241
|
+
| `toLensSelect(lensOrNarrowing, options?)` | `{ select }` for `findMany` at the base model. It selects each projected path's visible columns, the relations its declared paths open (a visible relation off them brings its visible columns only), and every column a `where` on the way reads. A to-many relation carries its visit's grants compiled as its `where`, so related rows come pre-narrowed — unless a grant reads that list: a grant reads it whole, as the database does, so it is fetched whole and `projectRows` cuts it. A to-one relation takes no `where` in Prisma, so `projectRows` drops one its grant hides. A relation that shows no column is fetched whole (Prisma can't select nothing). Bridges are skipped. A relation grant that needs a counting step throws. `rules` are the rules the rows will be re-checked with: each relation they read past the declared paths opens as a declared one (its grants applied, its visible columns kept). The rest of `options` is the clock and context for compiling the grants. The root's own grants are the query's `where`: `toPrisma(rule, { lens })`. |
|
|
1242
|
+
| `projectRows(lensOrNarrowing, rows, options?)` | Rows cut to what the lens shows, recursively. Hidden columns and relations are removed. A row a visit's `where` hides is dropped from the root or a list, and a to-one row becomes `null`. `keepGrantColumns: true` keeps the columns those `where`s read, even hidden ones, and a hidden to-one row, or a hidden row of a list a grant reads, as those columns alone, so `check(narrowRule(rule, lens), row)` re-tests the grants as the database does — for a rule passed in `rules` to both calls, or one that reads only the declared paths; that output carries hidden values, so never return it to a viewer. `rules` is as `toLensSelect`'s. The other options (`now`, `bindings`) are what each `where` is checked with. Plain JSON in and out. |
|
|
1243
|
+
|
|
1177
1244
|
### Evaluating Across Bridges
|
|
1178
1245
|
|
|
1179
1246
|
`path:` refs (used for value comparisons) walk via the same dotted-path mechanism as `field:`. Bridge keys (`'salesforce:Contact'`) are just plain object properties, so `path: 'salesforce:Contact.industry'` works in both `field:` (left side) and `path:` (right side) positions.
|