@inixiative/json-rules 3.3.1 → 3.4.1
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 +170 -44
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +173 -95
- package/dist/index.d.ts +173 -95
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -283,7 +283,7 @@ toSql(rule, { now });
|
|
|
283
283
|
| Option | Default | Governs |
|
|
284
284
|
| --- | --- | --- |
|
|
285
285
|
| `now` | — (required when a relative/period expression is present) | the anchor instant |
|
|
286
|
-
| `timeZone` | `'UTC'` | how `now` and period boundaries localize — a zone name, or a
|
|
286
|
+
| `timeZone` | `'UTC'` | how `now` and period boundaries localize — a zone name, or a `{ bind }` read from the bindings |
|
|
287
287
|
| `weekStart` | `'monday'` (ISO / isoWeek) | start of `week` for `this`/`last`/`next` |
|
|
288
288
|
|
|
289
289
|
Compilers resolve expressions to concrete `Date` bounds at compile time, so
|
|
@@ -328,11 +328,13 @@ grant there under `all`). `orderBy` is a non-empty array of `{ field, dir: 'asc'
|
|
|
328
328
|
|
|
329
329
|
## Path Semantics
|
|
330
330
|
|
|
331
|
-
`path` lets a rule
|
|
331
|
+
`path` lets a rule compare a field with another value on the row; caller-supplied values come
|
|
332
|
+
through `{ bind }` (`check(rule, row, { bindings })`; `bindRule` before compiling).
|
|
332
333
|
|
|
333
|
-
### Root
|
|
334
|
+
### Root Row Reference
|
|
334
335
|
|
|
335
|
-
|
|
336
|
+
A bare path reads the root row — the record `check()` evaluates, the table a compiler compiles
|
|
337
|
+
against:
|
|
336
338
|
|
|
337
339
|
```ts
|
|
338
340
|
{
|
|
@@ -350,7 +352,7 @@ enclosing array operator, `$$$.` the one above that, up to the root row. Logical
|
|
|
350
352
|
combinators (`all` / `any` / `if`) never add a level — only array and aggregate rules do.
|
|
351
353
|
|
|
352
354
|
Both `field` and `path` take the prefix. A bare `field` is always the current element; a
|
|
353
|
-
bare `path` is always the root
|
|
355
|
+
bare `path` is always the root row.
|
|
354
356
|
|
|
355
357
|
```ts
|
|
356
358
|
{
|
|
@@ -376,13 +378,20 @@ A ref deeper than the nesting (`$$.` at the top level, `$$$.` one array deep) th
|
|
|
376
378
|
`validateRuleInLens`. A reachable ancestor that lacks the named key fails the comparison
|
|
377
379
|
like any absent field.
|
|
378
380
|
|
|
379
|
-
`toSql()`
|
|
380
|
-
|
|
381
|
+
`toSql()` compiles a bare path or `path: '$.x'` as a same-row column comparison (equality,
|
|
382
|
+
ordered and set-free operators; a substring, pattern or set operator against a column throws).
|
|
383
|
+
`toPrisma()` compiles one only as a Prisma field reference: both columns of the same model at the
|
|
384
|
+
same visit (a bare path at the root, `$.` inside a relation filter), of exactly the same type, with
|
|
385
|
+
`equals` / `notEquals` / `lessThan` / `lessThanEquals` / `greaterThan` / `greaterThanEquals` and
|
|
386
|
+
no offset. The plan carries a `{ __field }` sentinel that `executePrismaPlan` resolves to
|
|
387
|
+
`prisma.<model>.fields.<column>`; read a plan's where only through it. Anything else throws, and
|
|
388
|
+
`validateRule(rule, { target: 'toPrisma', map, model })` / `describeRule` report it first. Every
|
|
389
|
+
other scope ref — a `$$.` path or any prefixed `field` — is check-only; both compilers throw.
|
|
381
390
|
|
|
382
391
|
### Offsets and Unit Amounts
|
|
383
392
|
|
|
384
393
|
An `offset` moves the comparison value. It is a value source of its own, with the comparison
|
|
385
|
-
value's contract: `{ value }`, `{ path }` (`$.` from the
|
|
394
|
+
value's contract: `{ value }`, `{ path }` (`$.` from the element, bare from the root row) or `{ bind }`
|
|
386
395
|
(with `bindOptional`). A field rule's offset reads a number, added to the comparison value; a
|
|
387
396
|
date rule's reads a rolling shift (`{ ago }` / `{ ahead }`) anchored on the comparison value
|
|
388
397
|
instead of `now`:
|
|
@@ -410,7 +419,7 @@ instead of `now`:
|
|
|
410
419
|
`{ ago: … }`) is check-only; to size a shift from the row, read the amount instead.
|
|
411
420
|
|
|
412
421
|
Any relative-date unit — in a `value` expression or an offset's rolling shift — is a number or a
|
|
413
|
-
value source: `{ path }` from the row
|
|
422
|
+
value source: `{ path }` from the row, `{ bind }`, or `{ value }`. A relative
|
|
414
423
|
window can take its size from the row it judges:
|
|
415
424
|
|
|
416
425
|
```ts
|
|
@@ -431,9 +440,9 @@ in double precision on every rail.
|
|
|
431
440
|
|
|
432
441
|
| | `check()` | `toSql()` | `toPrisma()` |
|
|
433
442
|
| --- | --- | --- | --- |
|
|
434
|
-
| literal
|
|
435
|
-
| `$.` numeric offset or unit amount | yes | `col + n` / `col ± make_interval(…)` | throws |
|
|
436
|
-
| `$.` date offset (a stored `{ ago }`) | yes | throws | throws |
|
|
443
|
+
| literal or bound offset / amount | yes | resolved to a parameter | resolved to a value |
|
|
444
|
+
| row (`$.` or bare) numeric offset or unit amount | yes | `col + n` / `col ± make_interval(…)` | throws |
|
|
445
|
+
| row (`$.` or bare) date offset (a stored `{ ago }`) | yes | throws | throws |
|
|
437
446
|
| `$$.` anything | yes | throws | throws |
|
|
438
447
|
|
|
439
448
|
`validateRuleInLens` gates offset and magnitude refs like `path` (they must resolve through
|
|
@@ -595,14 +604,14 @@ parents outside the groups where it fails, so childless parents stay in.
|
|
|
595
604
|
### Compiling under a lens
|
|
596
605
|
|
|
597
606
|
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
|
|
599
|
-
|
|
600
|
-
|
|
607
|
+
(`validateRuleInLens`: a rule it refuses throws; a bare value `path` is a root-row column, gated
|
|
608
|
+
like a field), narrowed by it, and compiled against its base lens. `toSql` takes it the same way.
|
|
609
|
+
Passing both throws.
|
|
601
610
|
|
|
602
611
|
```ts
|
|
603
612
|
const plan = toPrisma(rule, { lens: narrowing, now });
|
|
604
|
-
// Gated by the lens, narrowed by it
|
|
605
|
-
//
|
|
613
|
+
// Gated by the lens, narrowed by it (a bare `path` too, as a root-row column), then compiled
|
|
614
|
+
// against the base lens's map and model.
|
|
606
615
|
const where = await executePrismaPlan(plan, prisma);
|
|
607
616
|
```
|
|
608
617
|
|
|
@@ -703,9 +712,9 @@ Not every backend supports every rule shape.
|
|
|
703
712
|
| Date expressions (`ago`/`ahead`/`this`/`last`/`next`/`start`/`end`) + `within` | Yes | Yes | Yes |
|
|
704
713
|
| `dayIn` / `dayNotIn` | Yes | No | Yes |
|
|
705
714
|
| Windowing (`filter` / `orderBy` / `take` / `skip`) | Yes | Extremal (`take: 1`, aligned, no `filter`), or a `filter` alone | No |
|
|
706
|
-
| `path
|
|
707
|
-
| `offset` and unit amounts — value
|
|
708
|
-
| `offset` and unit amounts —
|
|
715
|
+
| `path` — a bare root-row column, or `$.` the current element's | Yes (reads the row) | Same model, same visit, exactly the same type, with `equals` / `notEquals` / `lessThan(Equals)` / `greaterThan(Equals)` — a Prisma field reference, resolved by `executePrismaPlan`; anything else throws | Yes (a column; not in a relation filter, not a substring or set operator) |
|
|
716
|
+
| `offset` and unit amounts — value or bind | Yes | Yes | Yes |
|
|
717
|
+
| `offset` and unit amounts — row refs (`$.` or bare) | Yes | No | Yes (not a date offset's) |
|
|
709
718
|
| `$$.` scope refs and `$`-prefixed `field` | Yes | No | No |
|
|
710
719
|
|
|
711
720
|
### NULL Semantics
|
|
@@ -721,7 +730,7 @@ compilers carry NULL rows explicitly:
|
|
|
721
730
|
| `notIn ['x']` | matches | `(col <> ALL($1) OR col IS NULL)` | `{ OR: [{ col: { notIn: ['x'] } }, { col: { equals: null } }] }` |
|
|
722
731
|
| `in ['x', null]` | matches | `(col = ANY($1) OR col IS NULL)` | `{ OR: [{ col: { in: ['x'] } }, { col: { equals: null } }] }` |
|
|
723
732
|
| `notIn ['x', null]` | no match | `(col <> ALL($1) AND col IS NOT NULL)` | `{ AND: [{ col: { notIn: ['x'] } }, { col: { not: null } }] }` |
|
|
724
|
-
| `equals` / `notEquals` with `path
|
|
733
|
+
| `equals` / `notEquals` with a column `path` | `null === null` | `IS [NOT] DISTINCT FROM` | `{ col: { equals: fields.other } }` plus the NULL arms of `IS [NOT] DISTINCT FROM` |
|
|
725
734
|
| `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) |
|
|
726
735
|
|
|
727
736
|
The **absent set** of a path is wider than a NULL leaf: an optional to-one hop can be NULL too,
|
|
@@ -852,8 +861,10 @@ A lens has three forms, each with its own job:
|
|
|
852
861
|
- `projectLens(lens)` returns `Record<dottedPath, ProjectedVisit>` for per-path checks where
|
|
853
862
|
sibling paths to the same model diverge.
|
|
854
863
|
- `projectLens(lens, { by: 'model' })` returns the leak-safe total surface *as a Lens* — every
|
|
855
|
-
|
|
856
|
-
the server→client builder surface; it never exposes the raw lens.
|
|
864
|
+
model the relations turned on reach, with the full narrowing applied, unioned per model, `where`
|
|
865
|
+
stripped. Use it as the server→client builder surface; it never exposes the raw lens. It is a
|
|
866
|
+
view, not a gate: as a lens it is bare and turns no relation on, so gate rules against the
|
|
867
|
+
narrowing it came from.
|
|
857
868
|
|
|
858
869
|
```ts
|
|
859
870
|
const records = storeLens(grantLens, ['user', 'org-acme', 'grant-7']); // persist each record
|
|
@@ -940,6 +951,26 @@ if (!result.ok) {
|
|
|
940
951
|
assertValidRule(rule, { target: 'toPrisma' });
|
|
941
952
|
```
|
|
942
953
|
|
|
954
|
+
Two error classes say whose input went wrong; each sets `name`, so check with `instanceof` or
|
|
955
|
+
`error.name`:
|
|
956
|
+
|
|
957
|
+
| Class | Thrown when | Fix |
|
|
958
|
+
| --- | --- | --- |
|
|
959
|
+
| `UsageError` | The caller's input is missing or malformed: no `now` for a relative date, an invalid `now` or time zone, a bind never bound (`bindRule` / `bindLens` before compiling; `bindings` for `check`). | Supply the input. |
|
|
960
|
+
| `LensRefusal` (`code`) | The lens itself can't do it: a later layer's grant reads what its parent hides, a grant or source a compile has no form for, a source path that can't carry its link. | Fix the lens — `validateNarrowing` reports the same refusal as an issue. |
|
|
961
|
+
|
|
962
|
+
```ts
|
|
963
|
+
import { LensRefusal, UsageError } from '@inixiative/json-rules';
|
|
964
|
+
|
|
965
|
+
try {
|
|
966
|
+
toPrisma(rule, { lens, now });
|
|
967
|
+
} catch (error) {
|
|
968
|
+
if (error instanceof UsageError) /* the caller's input */;
|
|
969
|
+
else if (error instanceof LensRefusal) /* the lens: error.code */;
|
|
970
|
+
else throw error; // the rule's shape — validateRule reports it
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
943
974
|
## Root-Array Rules in `check()`
|
|
944
975
|
|
|
945
976
|
When `data` is an array, the rule must be a tree of `all` / `any` whose leaves are **fieldless** `ArrayRule`s (no `field`, `arrayOperator` operates on the array itself).
|
|
@@ -1048,6 +1079,50 @@ const lens = createLens({
|
|
|
1048
1079
|
|
|
1049
1080
|
The lens is **schema only** — no data lives on it. Runtime data (rows, foreign tables, FE picker sources) is passed alongside, separately, when you need it.
|
|
1050
1081
|
|
|
1082
|
+
### Relations
|
|
1083
|
+
|
|
1084
|
+
Relations are fields, off by default. A bare lens reads its anchor model's columns and nothing
|
|
1085
|
+
else. The first narrowing over the base lens turns a relation on through the relation object —
|
|
1086
|
+
never through `picks`, which names columns only:
|
|
1087
|
+
|
|
1088
|
+
```ts
|
|
1089
|
+
const narrowing: LensNarrowing = {
|
|
1090
|
+
parent: lens,
|
|
1091
|
+
root: { relations: { org: { relations: { parent: {} } } } }, // along the path
|
|
1092
|
+
mapDefaults: {
|
|
1093
|
+
prisma: { models: { Org: { relations: { users: {} } } } }, // wherever Org is visited
|
|
1094
|
+
},
|
|
1095
|
+
};
|
|
1096
|
+
validateRuleInLens({ field: 'org.parent.name', operator: Operator.equals, value: 'Acme' }, narrowing); // ok
|
|
1097
|
+
validateRuleInLens({ field: 'posts', arrayOperator: ArrayOperator.any, condition: true }, narrowing);
|
|
1098
|
+
// { ok: false, errors: [{ path: 'posts', code: 'not_in_lens', message: "'posts' is a relation the lens does not turn on …" }] }
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
- **Off is hidden.** A relation that isn't turned on can't be crossed or named (`exists` /
|
|
1102
|
+
`notExists` included): the gate refuses it (`not_in_lens`), `walkLensPath` and `readLensValue`
|
|
1103
|
+
report it `hidden`, `projectLens` doesn't list it, and `toLensSelect` / `projectRows` don't fetch
|
|
1104
|
+
or keep it. That covers every relation a rule, a value `path` or `$` ref, an offset, an
|
|
1105
|
+
`orderBy`, a source `label` / `groupBy`, or a read crosses. A bridge is turned on by its key
|
|
1106
|
+
(`'salesforce:Contact'`).
|
|
1107
|
+
- **The relation object carries that hop's narrowing.** `relations.org = { where, picks, omits,
|
|
1108
|
+
relations }` narrows Org at that hop; on a model default it narrows the hop wherever the model is
|
|
1109
|
+
visited, and may nest further relations.
|
|
1110
|
+
- **Only the first narrowing turns relations on; later layers narrow.** A later layer may omit a
|
|
1111
|
+
relation (`omits: ['org']`, which `picks` never conflicts with), or restate one its parent shows
|
|
1112
|
+
to add that hop's narrowing — a restatement hides nothing else. Naming a relation its parent
|
|
1113
|
+
doesn't show fails `validateNarrowing` (`not_visible`), and does nothing at runtime.
|
|
1114
|
+
- **The model defaults grow a tree.** From the anchor and every spelled path, model-default
|
|
1115
|
+
turn-ons are followed breadth-first, each model once, at its nearest reach (ties: the earlier
|
|
1116
|
+
parent, then the relation declared first). Anything else is reached by spelling it under
|
|
1117
|
+
`root.relations`; a spelled node grows its own tree. Every posture walks the same tree, so they
|
|
1118
|
+
agree and stay small. `lensVisit(lens, 'org.users')` resolves one visit on demand, as
|
|
1119
|
+
`projectLens` would give it, or `null`.
|
|
1120
|
+
- **Grants.** The first narrowing's `where`s (and source eligibility `where`s) may read any
|
|
1121
|
+
relation on the schema; a later layer's may read only what its parent shows — a delegate can't
|
|
1122
|
+
probe a relation it can't see (`validateNarrowing` reports it; every posture throws). A bare
|
|
1123
|
+
value `path` reads the root row, so only `root.where` may hold one. `toLensSelect` fetches exactly the columns grants read, and
|
|
1124
|
+
`projectRows({ keepGrantColumns: true })` keeps them for a re-check.
|
|
1125
|
+
|
|
1051
1126
|
### LensNarrowing & `where`
|
|
1052
1127
|
|
|
1053
1128
|
`LensNarrowing` is a recursive tree that narrows a parent `Lens` (or another `LensNarrowing`). Each narrowing can add schema picks/omits per model, per-field enum picks/omits, and `where` clauses for data scope:
|
|
@@ -1058,7 +1133,7 @@ const narrowing: LensNarrowing = {
|
|
|
1058
1133
|
root: {
|
|
1059
1134
|
// path-specific narrowing at the lens anchor (FanUser)
|
|
1060
1135
|
picks: ['email', 'firstName', 'crmId'],
|
|
1061
|
-
where: { field: 'tenantId', operator: Operator.equals,
|
|
1136
|
+
where: { field: 'tenantId', operator: Operator.equals, bind: 'tenantId' },
|
|
1062
1137
|
},
|
|
1063
1138
|
mapDefaults: {
|
|
1064
1139
|
prisma: {
|
|
@@ -1071,20 +1146,21 @@ const narrowing: LensNarrowing = {
|
|
|
1071
1146
|
};
|
|
1072
1147
|
```
|
|
1073
1148
|
|
|
1074
|
-
Composition across chained narrowings is pure intersection. `where` clauses are anchored to the model they describe — `root.where` ANDs at the lens anchor, `mapDefaults[X].models[Y].where` injects at every visit of Y in map X, and `root.relations[R]...where` injects when the rule descends through R. Under the `all` array operator the grant goes into the rule's window `filter`, so out-of-scope rows are dropped before the user's "every row matches" check; `check()` and `toPrisma` run it. See [docs/LENS.md](./docs/LENS.md) for the full anchor semantics.
|
|
1149
|
+
Composition across chained narrowings is pure intersection: relations are turned on by the first narrowing and only hidden after it. `where` clauses are anchored to the model they describe — `root.where` ANDs at the lens anchor, `mapDefaults[X].models[Y].where` injects at every visit of Y in map X, and `root.relations[R]...where` injects when the rule descends through R. Under the `all` array operator the grant goes into the rule's window `filter`, so out-of-scope rows are dropped before the user's "every row matches" check; `check()` and `toPrisma` run it. See [docs/LENS.md](./docs/LENS.md) for the full anchor semantics.
|
|
1075
1150
|
|
|
1076
1151
|
### Lens Utilities
|
|
1077
1152
|
|
|
1078
1153
|
| Function | Purpose |
|
|
1079
1154
|
| --- | --- |
|
|
1080
|
-
| `validateNarrowing(narrowing)` | Returns `{ ok, errors: { path, message, code }[] }` for structural or chain problems, one code each: `not_in_lens`, `not_visible` (an item an ancestor hid), `conflicting_selection
|
|
1155
|
+
| `validateNarrowing(narrowing)` | Returns `{ ok, errors: { path, message, code }[] }` for structural or chain problems, one code each: `not_in_lens`, `not_visible` (an item an ancestor hid, or a relation the parent doesn't show), `conflicting_selection` (picks beside an omitted column), `wrong_kind` (a relation named in `picks`, among others), `value_not_allowed`, `invalid_source` (a source label / axis crossing a relation that is off, among others), `invalid_binding`, plus the lens gate's codes for a `where`. `assertValidNarrowing` throws instead. Call at narrowing construction. |
|
|
1081
1156
|
| `assertValidFieldMaps(set)` | Throws when a field name in any map holds `.` or `:` (a path step and a bridge marker). `validateFieldMaps(set)` returns `{ ok, errors }` with code `invalid_field_name`. To check a single map, wrap it: `assertValidFieldMaps({ maps: { prisma: map } })`. |
|
|
1082
|
-
| `projectLens(lens)` | Returns `Record<dottedPath, ProjectedVisit>` — each
|
|
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
|
|
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. |
|
|
1157
|
+
| `projectLens(lens)` | Returns `Record<dottedPath, ProjectedVisit>` — each path the relations turned on reach keys its own resolved narrowing (path picks/omits/enums chain-intersected ∩ `mapDefaults` for the target model); a relation field appears only where it is on. Sibling paths to the same model stay independent. Use for SDK-contract / OpenAPI emission, search-field enumeration, validation whitelists. See [docs/LENS.md §10](./docs/LENS.md). |
|
|
1158
|
+
| `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 a relation turned on reaches their model. `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`. |
|
|
1159
|
+
| `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`). Every relation a field, a value ref, an offset or an `orderBy` / aggregate field crosses must be turned on. A bare value `path` is a root-row column, gated like a field. The security gate. |
|
|
1085
1160
|
| `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
1161
|
| `getLensRoot(lensOrNarrowing)` | The base lens a narrowing chain is rooted at; a lens is its own. Throws on a cyclic chain. |
|
|
1087
|
-
| `
|
|
1162
|
+
| `lensVisit(lens, relationPath, options?)` | One visit as `projectLens` (by path) gives it — its shown fields with values and options, sources, labels and axes — at a dotted relation path from the anchor (`''` for the anchor), resolved on demand: nothing is enumerated, so it is cheap on any schema. `null` when a relation on the path isn't shown there (off, omitted, or outside the model-default tree). A builder walks a lens with it instead of re-deriving the lens's rules. |
|
|
1163
|
+
| `walkLensPath(lens, path)` | Resolves one dotted path through the lens hop by hop: `{ outcome: 'resolved', hops, terminal, jsonSubPath }`, or `hidden` (a column it doesn't keep, or a relation it doesn't turn on there) / `missing` / `pastScalar` with the failing `index`. |
|
|
1088
1164
|
| `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. |
|
|
1089
1165
|
| `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`. |
|
|
1090
1166
|
| `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. |
|
|
@@ -1149,9 +1225,10 @@ const narrowing: LensNarrowing = {
|
|
|
1149
1225
|
// Against a database: one DISTINCT query per sourced field, in Prisma and SQL form.
|
|
1150
1226
|
const [query] = toSourceQueries(narrowing);
|
|
1151
1227
|
// query.path => 'User', query.field => 'region'
|
|
1152
|
-
// query.composedWhere => the node's `where` AND the source's eligibility
|
|
1228
|
+
// query.composedWhere => the node's `where` AND the source's eligibility, narrowed as a rule is
|
|
1153
1229
|
// query.prisma => { model: 'User', distinct: ['region'], select: { region: true }, where: { AND: [...] } }
|
|
1154
1230
|
// query.sql => { sql: 'SELECT DISTINCT "t0"."region" FROM "User" AS "t0" WHERE (...)', params: ['t-42', true] }
|
|
1231
|
+
// A query with `recheck` (a source across a bridge) returns candidates; see "Sources across a bridge".
|
|
1155
1232
|
const { distinct, select, where } = query.prisma;
|
|
1156
1233
|
const rows = await prisma.user.findMany({ distinct, select, where });
|
|
1157
1234
|
const values = materializeSourceQuery(query, rows); // { path, mapName, model, field, options: [{ value }] }
|
|
@@ -1165,17 +1242,24 @@ const projection = projectLens(narrowing, { sourceValues: [values] });
|
|
|
1165
1242
|
|
|
1166
1243
|
| Function | Purpose |
|
|
1167
1244
|
| --- | --- |
|
|
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
|
|
1169
|
-
| `materializeSourceQuery(query, rows, { rowShape
|
|
1170
|
-
| `materializeSources(lensOrNarrowing, rows, options?)` | `SourceValues[]` for every sourced field, from rows fetched
|
|
1245
|
+
| `toSourceQueries(lensOrNarrowing, options?)` | `SourceQuery[]`, one per sourced field: `{ path, mapName, model, field, label?, groupBy?, composedWhere, prisma, sql, recheck? }`. `options` is the clock (`now`, `timeZone`, `weekStart`) a relative date in the where compiles with — required for one, a plain usage error without it; bind a lens's binds with `bindLens` first. `prisma.steps` is present when the where needs `executePrismaPlan`. `sql.sql` is `null` with an `error` when SQL can't express the where. A source that reads across a bridge gets an over-fetching query and a `recheck`: its rows are candidates, not options (see "Sources across a bridge"). `distinct` is the value and a sibling label column; a grouped source or a dotted label drops it, so every label comes back and the least one is picked. |
|
|
1246
|
+
| `materializeSourceQuery(query, rows, { rowShape?, lens?, now?, … })` | 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. A query with `recheck` needs `lens` and rows holding the far side: it re-checks each candidate (`check`, with the clock and bindings given) and reads a bridged label or axis from the far side; a missing far side is a `UsageError`. |
|
|
1247
|
+
| `materializeSources(lensOrNarrowing, rows, options?)` | `SourceValues[]` for every sourced field, from the rows the lens fetches — `toLensSelect`'s rows as fetched, or as `projectRows(…, { keepGrantColumns: true })` keeps them. A viewer's projection drops what sources read: a row lacking any key a read walks — through each relation and list element to the column — that a source or a grant on its path reads throws a `UsageError` (a fetch returns every key it selects, NULL as `null`). The path is walked down the rows — the tree is its link — each level's grants met, and each row it reaches must meet, through `check()` with `options`, its visit's grants, its source `where` narrowed as a rule is, the guards of the relations its label and axes cross and the values the lens allows — so it offers what `toSourceQueries` does. A scalar-list field gives one option per element; a value takes its least label. A `from: 'mapDefaults'` source throws (see below). |
|
|
1171
1248
|
|
|
1172
1249
|
Options never offer a value the lens disallows: `projectLens` drops fetched values outside a
|
|
1173
|
-
field's allowed set.
|
|
1250
|
+
field's allowed set. Nor do they come through a row the lens hides: each source `where` is
|
|
1251
|
+
narrowed under the whole lens as `narrowRule` narrows a rule — every relation it crosses carries
|
|
1252
|
+
that visit's grants, inside an array condition (into its `condition`, or its `filter` under `all`,
|
|
1253
|
+
a window or no condition) and on each hop and terminal relation of a dotted path. A grant a rule
|
|
1254
|
+
couldn't carry there (one narrowRule can't re-root, a to-many relation read flat) is refused, as
|
|
1255
|
+
it is for a rule; so is a source whose query has a window toPrisma can't compile (by its shape,
|
|
1256
|
+
before anything compiles — `materializeSources` refuses it too, so the two never disagree).
|
|
1174
1257
|
|
|
1175
1258
|
#### Two kinds of source
|
|
1176
1259
|
|
|
1177
|
-
A source declared down a relation path offers the rows **reachable** from there:
|
|
1178
|
-
|
|
1260
|
+
A source declared down a relation path offers the rows **reachable** from there: the path is
|
|
1261
|
+
carried down through each relation's inverse (where the map declares one), and every grant above
|
|
1262
|
+
it with it, so a Tag source under `User.tagAttachments.tag` offers the tags a live
|
|
1179
1263
|
attachment of an in-tenant user points at. When a field should offer every row the lens lets its
|
|
1180
1264
|
model show — linked or not, what a rule may *name* — point the path source at the model's own
|
|
1181
1265
|
source:
|
|
@@ -1211,16 +1295,58 @@ carries down only in the layer that declares it: every layer before or after it
|
|
|
1211
1295
|
theirs, so a child's pointer can only narrow what its parent gave, and a tenant layer added after a
|
|
1212
1296
|
pointer always narrows it. How depends on how it scopes: through `mapDefaults` (the model's own
|
|
1213
1297
|
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
|
|
1215
|
-
grant
|
|
1298
|
+
reach the pointer down the path, so it offers just the linked rows — and across a bridge the
|
|
1299
|
+
grant comes back as the query's `recheck` (below). Scope tenancy through `mapDefaults` to keep a pointer's unlinked rows.
|
|
1216
1300
|
A pointer whose model declares no source fails `validateNarrowing` (`invalid_source`) and throws
|
|
1217
1301
|
from `projectLens` (by path) / `toSourceQueries` / `materializeSources`, even where a layer hides
|
|
1218
1302
|
its field; `projectLens(…, { by: 'model' })` and `describeRuleSources` read the lens without
|
|
1219
1303
|
validating it. Across a bridge it is how a picker gets options at all: the
|
|
1220
1304
|
model source compiles against the far map alone, with that map's own tenancy, where a path source
|
|
1221
|
-
|
|
1305
|
+
over-fetches and re-checks (see "Sources across a bridge"). A grant a path can't carry down — a relation whose
|
|
1306
|
+
map declares no inverse — is a `LensRefusal`, never an empty list. `materializeSources` refuses a pointer — a fetched collection can't hold unlinked
|
|
1222
1307
|
rows; query it with `toSourceQueries` and `materializeSourceQuery`.
|
|
1223
1308
|
|
|
1309
|
+
#### Sources across a bridge
|
|
1310
|
+
|
|
1311
|
+
A source whose path, `where`, `label` or an axis reads across a bridge can't be decided by one
|
|
1312
|
+
database — each holds one side. `toSourceQueries` still returns a real query for the side it can
|
|
1313
|
+
see, and marks it with `recheck`:
|
|
1314
|
+
|
|
1315
|
+
- **The query over-fetches.** What reads across the bridge compiles to TRUE (the compilers'
|
|
1316
|
+
over-fetch), so the query never pre-filters on what it can't see: its rows are a **superset** of
|
|
1317
|
+
the true options. Everything local is still decided by the database.
|
|
1318
|
+
- **`recheck` is what the database couldn't decide** — the conjuncts of `composedWhere` that read
|
|
1319
|
+
across a bridge, or `true` when only the label or an axis does. **If `recheck` is present, the
|
|
1320
|
+
query's rows are candidates, not options.** A source that reads no bridge has no `recheck`.
|
|
1321
|
+
- **The query selects local columns only:** the value, a local label and local axes, the local
|
|
1322
|
+
columns `recheck` reads, and each crossed bridge's local `on` key (under the local relations the
|
|
1323
|
+
read crosses first). It drops `distinct`, since candidates differ in what the re-check reads. A
|
|
1324
|
+
label or axis across the bridge is not selected.
|
|
1325
|
+
- **Load the far side, then materialize.** Put the far row inline on each candidate under its
|
|
1326
|
+
bridge field (the `indexBridges` shape — one row, or a list where the bridge names many) and
|
|
1327
|
+
call `materializeSourceQuery(query, rows, { lens, rowShape?, now? })`. It requires every key the
|
|
1328
|
+
re-check and a bridged label or axis read, as `materializeSources` does — a candidate without
|
|
1329
|
+
the far side, a far row without a column read, or a list where the bridge names one row throws a
|
|
1330
|
+
`UsageError` — keeps the candidates `check(recheck, row)` holds, and reads the label and axes
|
|
1331
|
+
across the bridge from the far side, in either row shape. Without `lens` it throws.
|
|
1332
|
+
- **A source past a bridge** (its path crosses one) is queried against its own model's map; the
|
|
1333
|
+
grants above it are carried back across the bridge through the far model's bridge field, and
|
|
1334
|
+
come back as its `recheck` — so each candidate holds the near rows inline
|
|
1335
|
+
(`{ id, industry, 'prisma:FanUser': [{ email, … }] }`).
|
|
1336
|
+
- **SQL rows are flat**, so a bridged query whose re-check reads through a local relation has
|
|
1337
|
+
`sql.sql: null` (with `sql.error`); run the Prisma form.
|
|
1338
|
+
|
|
1339
|
+
```ts
|
|
1340
|
+
const [query] = toSourceQueries(lens, { now });
|
|
1341
|
+
const candidates = await prisma[query.model].findMany(query.prisma); // superset
|
|
1342
|
+
const rows = query.recheck === undefined ? candidates : await loadFarSide(candidates); // your join
|
|
1343
|
+
const values = materializeSourceQuery(query, rows, { lens, now });
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
`materializeSources` over fetched rows still answers a path source across a bridge when the caller
|
|
1347
|
+
supplies the root rows with the far side inline (`{ id, email, crmId, 'salesforce:Contact': { id,
|
|
1348
|
+
industry } }`); a pointer (`from: 'mapDefaults'`) always goes through the query.
|
|
1349
|
+
|
|
1224
1350
|
### Fetching Under a Lens
|
|
1225
1351
|
|
|
1226
1352
|
`toLensSelect` and `projectRows` fetch the rows a lens shows and cut them to it.
|
|
@@ -1229,17 +1355,17 @@ rows; query it with `toSourceQueries` and `materializeSourceQuery`.
|
|
|
1229
1355
|
import { check, executePrismaPlan, narrowRule, projectRows, toLensSelect, toPrisma } from '@inixiative/json-rules';
|
|
1230
1356
|
|
|
1231
1357
|
const where = await executePrismaPlan(toPrisma(true, { lens: narrowing, now }), prisma);
|
|
1232
|
-
const rows = await prisma.user.findMany({ where, ...toLensSelect(narrowing, { now
|
|
1358
|
+
const rows = await prisma.user.findMany({ where, ...toLensSelect(narrowing, { now }) });
|
|
1233
1359
|
const shown = projectRows(narrowing, rows, { now }); // what a viewer may see
|
|
1234
1360
|
// To re-test the grants in memory later — never to return to a viewer:
|
|
1235
|
-
const forRecheck = projectRows(narrowing, rows, { keepGrantColumns: true,
|
|
1361
|
+
const forRecheck = projectRows(narrowing, rows, { keepGrantColumns: true, now });
|
|
1236
1362
|
const holds = forRecheck.filter((row) => check(narrowRule(rule, narrowing), row, { now }) === true);
|
|
1237
1363
|
```
|
|
1238
1364
|
|
|
1239
1365
|
| Function | Purpose |
|
|
1240
1366
|
| --- | --- |
|
|
1241
|
-
| `toLensSelect(lensOrNarrowing, options?)` | `{ select }` for `findMany` at the base model. It selects each
|
|
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
|
|
1367
|
+
| `toLensSelect(lensOrNarrowing, options?)` | `{ select }` for `findMany` at the base model. It selects each visit's visible columns and the relations turned on there (one that is off is not fetched; the model-default tree bounds it), 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 — one turned on with all its columns hidden, or one a grant reads only for presence or a count — is fetched by its key alone — the join key, else `id` — even a hidden one, as a grant's columns are; never another column. The fetch carries it for the re-check and a viewer's projection drops it; a model with no key is not fetched, and presence on it can't be re-checked from fetched rows. A root that shows no column is selected by its `id` likewise. Bridges are skipped. It also selects what each projected source reads — the value, its label and axes, and every column and relation its option query's condition reads — as it does a grant's columns, so `materializeSources` over the fetched rows offers what the database does; a viewer's projection drops them. A to-many relation's grant that needs a counting step (a count or an aggregate), or has a window toPrisma can't compile, is refused (a `LensRefusal`, which `validateNarrowing` reports) before anything compiles. `options` is the clock for compiling the grants. The root's own grants are the query's `where`: `toPrisma(rule, { lens })`. |
|
|
1368
|
+
| `projectRows(lensOrNarrowing, rows, options?)` | Rows cut to what the lens shows, recursively. Hidden columns, and relations that are off or omitted, 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 and the projected sources 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 any rule the lens admits; that output carries hidden values, so never return it to a viewer. The other options (`now`, `bindings`) are what each `where` is checked with. Plain JSON in and out. |
|
|
1243
1369
|
|
|
1244
1370
|
### Evaluating Across Bridges
|
|
1245
1371
|
|