@stonyx/orm 0.3.2-alpha.60 → 0.3.2-alpha.62

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
@@ -315,15 +315,24 @@ Access classes define models and provide custom filtering/authorization logic.
315
315
  > [Identifying the collection](#identifying-the-collection) before copying this.**
316
316
  > Every attempt to identify the collection by parsing the request target has
317
317
  > failed **open** — five distinct variants of this same example, each found only
318
- > after the previous was fixed, by five different people. The sample below does
319
- > not parse anything: it reads `request.baseUrl`, the mount Express actually
320
- > matched.
318
+ > after the previous was fixed, by five different people. That section is now a
319
+ > record of what not to do, not a matching recipe: the sample below reads `model`
320
+ > from [the access context](#the-access-context-second-argument) and never looks
321
+ > at the mount at all, so all five variants are **unconstructible** against it
322
+ > rather than merely handled.
321
323
  >
322
324
  > That is still a stopgap. **The real fix is
323
325
  > [#202](https://github.com/abofs/stonyx-orm/issues/202)** — `access()` should
324
326
  > receive the model, the operation and the record, so there is nothing to
325
327
  > identify. Until it lands, prefer the array shape (`['read']`) or `false` where
326
328
  > you can: the **function** shape is the one that requires any matching at all.
329
+ >
330
+ > The one read of argument **one** that survives is `request.path`, for the
331
+ > `/archived` sub-path deny — and it has to. The context names which model and
332
+ > which verb, not which route, so that deny **cannot be expressed from the
333
+ > context alone** and a context-only rewrite would silently turn it into an
334
+ > allow.
335
+ >
327
336
  > The same warning is repeated at the top of `src/orm-request.ts`, which ships;
328
337
  > the longer write-up in `docs/usage-patterns.md` does **not** ship, so this
329
338
  > README and that source header are the two copies a consumer sees.
@@ -337,27 +346,39 @@ Access classes define models and provide custom filtering/authorization logic.
337
346
  export default class GlobalAccess {
338
347
  models = ['owner', 'animal'];
339
348
 
340
- access(request) {
341
- // `request.baseUrl` is the mount Express matched `/owners`, or
342
- // `/api/owners` under ORM_REST_ROUTE=/api. Never parse `originalUrl`: it is
343
- // the raw request target and can be absolute-form.
344
- const mount = request.baseUrl;
345
-
346
- // FAIL CLOSED. If Express did not tell us what it matched we are not behind
347
- // the mount we think we are, and an unidentifiable request denies rather
348
- // than falling through to the CRUD grant at the bottom.
349
- if (typeof mount !== 'string' || mount === '') return false;
350
-
351
- // Lower-cased because the router matched case-INSENSITIVELY, and a matcher
352
- // stricter than the router that dispatched the request can be stepped
353
- // around. The PATH only record ids stay at their real case below.
354
- const collection = mount.toLowerCase();
355
-
356
- // `request.path` is mount-relative and query-free, so sub-path rules need no
357
- // prefix arithmetic either. false 403 for the whole request.
358
- const path = String(request.path ?? '').toLowerCase();
349
+ access(request, { model, operation }) {
350
+ // `model` is the model this route was mounted for. It is assigned once, at
351
+ // mount time, and no request can influence it — not a mount prefix, not a
352
+ // query string, not a case-varied path, not an absolute-form request
353
+ // target. Nothing below parses anything, so none of the five fail-open
354
+ // variants recorded in the header is constructible against this predicate
355
+ // any more; they are history, not rules to follow.
356
+ //
357
+ // `operation` is destructured to name the whole contract at the point of
358
+ // use. This sample's rules are per-model and per-sub-path rather than
359
+ // per-verb, so it does not branch on it; the permission array at the bottom
360
+ // is where the verb is answered.
361
+
362
+ // FAIL CLOSED. `model` is absent for any caller that resolved this
363
+ // predicate without supplying the context, and a request this function
364
+ // cannot identify DENIES rather than falling through to the CRUD grant at
365
+ // the bottom. An unidentifiable input must never be the permissive path.
366
+ if (typeof model !== 'string' || model === '') return false;
367
+
368
+ if (model === 'owner') {
369
+ // The context names WHICH MODEL and WHICH VERB — not which route. Six
370
+ // distinct owner surfaces produce one identical context, so a rule that
371
+ // depends on the SUB-PATH still needs argument one. `request.path` is
372
+ // mount-relative and query-free, and it is the one read of the raw
373
+ // request the README sanctions. Lower-cased because the router matched
374
+ // case-insensitively, so a case-sensitive rule here would be stricter
375
+ // than the router and could be stepped around. false → 403 for the whole
376
+ // request.
377
+ //
378
+ // THIS DENY CANNOT BE EXPRESSED FROM THE CONTEXT ALONE. Migrating it away
379
+ // does not remove a rule, it turns a deny into an ALLOW, silently.
380
+ const path = String(request.path ?? '').toLowerCase();
359
381
 
360
- if (collection.endsWith('/owners')) {
361
382
  if (path === '/archived' || path.startsWith('/archived/')) return false;
362
383
 
363
384
  // Returning a function plugs it in as a per-record filter, and it is
@@ -373,7 +394,7 @@ export default class GlobalAccess {
373
394
  // inert. Deliberately NO `?? record.owner` fallback: accepting the raw
374
395
  // shape as well as the resolved one would absorb a resolution regression
375
396
  // silently, which is exactly what blinded this fixture before.
376
- if (collection.endsWith('/animals')) return record => record.owner?.id !== 'restricted';
397
+ if (model === 'animal') return record => record.owner?.id !== 'restricted';
377
398
 
378
399
  // Allows full access to all calls that don't match any of the above conditions
379
400
  return ['read', 'create', 'update', 'delete'];
@@ -523,9 +544,10 @@ sample, `getAccess('owner') === getAccess('animal')`.
523
544
  #### Passing the context makes a model-correct answer *possible*
524
545
 
525
546
  It does not make the answer model-correct on its own. **The resolved predicate
526
- has to read the context.** Measured against the access class shipped with this
527
- repo, on a request Express dispatched to `GET /owners/angela`, asked about
528
- **animals**:
547
+ has to read the context.** Against a predicate that ignores it the failure is
548
+ measurable. On a request Express dispatched to `GET /owners/angela`, asked about
549
+ **animals**, the sample as it shipped before
550
+ [#222](https://github.com/abofs/stonyx-orm/issues/222) answered:
529
551
 
530
552
  ```
531
553
  getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
@@ -533,18 +555,26 @@ getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
533
555
  ```
534
556
 
535
557
  That is the **owners** filter, and it returns `true` for animal 21 — the record
536
- hidden on every animal surface. Under a mount that predicate recognizes neither
537
- way it is worse still: it falls through to
538
- `['read', 'create', 'update', 'delete']`, a full CRUD grant.
539
-
540
- Either way the context was supplied and the answer is not the animal answer, and
541
- it is wrong in the direction that **grants**. That predicate is single-argument
542
- and identifies its collection from the request, so it answered about the
543
- collection the request is *addressed to* while being asked about another one.
544
- Every predicate in this repo, and in every consumer tree, is single-argument on
545
- the day this ships, and a caller has no supported way to tell which kind it
546
- resolved. The boot-time arity warning that would surface it is
547
- [#213](https://github.com/abofs/stonyx-orm/issues/213).
558
+ hidden on every animal surface. Under a mount such a predicate recognizes
559
+ neither way it is worse still: it falls through to
560
+ `['read', 'create', 'update', 'delete']`, a full CRUD grant. Either way the
561
+ context was supplied and the answer is not the animal answer, and it is wrong in
562
+ the direction that **grants** because that predicate was single-argument and
563
+ identified its collection from the request, so it answered about the collection
564
+ the request was *addressed to* while being asked about another one.
565
+
566
+ The sample shipped with this repo has since been migrated to read the context,
567
+ and the same call now answers with the **animal** filter:
568
+
569
+ ```
570
+ getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
571
+ -> record => record.owner?.id !== 'restricted'
572
+ ```
573
+
574
+ A single-argument predicate remains the default in every consumer tree, and a
575
+ caller has no supported way to tell which kind it resolved. The boot-time arity
576
+ warning that surfaces one is
577
+ [#221](https://github.com/abofs/stonyx-orm/issues/221).
548
578
 
549
579
  So: pass the context, and do not treat a resolved predicate's answer as
550
580
  model-specific until that predicate has been migrated to read it.
@@ -621,9 +651,17 @@ one both behave exactly as before.
621
651
 
622
652
  ### Identifying the collection
623
653
 
624
- **Do not reconstruct the request path.** Every version of this sample that tried
625
- to has failed **open**, and each variant was found only after the previous one
626
- was fixed:
654
+ **Do not reconstruct the request path and since
655
+ [#202](https://github.com/abofs/stonyx-orm/issues/202) you do not have to
656
+ identify the collection at all.** Read `model` from
657
+ [the access context](#the-access-context-second-argument): it is fixed at mount
658
+ time, no request can influence it, and there is nothing left to parse.
659
+
660
+ **Everything below is the record of what happened when this sample did parse
661
+ it.** It is kept as history, not as a recipe — none of these matching strategies
662
+ should be written into a new predicate. Every version of this sample that tried
663
+ to identify the collection from the request target failed **open**, and each
664
+ variant was found only after the previous one was fixed:
627
665
 
628
666
  | # | Variant | Why it fails open |
629
667
  |---|---|---|
@@ -633,13 +671,19 @@ was fixed:
633
671
  | 4 | hard-coded `/owners` | With `ORM_REST_ROUTE=/api` every url becomes `/api/owners/...` and the sample matches nothing — environment-specifically, which is harder to notice than failing everywhere. The remediation this document used to give was itself broken: `` `${config.orm.restServer.route}owners` `` evaluates to **`/apiowners`**, so a reader who followed the correction exactly still failed open and believed they had handled it. |
634
672
  | 5 | any match on `originalUrl` at all | HTTP/1.1 permits an **absolute-form** request-target. Express routes on `parseurl(req).pathname`, so the request dispatches normally — but `originalUrl` is the raw target. `GET http://anything.example/owners/angela` yields `originalUrl === 'http://anything.example/owners/angela'`, which has no `/owners` prefix. Measured: the record came back in full, `DELETE` succeeded, and it walked past a hard `return false` deny the same way. |
635
673
 
636
- **The fix is not a sixth rule.** It is to stop parsing:
674
+ **The fix is not a sixth rule, and it is not a better string to match.** It is
675
+ to stop identifying the collection at all.
637
676
 
638
- **Use `request.baseUrl`.** It is the mount Express *actually matched* when it
639
- dispatched the request. It carries no query string (variant 2), it is not
640
- mount-relative (variant 1), it already contains the configured `ORM_REST_ROUTE`
641
- prefix (variant 4 — there is nothing left to derive, so `/apiowners` is
642
- unconstructible), and it is unaffected by an absolute-form target (variant 5).
677
+ An intermediate revision read **`request.baseUrl`** the mount Express
678
+ *actually matched*. That closed all five variants: it carries no query string
679
+ (variant 2), it is not mount-relative (variant 1), it already contains the
680
+ configured `ORM_REST_ROUTE` prefix (variant 4 — there is nothing left to derive,
681
+ so `/apiowners` is unconstructible), and it is unaffected by an absolute-form
682
+ target (variant 5). It was still a transport artifact standing in for a
683
+ structural fact, and it is **no longer what the sample does**: the sample reads
684
+ `model`, so all five variants are unconstructible against it rather than
685
+ handled. The table below is retained as the measured evidence behind the five
686
+ variants, not because any of these values should be matched on:
643
687
 
644
688
  | request | `request.url` | `request.originalUrl` | `request.baseUrl` | `request.path` |
645
689
  |---|---|---|---|---|
@@ -650,21 +694,23 @@ unconstructible), and it is unaffected by an absolute-form target (variant 5).
650
694
  | `GET http://anything.example/owners/angela` | `http://anything.example/angela` | `http://anything.example/owners/angela` | `/owners` | `/angela` |
651
695
  | `GET /api/animals/22` (`ORM_REST_ROUTE=/api`) | `/22` | `/api/animals/22` | `/api/animals` | `/22` |
652
696
 
653
- Two rules remain, and they are the whole list:
654
-
655
- **1. Compare lower-cased.** `baseUrl` is the text the caller sent, not the
656
- registered mount `GET /OwNeRs/angela` yields `/OwNeRs`. The router matched it
657
- case-insensitively, so a case-sensitive comparison here is stricter than the
658
- router and can be walked past. Lower-case the **mount and path only**; record ids
659
- are case-sensitive and must be compared at their real case.
660
-
661
- **2. Fail closed when `baseUrl` is absent.** `String(request.originalUrl ?? '')`
662
- was added to stop a `TypeError`, and it traded fail-closed for fail-**open**: an
663
- empty string matches no collection, so `access()` fell through to the permission
664
- array and granted full CRUD. An input you cannot identify must **deny**.
665
-
666
- Use `request.path` mount-relative and query-free if you need to distinguish
667
- sub-paths beneath the mount, as the `/archived` deny above does.
697
+ **One read of argument one survives, and it must: `request.path`.** It is
698
+ mount-relative and query-free, and it is for rules that distinguish **sub-paths**
699
+ beneath the mount as the `/archived` deny in the sample above does. The context
700
+ names which model and which verb, **not which route**, so that deny *cannot be
701
+ expressed from the context alone*, and a context-only rewrite would silently turn
702
+ it into an allow. Lower-case it before comparing: the router matched
703
+ case-insensitively, so a case-sensitive sub-path rule is stricter than the router
704
+ that dispatched the request and can be stepped around. Record ids are
705
+ case-sensitive and must be compared at their real case.
706
+
707
+ **Fail closed on anything you cannot identify.**
708
+ `String(request.originalUrl ?? '')` was once added here to stop a `TypeError`,
709
+ and it traded fail-closed for fail-**open**: an empty string matched no
710
+ collection, so `access()` fell through to the permission array and granted full
711
+ CRUD. The same rule now applies to the context the sample returns `false` for
712
+ an absent `model` rather than falling through. An input you cannot identify must
713
+ **deny**.
668
714
 
669
715
  ### Known limitations
670
716
 
@@ -687,11 +733,14 @@ sub-paths beneath the mount, as the `/archived` deny above does.
687
733
  see [The access context](#the-access-context-second-argument):
688
734
  `Orm.instance.getAccess(modelName)` makes another model's predicate
689
735
  **reachable**, and `context.model` makes a **model-correct answer possible** —
690
- possible, not guaranteed: the resolved predicate has to read the context, and
691
- every predicate in tree is still single-argument
692
- ([#213](https://github.com/abofs/stonyx-orm/issues/213)), so today it answers
693
- about the collection the request is addressed to. **The mechanism exists; the
694
- ORM does not yet use it on this path.** The re-parenting write above is still
736
+ possible, not guaranteed: the resolved predicate has to read the context. The
737
+ sample shipped with this repo now does
738
+ ([#222](https://github.com/abofs/stonyx-orm/issues/222)), so
739
+ `getAccess('animal')` answers with the animal filter; a predicate that ignores
740
+ the second argument still answers about the collection the request is
741
+ addressed to, and the boot-time warning that surfaces one is
742
+ [#221](https://github.com/abofs/stonyx-orm/issues/221). **The mechanism
743
+ exists; the ORM does not yet use it on this path.** The re-parenting write above is still
695
744
  **not refused** — that enforcement is
696
745
  [#196](https://github.com/abofs/stonyx-orm/issues/196) and
697
746
  [#207](https://github.com/abofs/stonyx-orm/issues/207), which were blocked on
@@ -98,8 +98,8 @@
98
98
  *
99
99
  * PASSING THE CONTEXT MAKES A MODEL-CORRECT ANSWER POSSIBLE. It does not make
100
100
  * the answer model-correct on its own -- the resolved predicate has to READ it.
101
- * Measured against this repo's own shipped access class, on a request express
102
- * dispatched to `GET /owners/angela`, asked about ANIMALS:
101
+ * Measured against an ARITY-1 predicate, on a request express dispatched to
102
+ * `GET /owners/angela`, asked about ANIMALS:
103
103
  *
104
104
  * getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
105
105
  * -> record => record.id !== 'angela' && record.id !== 'restricted'
@@ -110,23 +110,30 @@
110
110
  * `['read', 'create', 'update', 'delete']`, a full CRUD grant. Either way the
111
111
  * context was supplied and the answer is not the animal answer, and it is wrong
112
112
  * in the GRANTING direction, because that predicate is arity-1 and identifies
113
- * its collection from the request. (The first of these is asserted on a live
114
- * dispatch by AC9 in test/integration/orm-test.ts.)
115
- *
116
- * Every predicate in this repo and in every consumer tree is arity-1 on the day
117
- * this ships, and the caller has no supported way to tell which kind it got --
118
- * the boot-time arity warning that would surface it is abofs/stonyx-orm#213.
113
+ * its collection from the request. (Asserted on a live dispatch by AC9 in
114
+ * test/integration/orm-test.ts, against a deliberately arity-1 predicate.)
115
+ *
116
+ * This repo's own sample access class has since been MIGRATED to read the
117
+ * context (abofs/stonyx-orm#222), so `getAccess('animal')` here now answers
118
+ * with the animal filter. That is not true of a consumer tree: an arity-1
119
+ * predicate keeps working -- the second argument is additive -- and the caller
120
+ * has no supported way to tell which kind it got. The boot-time arity warning
121
+ * that surfaces one is abofs/stonyx-orm#221.
119
122
  * So: pass the context, and do not treat a resolved predicate's answer as
120
123
  * model-specific until that predicate has been migrated to read the context.
121
124
  *
122
125
  * ---------------------------------------------------------------------------
123
126
  * DO NOT RECONSTRUCT THE REQUEST PATH INSIDE `access()`.
124
127
  * ---------------------------------------------------------------------------
125
- * `auth()` below hands your `access(request)` a raw transport artifact and asks
126
- * you to work out which collection it addresses. Every attempt to do that by
127
- * parsing the request target has failed OPEN. Five distinct variants of the
128
- * same three-line example have now been found, each after the previous was
129
- * fixed, by five different people:
128
+ * You do not have to. `auth()` below hands your predicate the ACCESS CONTEXT as
129
+ * argument two, and `context.model` already names the collection -- see the
130
+ * contract section above. Argument ONE is still the raw transport artifact, and
131
+ * everything from here to the end of this banner is the record of what happened
132
+ * when predicates worked the collection out from it. IT IS HISTORY, NOT
133
+ * GUIDANCE: do not write any of it into a new predicate. Every attempt to
134
+ * identify the collection by parsing the request target has failed OPEN. Five
135
+ * distinct variants of the same three-line example have now been found, each
136
+ * after the previous was fixed, by five different people:
130
137
  *
131
138
  * 1. `request.url` is mount-relative under `RestServer.mountRoute`, so a
132
139
  * prefix match against it is ALWAYS false.
@@ -145,22 +152,31 @@
145
152
  * last, and the record comes back in full. It walks past a hard
146
153
  * `return false` deny the same way.
147
154
  *
148
- * The fix is not a sixth rule. It is to stop parsing:
149
- *
150
- * `request.baseUrl` is the mount Express ACTUALLY MATCHED when it dispatched
151
- * the request. It carries no query string, it is not mount-relative, it is
152
- * unaffected by absolute-form, and it already includes the configured
153
- * `ORM_REST_ROUTE` prefix -- so there is nothing to derive and nothing to
154
- * join. Compare it lower-cased (the router matched case-insensitively) and
155
- * fail CLOSED when it is absent. Use `request.path` -- mount-relative and
156
- * query-free -- if you need to distinguish sub-paths.
155
+ * The fix is not a sixth rule, and it is not a better string to match. It is to
156
+ * stop identifying the collection at all: read `context.model`.
157
+ *
158
+ * An intermediate revision of the sample read `request.baseUrl` -- the mount
159
+ * Express ACTUALLY MATCHED. That closed all five variants (no query string,
160
+ * not mount-relative, unaffected by absolute-form, already carrying the
161
+ * configured `ORM_REST_ROUTE` prefix), but it was a transport artifact
162
+ * standing in for a structural fact and the sample no longer does it.
163
+ * `context.model` IS the structural fact, so all five variants are
164
+ * unconstructible against a migrated predicate rather than handled.
165
+ *
166
+ * ONE READ OF ARGUMENT ONE SURVIVES, AND IT MUST: `request.path`. It is
167
+ * mount-relative and query-free, and it is for rules that distinguish SUB-PATHS
168
+ * beneath the mount. The context names which model and which verb, NOT which
169
+ * route, so the sample's `/archived` deny cannot be expressed from the context
170
+ * alone and a context-ONLY rewrite would silently turn that deny into an allow.
171
+ * Lower-case it before comparing -- the router matched case-insensitively -- and
172
+ * compare record ids at their real case.
157
173
  *
158
174
  * `?? ''` is not a defence. It converts an absent request target into an empty
159
175
  * string, which matches no collection, which falls through to the permission
160
- * array -- a total grant. An input you cannot identify must DENY.
176
+ * array -- a total grant. An input you cannot identify must DENY, and that
177
+ * applies to the context too: the sample returns `false` for an absent `model`
178
+ * rather than falling through.
161
179
  *
162
- * THAT IS STILL A STOPGAP. `baseUrl` closes all five variants, but it is a
163
- * transport artifact being asked to stand in for a structural fact.
164
180
  * THE REAL FIX IS abofs/stonyx-orm#202: `access()` should receive the model,
165
181
  * the operation and the record. Prefer the array shape (`['read']`) or `false`
166
182
  * until #202 lands; the function shape is what requires any matching at all.
@@ -98,8 +98,8 @@
98
98
  *
99
99
  * PASSING THE CONTEXT MAKES A MODEL-CORRECT ANSWER POSSIBLE. It does not make
100
100
  * the answer model-correct on its own -- the resolved predicate has to READ it.
101
- * Measured against this repo's own shipped access class, on a request express
102
- * dispatched to `GET /owners/angela`, asked about ANIMALS:
101
+ * Measured against an ARITY-1 predicate, on a request express dispatched to
102
+ * `GET /owners/angela`, asked about ANIMALS:
103
103
  *
104
104
  * getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
105
105
  * -> record => record.id !== 'angela' && record.id !== 'restricted'
@@ -110,23 +110,30 @@
110
110
  * `['read', 'create', 'update', 'delete']`, a full CRUD grant. Either way the
111
111
  * context was supplied and the answer is not the animal answer, and it is wrong
112
112
  * in the GRANTING direction, because that predicate is arity-1 and identifies
113
- * its collection from the request. (The first of these is asserted on a live
114
- * dispatch by AC9 in test/integration/orm-test.ts.)
113
+ * its collection from the request. (Asserted on a live dispatch by AC9 in
114
+ * test/integration/orm-test.ts, against a deliberately arity-1 predicate.)
115
115
  *
116
- * Every predicate in this repo and in every consumer tree is arity-1 on the day
117
- * this ships, and the caller has no supported way to tell which kind it got --
118
- * the boot-time arity warning that would surface it is abofs/stonyx-orm#213.
116
+ * This repo's own sample access class has since been MIGRATED to read the
117
+ * context (abofs/stonyx-orm#222), so `getAccess('animal')` here now answers
118
+ * with the animal filter. That is not true of a consumer tree: an arity-1
119
+ * predicate keeps working -- the second argument is additive -- and the caller
120
+ * has no supported way to tell which kind it got. The boot-time arity warning
121
+ * that surfaces one is abofs/stonyx-orm#221.
119
122
  * So: pass the context, and do not treat a resolved predicate's answer as
120
123
  * model-specific until that predicate has been migrated to read the context.
121
124
  *
122
125
  * ---------------------------------------------------------------------------
123
126
  * DO NOT RECONSTRUCT THE REQUEST PATH INSIDE `access()`.
124
127
  * ---------------------------------------------------------------------------
125
- * `auth()` below hands your `access(request)` a raw transport artifact and asks
126
- * you to work out which collection it addresses. Every attempt to do that by
127
- * parsing the request target has failed OPEN. Five distinct variants of the
128
- * same three-line example have now been found, each after the previous was
129
- * fixed, by five different people:
128
+ * You do not have to. `auth()` below hands your predicate the ACCESS CONTEXT as
129
+ * argument two, and `context.model` already names the collection -- see the
130
+ * contract section above. Argument ONE is still the raw transport artifact, and
131
+ * everything from here to the end of this banner is the record of what happened
132
+ * when predicates worked the collection out from it. IT IS HISTORY, NOT
133
+ * GUIDANCE: do not write any of it into a new predicate. Every attempt to
134
+ * identify the collection by parsing the request target has failed OPEN. Five
135
+ * distinct variants of the same three-line example have now been found, each
136
+ * after the previous was fixed, by five different people:
130
137
  *
131
138
  * 1. `request.url` is mount-relative under `RestServer.mountRoute`, so a
132
139
  * prefix match against it is ALWAYS false.
@@ -145,22 +152,31 @@
145
152
  * last, and the record comes back in full. It walks past a hard
146
153
  * `return false` deny the same way.
147
154
  *
148
- * The fix is not a sixth rule. It is to stop parsing:
155
+ * The fix is not a sixth rule, and it is not a better string to match. It is to
156
+ * stop identifying the collection at all: read `context.model`.
149
157
  *
150
- * `request.baseUrl` is the mount Express ACTUALLY MATCHED when it dispatched
151
- * the request. It carries no query string, it is not mount-relative, it is
152
- * unaffected by absolute-form, and it already includes the configured
153
- * `ORM_REST_ROUTE` prefix -- so there is nothing to derive and nothing to
154
- * join. Compare it lower-cased (the router matched case-insensitively) and
155
- * fail CLOSED when it is absent. Use `request.path` -- mount-relative and
156
- * query-free -- if you need to distinguish sub-paths.
158
+ * An intermediate revision of the sample read `request.baseUrl` -- the mount
159
+ * Express ACTUALLY MATCHED. That closed all five variants (no query string,
160
+ * not mount-relative, unaffected by absolute-form, already carrying the
161
+ * configured `ORM_REST_ROUTE` prefix), but it was a transport artifact
162
+ * standing in for a structural fact and the sample no longer does it.
163
+ * `context.model` IS the structural fact, so all five variants are
164
+ * unconstructible against a migrated predicate rather than handled.
165
+ *
166
+ * ONE READ OF ARGUMENT ONE SURVIVES, AND IT MUST: `request.path`. It is
167
+ * mount-relative and query-free, and it is for rules that distinguish SUB-PATHS
168
+ * beneath the mount. The context names which model and which verb, NOT which
169
+ * route, so the sample's `/archived` deny cannot be expressed from the context
170
+ * alone and a context-ONLY rewrite would silently turn that deny into an allow.
171
+ * Lower-case it before comparing -- the router matched case-insensitively -- and
172
+ * compare record ids at their real case.
157
173
  *
158
174
  * `?? ''` is not a defence. It converts an absent request target into an empty
159
175
  * string, which matches no collection, which falls through to the permission
160
- * array -- a total grant. An input you cannot identify must DENY.
176
+ * array -- a total grant. An input you cannot identify must DENY, and that
177
+ * applies to the context too: the sample returns `false` for an absent `model`
178
+ * rather than falling through.
161
179
  *
162
- * THAT IS STILL A STOPGAP. `baseUrl` closes all five variants, but it is a
163
- * transport artifact being asked to stand in for a structural fact.
164
180
  * THE REAL FIX IS abofs/stonyx-orm#202: `access()` should receive the model,
165
181
  * the operation and the record. Prefer the array shape (`['read']`) or `false`
166
182
  * until #202 lands; the function shape is what requires any matching at all.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.3.2-alpha.60",
7
+ "version": "0.3.2-alpha.62",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "homepage": "https://github.com/abofs/stonyx-orm#readme",
63
63
  "dependencies": {
64
- "@stonyx/cron": "0.2.1-beta.84",
64
+ "@stonyx/cron": "0.2.1-beta.85",
65
65
  "@stonyx/events": "0.1.1-beta.52",
66
66
  "@stonyx/utils": "0.2.3-beta.26",
67
67
  "stonyx": "0.2.3-beta.77"
@@ -91,7 +91,7 @@
91
91
  }
92
92
  },
93
93
  "devDependencies": {
94
- "@stonyx/rest-server": "0.2.1-beta.83",
94
+ "@stonyx/rest-server": "0.2.1-beta.84",
95
95
  "@types/node": "^25.6.0",
96
96
  "mysql2": "^3.20.0",
97
97
  "pg": "^8.20.0",
@@ -98,8 +98,8 @@
98
98
  *
99
99
  * PASSING THE CONTEXT MAKES A MODEL-CORRECT ANSWER POSSIBLE. It does not make
100
100
  * the answer model-correct on its own -- the resolved predicate has to READ it.
101
- * Measured against this repo's own shipped access class, on a request express
102
- * dispatched to `GET /owners/angela`, asked about ANIMALS:
101
+ * Measured against an ARITY-1 predicate, on a request express dispatched to
102
+ * `GET /owners/angela`, asked about ANIMALS:
103
103
  *
104
104
  * getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
105
105
  * -> record => record.id !== 'angela' && record.id !== 'restricted'
@@ -110,23 +110,30 @@
110
110
  * `['read', 'create', 'update', 'delete']`, a full CRUD grant. Either way the
111
111
  * context was supplied and the answer is not the animal answer, and it is wrong
112
112
  * in the GRANTING direction, because that predicate is arity-1 and identifies
113
- * its collection from the request. (The first of these is asserted on a live
114
- * dispatch by AC9 in test/integration/orm-test.ts.)
113
+ * its collection from the request. (Asserted on a live dispatch by AC9 in
114
+ * test/integration/orm-test.ts, against a deliberately arity-1 predicate.)
115
115
  *
116
- * Every predicate in this repo and in every consumer tree is arity-1 on the day
117
- * this ships, and the caller has no supported way to tell which kind it got --
118
- * the boot-time arity warning that would surface it is abofs/stonyx-orm#213.
116
+ * This repo's own sample access class has since been MIGRATED to read the
117
+ * context (abofs/stonyx-orm#222), so `getAccess('animal')` here now answers
118
+ * with the animal filter. That is not true of a consumer tree: an arity-1
119
+ * predicate keeps working -- the second argument is additive -- and the caller
120
+ * has no supported way to tell which kind it got. The boot-time arity warning
121
+ * that surfaces one is abofs/stonyx-orm#221.
119
122
  * So: pass the context, and do not treat a resolved predicate's answer as
120
123
  * model-specific until that predicate has been migrated to read the context.
121
124
  *
122
125
  * ---------------------------------------------------------------------------
123
126
  * DO NOT RECONSTRUCT THE REQUEST PATH INSIDE `access()`.
124
127
  * ---------------------------------------------------------------------------
125
- * `auth()` below hands your `access(request)` a raw transport artifact and asks
126
- * you to work out which collection it addresses. Every attempt to do that by
127
- * parsing the request target has failed OPEN. Five distinct variants of the
128
- * same three-line example have now been found, each after the previous was
129
- * fixed, by five different people:
128
+ * You do not have to. `auth()` below hands your predicate the ACCESS CONTEXT as
129
+ * argument two, and `context.model` already names the collection -- see the
130
+ * contract section above. Argument ONE is still the raw transport artifact, and
131
+ * everything from here to the end of this banner is the record of what happened
132
+ * when predicates worked the collection out from it. IT IS HISTORY, NOT
133
+ * GUIDANCE: do not write any of it into a new predicate. Every attempt to
134
+ * identify the collection by parsing the request target has failed OPEN. Five
135
+ * distinct variants of the same three-line example have now been found, each
136
+ * after the previous was fixed, by five different people:
130
137
  *
131
138
  * 1. `request.url` is mount-relative under `RestServer.mountRoute`, so a
132
139
  * prefix match against it is ALWAYS false.
@@ -145,22 +152,31 @@
145
152
  * last, and the record comes back in full. It walks past a hard
146
153
  * `return false` deny the same way.
147
154
  *
148
- * The fix is not a sixth rule. It is to stop parsing:
155
+ * The fix is not a sixth rule, and it is not a better string to match. It is to
156
+ * stop identifying the collection at all: read `context.model`.
149
157
  *
150
- * `request.baseUrl` is the mount Express ACTUALLY MATCHED when it dispatched
151
- * the request. It carries no query string, it is not mount-relative, it is
152
- * unaffected by absolute-form, and it already includes the configured
153
- * `ORM_REST_ROUTE` prefix -- so there is nothing to derive and nothing to
154
- * join. Compare it lower-cased (the router matched case-insensitively) and
155
- * fail CLOSED when it is absent. Use `request.path` -- mount-relative and
156
- * query-free -- if you need to distinguish sub-paths.
158
+ * An intermediate revision of the sample read `request.baseUrl` -- the mount
159
+ * Express ACTUALLY MATCHED. That closed all five variants (no query string,
160
+ * not mount-relative, unaffected by absolute-form, already carrying the
161
+ * configured `ORM_REST_ROUTE` prefix), but it was a transport artifact
162
+ * standing in for a structural fact and the sample no longer does it.
163
+ * `context.model` IS the structural fact, so all five variants are
164
+ * unconstructible against a migrated predicate rather than handled.
165
+ *
166
+ * ONE READ OF ARGUMENT ONE SURVIVES, AND IT MUST: `request.path`. It is
167
+ * mount-relative and query-free, and it is for rules that distinguish SUB-PATHS
168
+ * beneath the mount. The context names which model and which verb, NOT which
169
+ * route, so the sample's `/archived` deny cannot be expressed from the context
170
+ * alone and a context-ONLY rewrite would silently turn that deny into an allow.
171
+ * Lower-case it before comparing -- the router matched case-insensitively -- and
172
+ * compare record ids at their real case.
157
173
  *
158
174
  * `?? ''` is not a defence. It converts an absent request target into an empty
159
175
  * string, which matches no collection, which falls through to the permission
160
- * array -- a total grant. An input you cannot identify must DENY.
176
+ * array -- a total grant. An input you cannot identify must DENY, and that
177
+ * applies to the context too: the sample returns `false` for an absent `model`
178
+ * rather than falling through.
161
179
  *
162
- * THAT IS STILL A STOPGAP. `baseUrl` closes all five variants, but it is a
163
- * transport artifact being asked to stand in for a structural fact.
164
180
  * THE REAL FIX IS abofs/stonyx-orm#202: `access()` should receive the model,
165
181
  * the operation and the record. Prefer the array shape (`['read']`) or `false`
166
182
  * until #202 lands; the function shape is what requires any matching at all.