@stonyx/orm 0.3.2-alpha.77 → 0.3.2-alpha.79
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 +132 -241
- package/dist/orm-request.d.ts +3 -13
- package/dist/orm-request.js +157 -161
- package/dist/types/orm-types.d.ts +4 -30
- package/package.json +1 -1
- package/src/orm-request.ts +159 -164
- package/src/types/orm-types.ts +4 -30
package/README.md
CHANGED
|
@@ -439,22 +439,11 @@ export default class GlobalAccess {
|
|
|
439
439
|
// does not remove a rule loudly, it turns a deny into an ALLOW, silently.
|
|
440
440
|
if (recordId === 'archived') return false;
|
|
441
441
|
|
|
442
|
-
// Returning a function plugs it in as a per-record filter
|
|
443
|
-
// on every surface addressed to one of these records
|
|
442
|
+
// Returning a function plugs it in as a per-record filter, and it is
|
|
443
|
+
// enforced on every surface addressed to one of these records:
|
|
444
444
|
// /owners, /owners/:id, /owners/:id/pets, /owners/:id/relationships/pets
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
// /animals/:id/owner, /animals/:id/relationships/owner
|
|
448
|
-
// Both readings are the same rule: an owner this predicate rejects is
|
|
449
|
-
// withheld wherever she is reachable, not only on /owners.
|
|
450
|
-
//
|
|
451
|
-
// NOTHING HERE IS AN EXISTENCE ORACLE, AND THE SPELLING DIFFERS BY WHOSE
|
|
452
|
-
// RECORD IS BEING REJECTED. A rejected ADDRESSED record is 404 — the same
|
|
453
|
-
// status as a record that does not exist. A rejected RELATED record is
|
|
454
|
-
// `data: null` at 200 — byte-identical to a relationship that is
|
|
455
|
-
// genuinely empty, because on those routes 404 is already the answer for
|
|
456
|
-
// a PARENT that does not exist. In both cases "rejected" and "not there"
|
|
457
|
-
// are the same answer, which is the property that matters.
|
|
445
|
+
// A rejected record is 404 on record routes — the same status as a record
|
|
446
|
+
// that does not exist — so the filter is not an existence oracle.
|
|
458
447
|
return record => record.id !== 'angela' && record.id !== 'restricted';
|
|
459
448
|
}
|
|
460
449
|
|
|
@@ -463,11 +452,7 @@ export default class GlobalAccess {
|
|
|
463
452
|
// inert. Deliberately NO `?? record.owner` fallback: accepting the raw
|
|
464
453
|
// shape as well as the resolved one would absorb a resolution regression
|
|
465
454
|
// silently, which is exactly what blinded this fixture before.
|
|
466
|
-
|
|
467
|
-
// fixture that makes the `hasMany` half of the relationship-route rules
|
|
468
|
-
// observable: gina is served, animal 18 is not, and every surface that
|
|
469
|
-
// names gina's pets has to drop it.
|
|
470
|
-
if (model === 'animal') return record => record.owner?.id !== 'restricted' && record.id !== 18;
|
|
455
|
+
if (model === 'animal') return record => record.owner?.id !== 'restricted';
|
|
471
456
|
|
|
472
457
|
// Allows full access to all calls that don't match any of the above conditions
|
|
473
458
|
return ['read', 'create', 'update', 'delete'];
|
|
@@ -680,11 +665,9 @@ A `throw` inside `access()` is a **denial**, not a 500.
|
|
|
680
665
|
A function return value is a **per-record predicate**, and it is enforced on
|
|
681
666
|
every endpoint that is addressed to a record — not only on the collection.
|
|
682
667
|
|
|
683
|
-
It is evaluated against the record the route is *addressed to
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
rows below. It is not a guarantee that a hidden record cannot be reached or
|
|
687
|
-
modified: a write to a *different* collection can still re-parent one. See
|
|
668
|
+
It is evaluated against the record the route is *addressed to*, **on that model
|
|
669
|
+
only**. It is not a guarantee that a hidden record cannot be reached or modified:
|
|
670
|
+
a write to a *different* collection can still re-parent one. See
|
|
688
671
|
[Known limitations](#known-limitations) and
|
|
689
672
|
[#207](https://github.com/abofs/stonyx-orm/issues/207).
|
|
690
673
|
|
|
@@ -692,42 +675,19 @@ modified: a write to a *different* collection can still re-parent one. See
|
|
|
692
675
|
|---|---|
|
|
693
676
|
| `GET /:models` | omitted from the collection |
|
|
694
677
|
| `GET /:models/:id` | `404` |
|
|
695
|
-
| `GET /:models/:id/{relationship}` |
|
|
696
|
-
| `GET /:models/:id/relationships/{relationship}` |
|
|
678
|
+
| `GET /:models/:id/{relationship}` | `404` — the **addressed** record is filtered, not the related one |
|
|
679
|
+
| `GET /:models/:id/relationships/{relationship}` | `404` — same |
|
|
697
680
|
| `PATCH /:models/:id` | `404`, no attribute is applied |
|
|
698
681
|
| `DELETE /:models/:id` | `404`, the record is not removed and no SQL `DELETE` is issued |
|
|
699
682
|
| `POST /:models` | `403`, and a record **this request inserted** is rolled back — see [Known limitations](#known-limitations) for why the rollback is conditional |
|
|
700
683
|
|
|
701
|
-
**A withheld related record is not an error, and that is the same rule.** The
|
|
702
|
-
addressed record is filtered with `404` and the related one with `data: null`
|
|
703
|
-
because in both cases the answer must be **identical to the answer for a record
|
|
704
|
-
that does not exist**. A `belongsTo` whose target is genuinely absent already
|
|
705
|
-
answers `200 {"data": null}`; a `hasMany` with no members already answers `200`
|
|
706
|
-
with an empty array. Withholding therefore has to be spelled the same way, or
|
|
707
|
-
the route becomes an existence oracle for a record on a collection the caller
|
|
708
|
-
may have no access to at all. Measured before this was closed — unauthenticated,
|
|
709
|
-
zero query parameters, one request each:
|
|
710
|
-
|
|
711
|
-
```
|
|
712
|
-
GET /traits/1/tag [target absent] -> 200 application/json 68 bytes
|
|
713
|
-
GET /traits/2/tag [target denied] -> 404 text/plain 9 bytes
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
`tag` is a model with **no route mounted at all**, so those two requests were the
|
|
717
|
-
only way to ask about it — and they answered differently. Both now answer
|
|
718
|
-
`200 {"data": null}`.
|
|
719
|
-
|
|
720
684
|
**Denied record-level requests return 404, not 403.** This is deliberate and it
|
|
721
685
|
is the property most easily "improved" away. 403 would confirm that the record
|
|
722
686
|
exists to a caller who is not allowed to know that, which turns the filter into
|
|
723
687
|
an existence oracle: `404` means "no such record", `403` means "there is one and
|
|
724
688
|
it is not yours". Every status on a record route must therefore be identical for
|
|
725
689
|
"filtered out" and "does not exist" — including `DELETE`, which is why deleting
|
|
726
|
-
a record that never existed also returns 404 rather than 204
|
|
727
|
-
**related** record on the two relationship families, which is why a denied
|
|
728
|
-
`belongsTo` target is `200 {"data": null}` rather than `404`: on that route
|
|
729
|
-
`404` is the answer for a parent that does not exist, so it is `data: null`, and
|
|
730
|
-
not the status, that carries "no target you may see".
|
|
690
|
+
a record that never existed also returns 404 rather than 204.
|
|
731
691
|
|
|
732
692
|
`POST` is the one exception and returns **403**, because 404 on a mounted
|
|
733
693
|
collection route is indistinguishable from "model not mounted" — a genuinely
|
|
@@ -961,117 +921,59 @@ per-record filter. An input you cannot identify must **deny**.
|
|
|
961
921
|
operation and which record the request addresses. The five variants above are
|
|
962
922
|
the five ways that has been observed to fail open so far. Tracked as
|
|
963
923
|
[#202](https://github.com/abofs/stonyx-orm/issues/202).
|
|
964
|
-
- **
|
|
965
|
-
|
|
966
|
-
`GET
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
Before this, both families served a record hidden on every one of its own
|
|
981
|
-
surfaces, in full, from another model's route, at **zero query parameters**.
|
|
982
|
-
The severe case is a model **claimed by no access class**: `getAccess()`
|
|
983
|
-
returns `undefined`, no route is mounted for it at all, and it was still
|
|
984
|
-
readable as a related resource — a collection the consumer deliberately never
|
|
985
|
-
exposed.
|
|
986
|
-
|
|
987
|
-
**The `belongsTo` shape is not an existence oracle, and it was measured
|
|
988
|
-
rather than reasoned about.** An earlier revision of this fix answered `404`
|
|
989
|
-
for a denied target, which made it distinguishable from a target that is
|
|
990
|
-
genuinely absent. Unauthenticated, zero query parameters, one request each, on
|
|
991
|
-
`tag` — a model with **no route mounted at all**:
|
|
992
|
-
|
|
993
|
-
```
|
|
994
|
-
GET /traits/1/tag [target absent] -> 200 application/json 68 bytes
|
|
995
|
-
GET /traits/2/tag [target denied] -> 404 text/plain 9 bytes
|
|
996
|
-
```
|
|
997
|
-
|
|
998
|
-
`GET /traits/1` and `GET /traits/2` report `relationships.tag = {"data":null}`
|
|
999
|
-
byte-identical modulo the id, because
|
|
1000
|
-
[#234](https://github.com/abofs/stonyx-orm/issues/234) closed that oracle
|
|
1001
|
-
deliberately — so this route was the one remaining way to ask which of those
|
|
1002
|
-
two nulls was a denial. Under `data: null` both requests answer `200`, same
|
|
1003
|
-
content-type, same content-length, same bytes modulo the parent id the caller
|
|
1004
|
-
put in the URL. It discloses nothing further: `links` on these routes are
|
|
1005
|
-
derived entirely from the parent and the relationship name, there is no `meta`
|
|
1006
|
-
and there are no counts. This also brings the two families back into line with
|
|
1007
|
-
the module-wide rule under [Filter functions](#filter-functions) — *every
|
|
1008
|
-
status on a record route must be identical for "filtered out" and "does not
|
|
1009
|
-
exist"* — which the `404` spelling was an exception to.
|
|
1010
|
-
|
|
1011
|
-
**Per-record denies for a related resource are not expressible.** A predicate resolved for a
|
|
1012
|
-
related resource on these routes receives `recordId: null` and a `request`
|
|
1013
|
-
whose `params` name a record of a **different model**. So the inputs it has
|
|
1014
|
-
are the model name, the operation and the request — and **a rule that needs to
|
|
1015
|
-
know *which* related record it is being asked about cannot be written**.
|
|
1016
|
-
Model-level denies (`return false` for a model) work. Request-level denies (a
|
|
1017
|
-
rule reading a header, a tenant, the method) work. The per-record **filter**
|
|
1018
|
-
shape works too — `access()` may return a function, and that function receives
|
|
1019
|
-
the whole record, id included. What does not work is branching on the record's
|
|
1020
|
-
identity *before* returning, because `access()` is not told it.
|
|
1021
|
-
|
|
1022
|
-
This is not an oversight and it is not closed here. The verdict is resolved
|
|
1023
|
-
**once per type**, cached, before any record has been examined — a `hasMany`
|
|
1024
|
-
related-resource route returns many records of one type, so seeding `recordId`
|
|
1025
|
-
from a record would let the first one decide the context for all of them. The
|
|
1026
|
-
rule the framework holds to is: **`recordId` may name a record only where the
|
|
1027
|
-
route addresses exactly one record of the model being asked about.** That is
|
|
1028
|
-
true for `GET /owners/{id}`, false for linkage, and false for a `hasMany`
|
|
1029
|
-
related-resource route.
|
|
1030
|
-
|
|
1031
|
-
- **`?include=` records are still not filtered — the relationship routes now
|
|
1032
|
-
are.** *Re-specified by [#232](https://github.com/abofs/stonyx-orm/issues/232);
|
|
1033
|
-
the sentence this replaces said all three surfaces were unfiltered, and two of
|
|
1034
|
-
them no longer are.* `GET /animals/1/owner` and
|
|
1035
|
-
`GET /animals/1/relationships/owner` resolve the related model's own access
|
|
1036
|
-
class (see the bullet above). **`?include=owner` still does not**: it
|
|
1037
|
-
serializes the related record without resolving that class, so a filter on
|
|
1038
|
-
`/owners` does not hide an owner reached through `?include=` on `/animals`.
|
|
1039
|
-
There are **two** open questions here and they are owned separately —
|
|
1040
|
-
[#233](https://github.com/abofs/stonyx-orm/issues/233), the remaining child of
|
|
1041
|
-
[#196](https://github.com/abofs/stonyx-orm/issues/196), owns whether a
|
|
1042
|
-
resource enters `included` **at all** (membership), and
|
|
1043
|
-
[#235](https://github.com/abofs/stonyx-orm/issues/235) owns the
|
|
1044
|
-
`relationships.*.data` emitted **inside** a record that is already there
|
|
1045
|
-
(linkage). Neither closes the other, and following only #233 will not lead you
|
|
1046
|
-
to the second. Membership — whether the related resource is served at all — is
|
|
1047
|
-
a different question from which ids a document may *name*, immediately
|
|
1048
|
-
below.
|
|
1049
|
-
- **Relationship linkage is filtered on the four request-bound read surfaces,
|
|
1050
|
-
and only there.** A document's `relationships.*.data` used to publish the id
|
|
1051
|
-
of every related record unconditionally, so a record hidden on every one of
|
|
1052
|
-
its own surfaces was still named inside another model's document — with no
|
|
1053
|
-
`include=`, no relationship route and no query string
|
|
924
|
+
- **Related and included records are not filtered.** The predicate is evaluated
|
|
925
|
+
against the record the route is *addressed to*. `GET /animals/1/owner`,
|
|
926
|
+
`GET /animals/1/relationships/owner` and `?include=owner` all serialize the
|
|
927
|
+
related record without resolving that model's own access class, so a filter on
|
|
928
|
+
`/owners` does not hide an owner reached through `/animals`. Tracked as
|
|
929
|
+
[#196](https://github.com/abofs/stonyx-orm/issues/196), which covers
|
|
930
|
+
`include=`, related-resource routes and relationship-linkage routes. This is
|
|
931
|
+
**membership** — whether the related resource is served at all — and it is a
|
|
932
|
+
different question from which ids a document may *name*, immediately below.
|
|
933
|
+
- **Relationship linkage is filtered on every request-bound surface that
|
|
934
|
+
serializes a record — the reads, the two writes, and `included`.** A
|
|
935
|
+
document's `relationships.*.data` used to publish the id of every related
|
|
936
|
+
record unconditionally, so a record hidden on every one of its own surfaces
|
|
937
|
+
was still named inside another model's document — with no `include=`, no
|
|
938
|
+
relationship route and no query string
|
|
1054
939
|
([#234](https://github.com/abofs/stonyx-orm/issues/234)). The ORM now resolves
|
|
1055
|
-
the **related** model's own access class on `GET /:models`, `GET /:models/:id
|
|
1056
|
-
|
|
1057
|
-
`
|
|
1058
|
-
|
|
940
|
+
the **related** model's own access class on `GET /:models`, `GET /:models/:id`,
|
|
941
|
+
both `GET /:models/:id/{relationship}` shapes, the `POST /:models` and
|
|
942
|
+
`PATCH /:models/:id` **response documents**, and every record inside an
|
|
943
|
+
`?include=` **`included`** array
|
|
944
|
+
([#235](https://github.com/abofs/stonyx-orm/issues/235)), and asks it
|
|
945
|
+
`{ model: <related>, operation: 'read' }`. **`operation` is `'read'` even on a
|
|
946
|
+
write route, and that is correct rather than an oversight** — the question
|
|
947
|
+
asked of the *related* model is "may this caller **read** this id", not "may
|
|
948
|
+
they update it". An access class that grants `['create']` but not `['read']`
|
|
949
|
+
on the related model therefore denies that linkage on its own `POST`
|
|
950
|
+
response; that is the fail-closed direction. Do **not** wire these handlers to
|
|
951
|
+
`methodAccessMap[request.method]`: it would ask a different question on a
|
|
952
|
+
write route than on a read route, which is the two-vocabularies failure
|
|
953
|
+
`createLinkageFilter` exists to prevent. An unresolvable class
|
|
954
|
+
(`getAccess()` → `undefined`) and a predicate that throws both **deny**.
|
|
955
|
+
|
|
956
|
+
**What the two write surfaces cost before #235, measured rather than
|
|
957
|
+
described:** one HTTP verb defeated the filter on the same record. On
|
|
958
|
+
`dev @ 8dda5d6`, seconds apart, with no query string and no relationship
|
|
959
|
+
route, `GET /animals/1` returned `owner.data: null` while `PATCH /animals/1`
|
|
960
|
+
returned **200 naming angela**. Any caller who could read a record could also
|
|
961
|
+
write it and be handed the id the read withheld. That consequence is kept here
|
|
962
|
+
after the fix, and stated as a measurement, because **naming the two handlers
|
|
963
|
+
is not a substitute for it** — a reader who is told only that `POST` and
|
|
964
|
+
`PATCH` are now covered cannot tell what was wrong, and a reviewer cannot tell
|
|
965
|
+
whether the fix addressed it. A
|
|
1059
966
|
filtered-out relationship is **indistinguishable from a genuinely empty one** —
|
|
1060
967
|
an emptied `hasMany` is `data: []` and an emptied `belongsTo` is `data: null`,
|
|
1061
968
|
both **keeping their `links`**, which are built from the serialized record's
|
|
1062
|
-
own id and never from the related one.
|
|
969
|
+
own id and never from the related one. On the two **write** surfaces there are
|
|
970
|
+
no `links` to keep: neither handler passes a `baseUrl`, so a filtered and a
|
|
971
|
+
genuinely-empty relationship are both a bare `{ "data": … }` there. That is
|
|
972
|
+
pre-existing and deliberate — adding `baseUrl` to the write handlers would be
|
|
973
|
+
an unrelated change to their response shape. Nothing errors and no status changes,
|
|
1063
974
|
because throwing here would be an existence oracle *and* would throw out of
|
|
1064
975
|
the enclosing `JSON.stringify`.
|
|
1065
976
|
|
|
1066
|
-
**[#232](https://github.com/abofs/stonyx-orm/issues/232) holds to the same
|
|
1067
|
-
spelling on the routes where that linkage is the *primary* data.** A denied
|
|
1068
|
-
member is dropped from the `hasMany` array and a denied `belongsTo` target is
|
|
1069
|
-
`data: null`, at `200`, `links` intact — so the claim above is true of both
|
|
1070
|
-
`GET /:models/:id/{relationship}` shapes as *routes* and not only as linkage
|
|
1071
|
-
emitted inside somebody else's document. An earlier revision of #232 answered
|
|
1072
|
-
`404` on the `belongsTo` shape and did contradict this paragraph; that is
|
|
1073
|
-
measured and closed in the #232 bullet above.
|
|
1074
|
-
|
|
1075
977
|
**That resolves the right class; it does not guarantee a model-correct
|
|
1076
978
|
answer, and the failure direction is not the safe one.** Only a predicate that
|
|
1077
979
|
*reads* `context.model` can answer about the model it was asked about — see
|
|
@@ -1092,56 +994,50 @@ per-record filter. An input you cannot identify must **deny**.
|
|
|
1092
994
|
*permitted* related record, which is recorded in the release notes as a
|
|
1093
995
|
breaking change.
|
|
1094
996
|
|
|
1095
|
-
**Not yet covered
|
|
1096
|
-
withhold
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
- **`
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
997
|
+
**Not yet covered by #235. Each still publishes ids the surfaces above
|
|
998
|
+
withhold, except where its own owning issue has since closed it — the first
|
|
999
|
+
entry names an issue that is in flight as this is written:**
|
|
1000
|
+
|
|
1001
|
+
- **`GET /:models/:id/relationships/{relationship}`, and its state is #232's
|
|
1002
|
+
to report rather than this entry's.**
|
|
1003
|
+
[#232](https://github.com/abofs/stonyx-orm/issues/232) owns the
|
|
1004
|
+
relationships-linkage route. Its *primary data* is linkage,
|
|
1005
|
+
so filtering it is a **membership** decision — which is why it is the filed
|
|
1006
|
+
child of [#196](https://github.com/abofs/stonyx-orm/issues/196) and not of
|
|
1007
|
+
#234. The route builds its `{type, id}` objects by hand and never calls
|
|
1008
|
+
`toJSON`, so the `linkage` **option** never reaches it; whatever that route
|
|
1009
|
+
filters, it filters itself. Measured **on `dev @ 8dda5d6`**, the commit
|
|
1010
|
+
#235 branched from: `GET /animals/1/relationships/owner` answered
|
|
1011
|
+
`{"type":"owner","id":"angela"}` while `GET /owners/angela` was `404`. That
|
|
1012
|
+
measurement is pinned to a commit on purpose, so that it does not quietly
|
|
1013
|
+
become a false claim about `dev`. **PR
|
|
1014
|
+
[#247](https://github.com/abofs/stonyx-orm/pull/247) is in flight against
|
|
1015
|
+
this entry**; if it has landed, this route is covered and the bullet #247
|
|
1016
|
+
adds above supersedes this one.
|
|
1017
|
+
- **Whether a related resource appears in `included` at all.**
|
|
1105
1018
|
[#233](https://github.com/abofs/stonyx-orm/issues/233) owns whether a
|
|
1106
|
-
resource appears in `included
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
`
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
is
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
- **The absence of `attributes.<fk>` on a `POST` response proves a hidden
|
|
1129
|
-
record exists** — [#246](https://github.com/abofs/stonyx-orm/issues/246).
|
|
1130
|
-
`createHandler` copies each supplied relationship's raw id into
|
|
1131
|
-
`attributes`; when the related record resolves, the value is consumed and
|
|
1132
|
-
does not appear, and when it does not resolve, it survives. The oracle runs
|
|
1133
|
-
in the negative space, so nothing this list's surfaces withhold is
|
|
1134
|
-
*published* — the **absence** is the signal. Pre-existing, and it does not
|
|
1135
|
-
compose with the two relationship families above: they emit no `attributes`
|
|
1136
|
-
for a related record at all.
|
|
1137
|
-
- ~~**`GET /:models/:id/relationships/{relationship}`**~~ — **covered as of
|
|
1138
|
-
[#232](https://github.com/abofs/stonyx-orm/issues/232)**, together with
|
|
1139
|
-
`GET /:models/:id/{relationship}`. Left in place rather than deleted because
|
|
1140
|
-
this list is what a reader consults to find out what is *not* covered, and a
|
|
1141
|
-
silently vanishing entry reads as an entry that was never there. Its
|
|
1142
|
-
*primary data* is linkage, so filtering it was a **membership** decision —
|
|
1143
|
-
which is why it belonged to #232 and not to
|
|
1144
|
-
[#234](https://github.com/abofs/stonyx-orm/issues/234).
|
|
1019
|
+
related resource appears in `included`. #235 filters what a record
|
|
1020
|
+
*already in* `included` may **name**; a hidden record is still a
|
|
1021
|
+
**member** of that array. The two are different questions and neither closes
|
|
1022
|
+
the other: after #235, `GET /animals/1?include=owner,owner.pets` returns
|
|
1023
|
+
`owner.data: null` on every permitted animal it sideloads **and still
|
|
1024
|
+
includes the hidden owner as a resource**.
|
|
1025
|
+
- **A computed attribute that interpolates a related record's id.**
|
|
1026
|
+
[#245](https://github.com/abofs/stonyx-orm/issues/245) owns this channel,
|
|
1027
|
+
and **it is open as this is written**. `relationships.*.data` is a structure
|
|
1028
|
+
this module builds, so it can be filtered; a computed property is arbitrary
|
|
1029
|
+
consumer code returning an arbitrary value. Whether that makes the channel a
|
|
1030
|
+
**framework defect** the ORM should close — by handing computed getters a
|
|
1031
|
+
verdict, or by refusing to run them while a filter is in force — or a
|
|
1032
|
+
**consumer contract** the ORM should only document, is the question #245
|
|
1033
|
+
must decide. **This README does not decide it; neither reading should be
|
|
1034
|
+
read out of the text here.** Measured on this repo's own fixture, where the
|
|
1035
|
+
`animal` model has a `get tag()` that interpolates `owner.id`: **every**
|
|
1036
|
+
animal document on **every** surface — including the ones above — carries
|
|
1037
|
+
`attributes.tag: "angela's small dog"` for an owner that answers `404`. That
|
|
1038
|
+
measurement is where #245 starts, and it holds whichever way the decision
|
|
1039
|
+
lands. Until it lands, if your access rules hide a record, audit your
|
|
1040
|
+
computed properties for its identifiers.
|
|
1145
1041
|
- **A bare `toJSON()` still emits unfiltered linkage, and that is deliberate.**
|
|
1146
1042
|
`Record.toJSON()` **applies** a verdict; it never **resolves** one. It has no
|
|
1147
1043
|
request, and the documented `access()` contract permits a predicate to read
|
|
@@ -1235,11 +1131,29 @@ read all of these.
|
|
|
1235
1131
|
|
|
1236
1132
|
#### `Record.toJSON()` does not filter relationship linkage unless you pass a verdict
|
|
1237
1133
|
|
|
1238
|
-
**The framework
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1134
|
+
**The framework resolves a verdict for you on every request-bound surface that
|
|
1135
|
+
serializes a record through `toJSON()`. You own it everywhere else.**
|
|
1136
|
+
|
|
1137
|
+
Those surfaces are `GET /:models`, `GET /:models/:id`, both shapes of
|
|
1138
|
+
`GET /:models/:id/{relationship}`, the `POST /:models` and `PATCH /:models/:id`
|
|
1139
|
+
**response documents**, and every record inside an `?include=` **`included`**
|
|
1140
|
+
array ([#234](https://github.com/abofs/stonyx-orm/issues/234) for the four
|
|
1141
|
+
reads, [#235](https://github.com/abofs/stonyx-orm/issues/235) for the two
|
|
1142
|
+
writes and `included`). Each resolves a linkage verdict and passes it to
|
|
1143
|
+
`toJSON()` for you.
|
|
1144
|
+
|
|
1145
|
+
**`GET /:models/:id/relationships/{relationship}` is not on that list, and its
|
|
1146
|
+
state is not this section's to report.** It builds its `{ type, id }` objects by
|
|
1147
|
+
hand instead of calling `toJSON()`, so the `linkage` **option** never reaches it
|
|
1148
|
+
— whatever that route filters, it filters itself. And because its linkage *is*
|
|
1149
|
+
its primary data, filtering it is a **membership** decision rather than a
|
|
1150
|
+
linkage one. Membership on both relationship route families is owned by
|
|
1151
|
+
[#232](https://github.com/abofs/stonyx-orm/issues/232) (PR
|
|
1152
|
+
[#247](https://github.com/abofs/stonyx-orm/pull/247), in flight as this is
|
|
1153
|
+
written); read that issue for its state rather than inferring it here, because
|
|
1154
|
+
this section describes only what `toJSON()` filters.
|
|
1155
|
+
|
|
1156
|
+
Any other path to a document — `JSON.stringify(record)`, `res.json(record)`,
|
|
1243
1157
|
`console.log(record)`, a custom route, a queue payload, a websocket frame —
|
|
1244
1158
|
calls `toJSON()` with no verdict, and **the no-verdict document names every
|
|
1245
1159
|
related id, including records hidden on every one of their own surfaces**
|
|
@@ -1252,31 +1166,6 @@ default, the default is the unfiltered document, and a filtered relationship is
|
|
|
1252
1166
|
byte-identical to a genuinely empty one — so nothing on the wire distinguishes
|
|
1253
1167
|
"filtered" from "forgotten".
|
|
1254
1168
|
|
|
1255
|
-
**And `linkage` cannot reach a document you build by hand.** It is an *option to
|
|
1256
|
-
`toJSON()`*, so it filters only what goes through `toJSON()`. The ORM's own
|
|
1257
|
-
`GET /:models/:id/relationships/{relationship}` route is the worked example: its
|
|
1258
|
-
primary data *is* linkage, it assembles `{ type, id }` directly rather than
|
|
1259
|
-
serializing a record, and it therefore resolves and applies the verdict itself
|
|
1260
|
-
([#232](https://github.com/abofs/stonyx-orm/issues/232)). If you assemble
|
|
1261
|
-
linkage the same way anywhere — a custom relationship route, a projection, a
|
|
1262
|
-
hand-built document — **passing `linkage` to `toJSON()` does nothing for it and
|
|
1263
|
-
nothing warns**. Build the filter and consult it before you emit an id:
|
|
1264
|
-
|
|
1265
|
-
```js
|
|
1266
|
-
const linkage = createLinkageFilter(request);
|
|
1267
|
-
|
|
1268
|
-
if (related && linkage(related.__model.__name, related)) {
|
|
1269
|
-
data = { type: related.__model.__name, id: related.id };
|
|
1270
|
-
} else {
|
|
1271
|
-
data = null; // withheld and genuinely-empty must be the SAME answer
|
|
1272
|
-
}
|
|
1273
|
-
```
|
|
1274
|
-
|
|
1275
|
-
The `else` branch is the part that is easy to get wrong. Answering `404`, `403`
|
|
1276
|
-
or an `errors` member for the withheld case makes the route an **existence
|
|
1277
|
-
oracle** — see [Filter functions](#filter-functions) for the rule and for the
|
|
1278
|
-
measurement that closed it on this route.
|
|
1279
|
-
|
|
1280
1169
|
Do this:
|
|
1281
1170
|
|
|
1282
1171
|
```js
|
|
@@ -1564,13 +1453,15 @@ GET /animals/1
|
|
|
1564
1453
|
#### Limitations
|
|
1565
1454
|
|
|
1566
1455
|
- Only available on GET endpoints (not POST/PATCH)
|
|
1567
|
-
- **`included`
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
[
|
|
1456
|
+
- **`included` is access-filtered on one of the two questions, not both.** What
|
|
1457
|
+
a record already in `included` may **name** in its own
|
|
1458
|
+
`relationships.*.data` is filtered
|
|
1459
|
+
([#235](https://github.com/abofs/stonyx-orm/issues/235)) — `?include=` no
|
|
1460
|
+
longer republishes ids the primary document withholds. Whether a resource
|
|
1461
|
+
appears in `included` **at all** is *membership* and is still unfiltered
|
|
1462
|
+
([#233](https://github.com/abofs/stonyx-orm/issues/233)): a record that is
|
|
1463
|
+
404 on its own routes is still served as an `included` resource, attributes
|
|
1464
|
+
and all. See [Consumer Contracts](#consumer-contracts).
|
|
1574
1465
|
|
|
1575
1466
|
## Lifecycle Hooks
|
|
1576
1467
|
|
package/dist/orm-request.d.ts
CHANGED
|
@@ -65,19 +65,9 @@
|
|
|
65
65
|
* warning below sanctions. This repo's own fixture has such a rule: its
|
|
66
66
|
* `/archived` deny cannot be expressed from the context alone, and a predicate
|
|
67
67
|
* migrated to context-only would silently drop it, turning a deny into an
|
|
68
|
-
* allow.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* CORRECTED HERE RATHER THAN DELETED. Both relationship route families resolve
|
|
72
|
-
* the RELATED model's own access class and ask it
|
|
73
|
-
* `{ model: <related>, operation: 'read', recordId: null }`
|
|
74
|
-
* (abofs/stonyx-orm#232), so those surfaces no longer serve another model's
|
|
75
|
-
* records under `model: 'owner'` unexamined. What the context still gives no
|
|
76
|
-
* signal of is WHICH related record is being asked about -- `recordId` is
|
|
77
|
-
* `null` there and `request.params` names a record of a different model. See
|
|
78
|
-
* `AccessContext.recordId` in ./types/orm-types.ts for the full statement of
|
|
79
|
-
* that limit. `?include=` is still unfiltered and is abofs/stonyx-orm#233 /
|
|
80
|
-
* #235.
|
|
68
|
+
* allow. The related-resource and `?include=` surfaces serve ANOTHER model's
|
|
69
|
+
* records under `model: 'owner'`, and the context gives no signal of that
|
|
70
|
+
* (abofs/stonyx-orm#196).
|
|
81
71
|
*
|
|
82
72
|
* SUPERSEDED 2026-09-01 BY abofs/stonyx-orm#236/#237, AND KEPT FOR THE
|
|
83
73
|
* CONSTRAINT IT STATES RATHER THAN AS A DESCRIPTION OF THE CODE. The context
|