@inixiative/json-rules 3.4.1 → 3.5.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 CHANGED
@@ -308,7 +308,7 @@ predicate. Pipeline: filter → order → skip → take. Direction comes from `o
308
308
  ```
309
309
 
310
310
  `filter` is a condition each element must pass to enter the window (`narrowRule` puts a lens
311
- grant there under `all`). `orderBy` is a non-empty array of `{ field, dir: 'asc' | 'desc' }` (multi-key);
311
+ clamp there under `all`). `orderBy` is a non-empty array of `{ field, dir: 'asc' | 'desc' }` (multi-key);
312
312
  `take`/`skip` are non-negative integers. **Empty-window semantics are author-driven**:
313
313
  `all` is vacuously true on an empty window, `atLeast: 1` (or `any`) is false. To require
314
314
  "the windowed element matches **and** one exists," combine `all` with `notEmpty` / `atLeast: 1`.
@@ -830,7 +830,7 @@ Rules:
830
830
  - `Condition`, `StrictCondition`, `Rule`, `AggregateRule`, `AggregateMode`, `ArrayRule`, `DateRule`, `Row`, `CheckData`
831
831
  - `GroupByStep`, `WhereStep`, `PrismaStep`, `PrismaWhere`, `StepRef` (a Prisma plan's steps); `ScopeRef`, `ScopedRef`, `ScopeOutOfBounds` (scope refs)
832
832
  - `CheckOptions`, `CompileOptions`, `ToPrismaOptions`, `ToSqlOptions`, `ToSqlResult`, `ToPrismaResult`, `ValidateRuleOptions`, `ListBindingsOptions`, `ValidationIssue`, `ValidationResult`
833
- - rule parts: `All`, `Any`, `IfThenElse`, `RuleValue`, `RuleScalar`, `OrderedRuleValue`, `ValueSourceOf`, `ValueSourceFields`, `NumberOffset`, `Magnitude`, `WindowFields`, `OrderBy`, `SortDir`
833
+ - rule parts: `All`, `Any`, `IfThenElse`, `RuleValue`, `RuleScalar`, `OrderedRuleValue`, `ValueSourceOf`, `ValueSourceFields`, `NumberOffset`, `Magnitude`, `WindowFields`, `OrderBy`, `SortDir` (and `orderRecords` to sort records by an `OrderBy`)
834
834
  - dates: `DateRuleValue`, `DateInputValue`, `DateInputOrExpr`, `DateExpr`, `RollingExpr`, `PeriodExpr`, `EdgeExpr`, `PeriodUnit`, `RelativeUnits`, `DateOffset`, `DateConfig`, `TimeZoneConfig`, `WeekStart`
835
835
  - 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`
836
836
  - `engineGlobals`, `EngineGlobalsState`, `PrismaProvider`, `FuzzyConfig`
@@ -841,7 +841,7 @@ Lens & bridges:
841
841
  - `FieldMap`, `FieldMapEntry`, `ModelEntry`, `SourceOption`, `FieldMapSet`, `Bridge`, `BridgeEndpoint`, `BridgeCardinality`, `BridgeDictionary`
842
842
  - `createLens`, `storeLens`, `composeLens`, `StoredLens`, `stitchFieldMaps`, `indexBridges`, `validateFieldMaps`, `assertValidFieldMaps`
843
843
  - `validateNarrowing`, `assertValidNarrowing`, `validateRuleInLens`, `narrowRule`, `coerceRule`
844
- - `bindLens`, `listLensBindings`, `getLensRoot`
844
+ - `bindLens`, `listLensBindings`, `getLensRoot`, `clampLens`, `LensClamps`
845
845
  - `projectLens`, `walkLensPath`, `readLensValue`, `LensValue`, `describeRule`, `describeRuleSources`
846
846
  - `toLensSelect`, `projectRows`, `LensSelect`, `LensRelationSelect`, `LensSelectOptions`, `ProjectRowsOptions`
847
847
  - `toSourceQueries`, `materializeSources`, `materializeSourceQuery`
@@ -867,9 +867,9 @@ A lens has three forms, each with its own job:
867
867
  narrowing it came from.
868
868
 
869
869
  ```ts
870
- const records = storeLens(grantLens, ['user', 'org-acme', 'grant-7']); // persist each record
871
- // later: fetch 'grant-7', then the ids in its `parents`
872
- const lens = composeLens('grant-7', { user, 'org-acme': orgAcme, 'grant-7': grant7 });
870
+ const records = storeLens(delegateLens, ['user', 'org-acme', 'delegate-7']); // persist each record
871
+ // later: fetch 'delegate-7', then the ids in its `parents`
872
+ const lens = composeLens('delegate-7', { user, 'org-acme': orgAcme, 'delegate-7': delegate7 });
873
873
  ```
874
874
 
875
875
  ### Operator Catalog
@@ -957,7 +957,7 @@ Two error classes say whose input went wrong; each sets `name`, so check with `i
957
957
  | Class | Thrown when | Fix |
958
958
  | --- | --- | --- |
959
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. |
960
+ | `LensRefusal` (`code`) | The lens itself can't do it: a later layer's clamp reads what its parent hides, a clamp 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
961
 
962
962
  ```ts
963
963
  import { LensRefusal, UsageError } from '@inixiative/json-rules';
@@ -1014,7 +1014,7 @@ boundary is **enforced, not documented**:
1014
1014
 
1015
1015
  - a rule authored against a lens provably can't reference outside it —
1016
1016
  `validateRuleInLens`, at author time;
1017
- - the row-scope **`where` is the grant, applied server-side at execution** via
1017
+ - the row-scope **`where` is the clamp, applied server-side at execution** via
1018
1018
  `narrowRule` — the authored rule never sees it and can't escape it;
1019
1019
  - what reaches an untrusted party reveals nothing hidden — `projectLens(…, { by: 'model' })`.
1020
1020
 
@@ -1022,7 +1022,7 @@ A lens defines a **surface area**, reused for distinct, separately-enforced
1022
1022
  constraints that may **diverge**: the *data-flow* surface (what you receive / pass
1023
1023
  into an interpolated template / expose to a client) vs the *reasoning* surface
1024
1024
  (what you may author predicates against — which can be narrower than what you
1025
- actually get back). One predicate DSL (`Condition`) expresses both the **grant**
1025
+ actually get back). One predicate DSL (`Condition`) expresses both the **clamp**
1026
1026
  (`where`) and the **use** (rules), compiling to `check` / `toPrisma` / `toSql`.
1027
1027
  That's why the same primitive backs permissions, email targeting/conditions,
1028
1028
  feature flags, and state-transition guards — it's the authority/visibility spine
@@ -1117,11 +1117,11 @@ validateRuleInLens({ field: 'posts', arrayOperator: ArrayOperator.any, condition
1117
1117
  `root.relations`; a spelled node grows its own tree. Every posture walks the same tree, so they
1118
1118
  agree and stay small. `lensVisit(lens, 'org.users')` resolves one visit on demand, as
1119
1119
  `projectLens` would give it, or `null`.
1120
- - **Grants.** The first narrowing's `where`s (and source eligibility `where`s) may read any
1120
+ - **Clamps.** The first narrowing's `where`s (and source eligibility `where`s) may read any
1121
1121
  relation on the schema; a later layer's may read only what its parent shows — a delegate can't
1122
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.
1123
+ value `path` reads the root row, so only `root.where` may hold one. `toLensSelect` fetches exactly the columns clamps read, and
1124
+ `projectRows({ keepClampColumns: true })` keeps them for a re-check.
1125
1125
 
1126
1126
  ### LensNarrowing & `where`
1127
1127
 
@@ -1146,7 +1146,7 @@ const narrowing: LensNarrowing = {
1146
1146
  };
1147
1147
  ```
1148
1148
 
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.
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 clamp 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.
1150
1150
 
1151
1151
  ### Lens Utilities
1152
1152
 
@@ -1159,10 +1159,12 @@ Composition across chained narrowings is pure intersection: relations are turned
1159
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. |
1160
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. |
1161
1161
  | `getLensRoot(lensOrNarrowing)` | The base lens a narrowing chain is rooted at; a lens is its own. Throws on a cyclic chain. |
1162
+ | `clampLens(lens, clamps)` | The lens with `clamps` (`{ root?: { where }, mapDefaults?: { [map]: { models: { [model]: { where?, sources? } } } } }`) ANDed into its first narrowing over the base lens, one added over a bare lens; later layers kept as they were. Only the first layer's clamps read the whole schema, so a clamp on what a later layer hides goes here. A source's `where` ANDs; its `label` / `groupBy` win, and a source clamp on a field with none adds that option set. Wheres only narrow. |
1162
1163
  | `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
1164
  | `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`. |
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. |
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`. |
1165
+ | `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 clamps: one a clamp hides, or a missing one, reads `null`. Only own properties are read, into a Json column too. `options` is what each clamp is checked with (`now`, `bindings`). For values a template interpolates. |
1166
+ | `orderRecords(items, orderBy)` | Records sorted by an `OrderBy` exactly as a window orders them on every rail: each key in turn, own-property path reads, `dir` order, NULL or absent last in either direction, ties in input order. Returns a new array. Strings sort in JavaScript code-unit order, which can differ from a database's collation (see [Where the Rails Differ](#where-the-rails-differ)). |
1167
+ | `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 clamp 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`. |
1166
1168
  | `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. |
1167
1169
 
1168
1170
  ```ts
@@ -1208,6 +1210,7 @@ to each field as `options`.
1208
1210
 
1209
1211
  ```ts
1210
1212
  import {
1213
+ executePrismaPlan,
1211
1214
  materializeSourceQuery,
1212
1215
  materializeSources,
1213
1216
  projectLens,
@@ -1229,8 +1232,13 @@ const [query] = toSourceQueries(narrowing);
1229
1232
  // query.prisma => { model: 'User', distinct: ['region'], select: { region: true }, where: { AND: [...] } }
1230
1233
  // query.sql => { sql: 'SELECT DISTINCT "t0"."region" FROM "User" AS "t0" WHERE (...)', params: ['t-42', true] }
1231
1234
  // A query with `recheck` (a source across a bridge) returns candidates; see "Sources across a bridge".
1232
- const { distinct, select, where } = query.prisma;
1233
- const rows = await prisma.user.findMany({ distinct, select, where });
1235
+ // `steps` (a count step, a column reference) resolve through executePrismaPlan first.
1236
+ const { distinct, select, where, steps } = query.prisma; // query.model === 'User'
1237
+ const rows = await prisma.user.findMany({
1238
+ distinct,
1239
+ select,
1240
+ where: steps ? await executePrismaPlan({ steps }, prisma) : where,
1241
+ });
1234
1242
  const values = materializeSourceQuery(query, rows); // { path, mapName, model, field, options: [{ value }] }
1235
1243
 
1236
1244
  // Or from rows already fetched under the lens (relations inline):
@@ -1242,15 +1250,15 @@ const projection = projectLens(narrowing, { sourceValues: [values] });
1242
1250
 
1243
1251
  | Function | Purpose |
1244
1252
  | --- | --- |
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. |
1253
+ | `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` (a count step, or a column reference to resolve): query with the where it returns. `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
1254
  | `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). |
1255
+ | `materializeSources(lensOrNarrowing, rows, options?)` | `SourceValues[]` for every sourced field, from the rows the lens fetches — `toLensSelect`'s rows as fetched, or as `projectRows(…, { keepClampColumns: 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 clamp 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 clamps met, and each row it reaches must meet, through `check()` with `options`, its visit's clamps, 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). |
1248
1256
 
1249
1257
  Options never offer a value the lens disallows: `projectLens` drops fetched values outside a
1250
1258
  field's allowed set. Nor do they come through a row the lens hides: each source `where` is
1251
1259
  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
1260
+ that visit's clamps, inside an array condition (into its `condition`, or its `filter` under `all`,
1261
+ a window or no condition) and on each hop and terminal relation of a dotted path. A clamp a rule
1254
1262
  couldn't carry there (one narrowRule can't re-root, a to-many relation read flat) is refused, as
1255
1263
  it is for a rule; so is a source whose query has a window toPrisma can't compile (by its shape,
1256
1264
  before anything compiles — `materializeSources` refuses it too, so the two never disagree).
@@ -1258,7 +1266,7 @@ before anything compiles — `materializeSources` refuses it too, so the two nev
1258
1266
  #### Two kinds of source
1259
1267
 
1260
1268
  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
1269
+ carried down through each relation's inverse (where the map declares one), and every clamp above
1262
1270
  it with it, so a Tag source under `User.tagAttachments.tag` offers the tags a live
1263
1271
  attachment of an in-tenant user points at. When a field should offer every row the lens lets its
1264
1272
  model show — linked or not, what a rule may *name* — point the path source at the model's own
@@ -1290,19 +1298,19 @@ mapDefaults: {
1290
1298
 
1291
1299
  `from: 'mapDefaults'` resolves where it sits — `mapDefaults[<this path's map>].models[<this path's
1292
1300
  model>].sources[<field>]` — and takes that source's eligibility (tenancy included), label and
1293
- axes; its own `where`, and child layers, only narrow it. A pointer drops the grants the path
1301
+ axes; its own `where`, and child layers, only narrow it. A pointer drops the clamps the path
1294
1302
  carries down only in the layer that declares it: every layer before or after it still carries
1295
1303
  theirs, so a child's pointer can only narrow what its parent gave, and a tenant layer added after a
1296
1304
  pointer always narrows it. How depends on how it scopes: through `mapDefaults` (the model's own
1297
- grant or source) the pointer still offers unlinked rows; through a root `where` the grant can only
1305
+ clamp or source) the pointer still offers unlinked rows; through a root `where` the clamp can only
1298
1306
  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.
1307
+ clamp comes back as the query's `recheck` (below). Scope tenancy through `mapDefaults` to keep a pointer's unlinked rows.
1300
1308
  A pointer whose model declares no source fails `validateNarrowing` (`invalid_source`) and throws
1301
1309
  from `projectLens` (by path) / `toSourceQueries` / `materializeSources`, even where a layer hides
1302
1310
  its field; `projectLens(…, { by: 'model' })` and `describeRuleSources` read the lens without
1303
1311
  validating it. Across a bridge it is how a picker gets options at all: the
1304
1312
  model source compiles against the far map alone, with that map's own tenancy, where a path source
1305
- over-fetches and re-checks (see "Sources across a bridge"). A grant a path can't carry down — a relation whose
1313
+ over-fetches and re-checks (see "Sources across a bridge"). A clamp a path can't carry down — a relation whose
1306
1314
  map declares no inverse — is a `LensRefusal`, never an empty list. `materializeSources` refuses a pointer — a fetched collection can't hold unlinked
1307
1315
  rows; query it with `toSourceQueries` and `materializeSourceQuery`.
1308
1316
 
@@ -1330,15 +1338,22 @@ see, and marks it with `recheck`:
1330
1338
  `UsageError` — keeps the candidates `check(recheck, row)` holds, and reads the label and axes
1331
1339
  across the bridge from the far side, in either row shape. Without `lens` it throws.
1332
1340
  - **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
1341
+ clamps above it are carried back across the bridge through the far model's bridge field, and
1334
1342
  come back as its `recheck` — so each candidate holds the near rows inline
1335
1343
  (`{ id, industry, 'prisma:FanUser': [{ email, … }] }`).
1344
+ - **A re-check reading into a Json column selects the column whole** (`meta: true`, never a nested
1345
+ select into it); the re-check reads inside it in memory.
1336
1346
  - **SQL rows are flat**, so a bridged query whose re-check reads through a local relation has
1337
1347
  `sql.sql: null` (with `sql.error`); run the Prisma form.
1338
1348
 
1339
1349
  ```ts
1340
1350
  const [query] = toSourceQueries(lens, { now });
1341
- const candidates = await prisma[query.model].findMany(query.prisma); // superset
1351
+ const { distinct, select, where, steps } = query.prisma; // query.model === 'User'
1352
+ const candidates = await prisma.user.findMany({
1353
+ distinct,
1354
+ select,
1355
+ where: steps ? await executePrismaPlan({ steps }, prisma) : where,
1356
+ }); // a superset when `recheck` is present
1342
1357
  const rows = query.recheck === undefined ? candidates : await loadFarSide(candidates); // your join
1343
1358
  const values = materializeSourceQuery(query, rows, { lens, now });
1344
1359
  ```
@@ -1357,15 +1372,15 @@ import { check, executePrismaPlan, narrowRule, projectRows, toLensSelect, toPris
1357
1372
  const where = await executePrismaPlan(toPrisma(true, { lens: narrowing, now }), prisma);
1358
1373
  const rows = await prisma.user.findMany({ where, ...toLensSelect(narrowing, { now }) });
1359
1374
  const shown = projectRows(narrowing, rows, { now }); // what a viewer may see
1360
- // To re-test the grants in memory later — never to return to a viewer:
1361
- const forRecheck = projectRows(narrowing, rows, { keepGrantColumns: true, now });
1375
+ // To re-test the clamps in memory later — never to return to a viewer:
1376
+ const forRecheck = projectRows(narrowing, rows, { keepClampColumns: true, now });
1362
1377
  const holds = forRecheck.filter((row) => check(narrowRule(rule, narrowing), row, { now }) === true);
1363
1378
  ```
1364
1379
 
1365
1380
  | Function | Purpose |
1366
1381
  | --- | --- |
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. |
1382
+ | `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 clamps compiled as its `where`, so related rows come pre-narrowed — unless a clamp reads that list: a clamp 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 clamp hides. A relation that shows no column — one turned on with all its columns hidden, or one a clamp reads only for presence or a count — is fetched by its key alone — the join key, else `id` — even a hidden one, as a clamp'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 clamp's columns, so `materializeSources` over the fetched rows offers what the database does; a viewer's projection drops them. For a source across a bridge that is its local reads: what it reads on this side, hidden or not, and each crossed bridge's local `on` key, so the rows plus the far side loaded under its bridge field answer it. A to-many relation's clamp 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 clamps. The root's own clamps are the query's `where`: `toPrisma(rule, { lens })`. |
1383
+ | `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`. `keepClampColumns: 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 clamp reads, as those columns alone, so `check(narrowRule(rule, lens), row)` re-tests the clamps 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. |
1369
1384
 
1370
1385
  ### Evaluating Across Bridges
1371
1386