@stonyx/orm 0.3.2-alpha.88 → 0.3.2-alpha.89
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 +92 -25
- package/dist/orm-request.d.ts +12 -2
- package/dist/orm-request.js +22 -6
- package/package.json +1 -1
- package/src/orm-request.ts +22 -6
package/README.md
CHANGED
|
@@ -985,10 +985,13 @@ per-record filter. An input you cannot identify must **deny**.
|
|
|
985
985
|
"indistinguishable" as a property of the response — it is a property of the
|
|
986
986
|
relationship member alone. The other channels in the same class are
|
|
987
987
|
[#245](https://github.com/abofs/stonyx-orm/issues/245) (computed attributes,
|
|
988
|
-
which is the one measured above)
|
|
989
|
-
[#
|
|
990
|
-
|
|
991
|
-
|
|
988
|
+
which is the one measured above) and
|
|
989
|
+
[#246](https://github.com/abofs/stonyx-orm/issues/246) (the
|
|
990
|
+
`attributes.<fk>` echo of an unresolved `belongsTo` target) — **both still
|
|
991
|
+
open**. `included` membership was the third,
|
|
992
|
+
[#233](https://github.com/abofs/stonyx-orm/issues/233), and it is **closed**:
|
|
993
|
+
a related record its own model's access class rejects is dropped at the
|
|
994
|
+
traversal's push site and is no longer a member. **Audit your
|
|
992
995
|
computed properties before you treat a dropped member as unobservable.** Nothing on either family errors and no status changes — the
|
|
993
996
|
status on these routes belongs to the **parent**, and `data` carries the
|
|
994
997
|
answer about the related record. The `/relationships/` family built its `{type, id}` by
|
|
@@ -1046,24 +1049,35 @@ per-record filter. An input you cannot identify must **deny**.
|
|
|
1046
1049
|
true for `GET /owners/{id}`, false for linkage, and false for a `hasMany`
|
|
1047
1050
|
related-resource route.
|
|
1048
1051
|
|
|
1049
|
-
- **`?include=` records are
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1052
|
+
- **`?include=` records are filtered on both questions now, and so are the
|
|
1053
|
+
relationship routes.** *Re-specified twice, each time by the story that
|
|
1054
|
+
falsified it, and recorded here rather than deleted. The original said all
|
|
1055
|
+
three surfaces were unfiltered.
|
|
1056
|
+
[#232](https://github.com/abofs/stonyx-orm/issues/232) made two of them
|
|
1057
|
+
filtered and the bullet became "**`?include=` records are still not
|
|
1058
|
+
filtered — the relationship routes now are**", with the body "**`?include=owner`
|
|
1059
|
+
still does not**: it serializes the related record without resolving that
|
|
1060
|
+
class, so a filter on `/owners` does not hide an owner reached through
|
|
1061
|
+
`?include=` on `/animals`."
|
|
1062
|
+
[#233](https://github.com/abofs/stonyx-orm/issues/233) falsified that half
|
|
1063
|
+
too.* `GET /animals/1/owner` and
|
|
1053
1064
|
`GET /animals/1/relationships/owner` resolve the related model's own access
|
|
1054
|
-
class (see the bullet above). **`?include=owner`
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1065
|
+
class (see the bullet above). **`?include=owner` now resolves it as well**,
|
|
1066
|
+
at the traversal's push site, so a filter on `/owners` *does* hide an owner
|
|
1067
|
+
reached through `?include=` on `/animals`: measured on this branch,
|
|
1068
|
+
`GET /animals/1?include=owner` answers `200` with **no `included` array at
|
|
1069
|
+
all**, where before it served the hidden owner's full document.
|
|
1070
|
+
There were **two** questions here and they were owned separately — #233, the
|
|
1071
|
+
remaining child of
|
|
1059
1072
|
[#196](https://github.com/abofs/stonyx-orm/issues/196), owns whether a
|
|
1060
1073
|
resource enters `included` **at all** (membership), and
|
|
1061
1074
|
[#235](https://github.com/abofs/stonyx-orm/issues/235) owns the
|
|
1062
1075
|
`relationships.*.data` emitted **inside** a record that is already there
|
|
1063
|
-
(linkage).
|
|
1064
|
-
|
|
1065
|
-
a
|
|
1066
|
-
|
|
1076
|
+
(linkage). **Both have now landed, and neither closed the other** — they are
|
|
1077
|
+
still answered by different mechanisms at different sites, and a resource can
|
|
1078
|
+
legitimately be a member while its own linkage is filtered. Membership —
|
|
1079
|
+
whether the related resource is served at all — remains a different question
|
|
1080
|
+
from which ids a document may *name*, immediately below.
|
|
1067
1081
|
- **Relationship linkage is filtered on every request-bound surface that
|
|
1068
1082
|
serializes a record — the reads, the two writes, and `included`.** A
|
|
1069
1083
|
document's `relationships.*.data` used to publish the id of every related
|
|
@@ -1124,7 +1138,8 @@ per-record filter. An input you cannot identify must **deny**.
|
|
|
1124
1138
|
answered by different mechanisms and neither closes the other — a resource
|
|
1125
1139
|
can legitimately be a member while its own linkage is filtered. A related
|
|
1126
1140
|
resource is judged by **its own** model's access class at the traversal's
|
|
1127
|
-
push site, so a record
|
|
1141
|
+
push site, so a record that class's **per-record filter** rejects is not a
|
|
1142
|
+
member, and **the
|
|
1128
1143
|
subtree beneath it is never traversed**: dropping a parent *after* descending
|
|
1129
1144
|
through it would publish that parent's exact child set. Measured on
|
|
1130
1145
|
`dev @ c106cf9`, `GET /animals/1?include=owner,owner.pets` returned nine
|
|
@@ -1138,6 +1153,37 @@ per-record filter. An input you cannot identify must **deny**.
|
|
|
1138
1153
|
top-level keys, no `included` member on either, no `errors` — so its absence
|
|
1139
1154
|
carries no signal about whether the record exists.
|
|
1140
1155
|
|
|
1156
|
+
**Read "per-record filter" literally — it is not "everything the
|
|
1157
|
+
record-addressed route refuses", and the difference is measurable.** The
|
|
1158
|
+
linkage ask carries `recordId: null`, so a deny expressed as a
|
|
1159
|
+
request-scoped `return false` cannot fire on this path at all. The shipped
|
|
1160
|
+
sample expresses the `archived` owner's deny that way, and measured
|
|
1161
|
+
unauthenticated on this branch — byte-identically on `dev @ b23cfec`, so this
|
|
1162
|
+
is not a regression this change introduces —
|
|
1163
|
+
`GET /owners/archived` is `403` while `GET /animals/9500?include=owner`
|
|
1164
|
+
answers `200` with her full document. That is
|
|
1165
|
+
[#243](https://github.com/abofs/stonyx-orm/issues/243)'s mechanism, and #243
|
|
1166
|
+
records it on `GET /owners` only; it reaches `included` membership and the
|
|
1167
|
+
`#232` related-resource route the same way. **#233 does not close it, and a
|
|
1168
|
+
consumer who needs `?include=` to honour a per-record deny must express that
|
|
1169
|
+
deny as a filter.**
|
|
1170
|
+
|
|
1171
|
+
**And read "carries no signal" as a property of the `included` member, the
|
|
1172
|
+
same way [Known limitations](#known-limitations) already asks you to read
|
|
1173
|
+
"indistinguishable" — not as a property of the whole response.** The prune is
|
|
1174
|
+
unobservable in `included`; the *rest* of the document is a separate
|
|
1175
|
+
question, and the same computed-and-serialized-attribute channels recorded
|
|
1176
|
+
above still apply to it. Measured counter-example on this very fixture pair:
|
|
1177
|
+
`GET /traits/2?include=tag` prunes the unclaimed `tag` from `included` and
|
|
1178
|
+
still serves `attributes.tag: "never-mounted"`, while `GET /traits/1` has no
|
|
1179
|
+
`tag` key at all — an existence oracle in `attributes`, from
|
|
1180
|
+
`src/serializer.ts` echoing the raw foreign key of a `belongsTo` target that
|
|
1181
|
+
did not resolve. That is
|
|
1182
|
+
[#246](https://github.com/abofs/stonyx-orm/issues/246)'s mechanism reached on
|
|
1183
|
+
a **read**, with [#248](https://github.com/abofs/stonyx-orm/issues/248) as
|
|
1184
|
+
its precondition — **audit your attributes before you treat a pruned
|
|
1185
|
+
sideload as unobservable.**
|
|
1186
|
+
|
|
1141
1187
|
**That resolves the right class; it does not guarantee a model-correct
|
|
1142
1188
|
answer, and the failure direction is not the safe one.** Only a predicate that
|
|
1143
1189
|
*reads* `context.model` can answer about the model it was asked about — see
|
|
@@ -1733,20 +1779,41 @@ GET /animals/1
|
|
|
1733
1779
|
2. Recursively traverses relationships depth-first
|
|
1734
1780
|
3. Deduplication still by type+id (no duplicates in included array)
|
|
1735
1781
|
4. Gracefully handles null/missing relationships at any depth
|
|
1736
|
-
5. Each
|
|
1782
|
+
5. Each related record is judged by **its own** model's access class at the
|
|
1783
|
+
push site before it is added, so a denied record neither enters `included`
|
|
1784
|
+
nor becomes a parent at the next depth
|
|
1785
|
+
([#233](https://github.com/abofs/stonyx-orm/issues/233)) — see the
|
|
1786
|
+
Limitations below for what "denied" does and does not cover
|
|
1787
|
+
6. Each included record gets full `toJSON()` representation, with its
|
|
1788
|
+
**linkage filtered** by the same verdict object the primary document was
|
|
1789
|
+
serialized with ([#235](https://github.com/abofs/stonyx-orm/issues/235))
|
|
1737
1790
|
|
|
1738
1791
|
#### Limitations
|
|
1739
1792
|
|
|
1740
1793
|
- Only available on GET endpoints (not POST/PATCH)
|
|
1741
|
-
- **`included` is access-filtered on
|
|
1742
|
-
|
|
1794
|
+
- **`included` is access-filtered on both of the two questions.** *Re-specified
|
|
1795
|
+
by [#233](https://github.com/abofs/stonyx-orm/issues/233). The sentence this
|
|
1796
|
+
replaces said membership "is still unfiltered (#233): a record that is 404 on
|
|
1797
|
+
its own routes is still served as an `included` resource, attributes and
|
|
1798
|
+
all."* What a record already in `included` may **name** in its own
|
|
1743
1799
|
`relationships.*.data` is filtered
|
|
1744
1800
|
([#235](https://github.com/abofs/stonyx-orm/issues/235)) — `?include=` no
|
|
1745
1801
|
longer republishes ids the primary document withholds. Whether a resource
|
|
1746
|
-
appears in `included` **at all** is *membership
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
and
|
|
1802
|
+
appears in `included` **at all** is *membership*, and #233 filters it: a
|
|
1803
|
+
related resource is judged by **its own** model's access class at the
|
|
1804
|
+
traversal's push site, so a record that class's **per-record filter** rejects
|
|
1805
|
+
is not a member, and the subtree beneath it is never traversed.
|
|
1806
|
+
- **Membership is filtered by the per-record filter, not by everything a
|
|
1807
|
+
record-addressed route refuses.** A deny expressed as a request-scoped
|
|
1808
|
+
`return false` — the shape the shipped sample uses for the `archived`
|
|
1809
|
+
owner — is **not** expressible on this path, because the linkage ask carries
|
|
1810
|
+
`recordId: null`. Measured on this branch and byte-identically on `dev`:
|
|
1811
|
+
`GET /owners/archived` is `403` while `GET /animals/9500?include=owner`
|
|
1812
|
+
serves her document in full. That is
|
|
1813
|
+
[#243](https://github.com/abofs/stonyx-orm/issues/243)'s mechanism, not
|
|
1814
|
+
#233's, and it reaches every linkage surface rather than only `GET /owners`.
|
|
1815
|
+
Express a per-record deny as a **filter** if you need `?include=` to honour
|
|
1816
|
+
it. See [Consumer Contracts](#consumer-contracts).
|
|
1750
1817
|
|
|
1751
1818
|
## Lifecycle Hooks
|
|
1752
1819
|
|
package/dist/orm-request.d.ts
CHANGED
|
@@ -76,8 +76,18 @@
|
|
|
76
76
|
* signal of is WHICH related record is being asked about -- `recordId` is
|
|
77
77
|
* `null` there and `request.params` names a record of a different model. See
|
|
78
78
|
* `AccessContext.recordId` in ./types/orm-types.ts for the full statement of
|
|
79
|
-
* that limit.
|
|
80
|
-
*
|
|
79
|
+
* that limit.
|
|
80
|
+
*
|
|
81
|
+
* AND THE `?include=` HALF IS NOW OUT OF DATE TOO, CORRECTED THE SAME WAY. It
|
|
82
|
+
* read: "`?include=` is still unfiltered and is abofs/stonyx-orm#233 / #235."
|
|
83
|
+
* Both have landed. #235 filters what a record already in `included` may NAME,
|
|
84
|
+
* and #233 filters MEMBERSHIP at the traversal's push site -- see
|
|
85
|
+
* `traverseIncludePath` below. The ask is the same shape as the
|
|
86
|
+
* related-resource one and carries the same limit: `recordId` is `null`, so a
|
|
87
|
+
* deny expressible only as a request-scoped `return false` -- which is how the
|
|
88
|
+
* shipped sample spells `/archived` -- still cannot fire on this path. That
|
|
89
|
+
* residual is abofs/stonyx-orm#243's, not #233's, and it is measured
|
|
90
|
+
* byte-identical on `dev`.
|
|
81
91
|
*
|
|
82
92
|
* SUPERSEDED 2026-09-01 BY abofs/stonyx-orm#236/#237, AND KEPT FOR THE
|
|
83
93
|
* CONSTRAINT IT STATES RATHER THAN AS A DESCRIPTION OF THE CODE. The context
|
package/dist/orm-request.js
CHANGED
|
@@ -76,8 +76,18 @@
|
|
|
76
76
|
* signal of is WHICH related record is being asked about -- `recordId` is
|
|
77
77
|
* `null` there and `request.params` names a record of a different model. See
|
|
78
78
|
* `AccessContext.recordId` in ./types/orm-types.ts for the full statement of
|
|
79
|
-
* that limit.
|
|
80
|
-
*
|
|
79
|
+
* that limit.
|
|
80
|
+
*
|
|
81
|
+
* AND THE `?include=` HALF IS NOW OUT OF DATE TOO, CORRECTED THE SAME WAY. It
|
|
82
|
+
* read: "`?include=` is still unfiltered and is abofs/stonyx-orm#233 / #235."
|
|
83
|
+
* Both have landed. #235 filters what a record already in `included` may NAME,
|
|
84
|
+
* and #233 filters MEMBERSHIP at the traversal's push site -- see
|
|
85
|
+
* `traverseIncludePath` below. The ask is the same shape as the
|
|
86
|
+
* related-resource one and carries the same limit: `recordId` is `null`, so a
|
|
87
|
+
* deny expressible only as a request-scoped `return false` -- which is how the
|
|
88
|
+
* shipped sample spells `/archived` -- still cannot fire on this path. That
|
|
89
|
+
* residual is abofs/stonyx-orm#243's, not #233's, and it is measured
|
|
90
|
+
* byte-identical on `dev`.
|
|
81
91
|
*
|
|
82
92
|
* SUPERSEDED 2026-09-01 BY abofs/stonyx-orm#236/#237, AND KEPT FOR THE
|
|
83
93
|
* CONSTRAINT IT STATES RATHER THAN AS A DESCRIPTION OF THE CODE. The context
|
|
@@ -758,11 +768,17 @@ export default class OrmRequest extends Request {
|
|
|
758
768
|
// is the SAME object the primary document is serialized with -- one
|
|
759
769
|
// verdict per type for the whole response, sideload included.
|
|
760
770
|
//
|
|
761
|
-
// The boundary
|
|
762
|
-
//
|
|
771
|
+
// The boundary, so the next reader does not have to derive it: this
|
|
772
|
+
// closes what a record already in `included` may NAME. WHETHER a
|
|
763
773
|
// resource appears in `included` at all is MEMBERSHIP and it is
|
|
764
|
-
// abofs/stonyx-orm#233's
|
|
765
|
-
//
|
|
774
|
+
// abofs/stonyx-orm#233's. THAT IS NOW CLOSED TOO, and the same `linkage`
|
|
775
|
+
// object closes it: `buildResponse` hands this filter to
|
|
776
|
+
// `collectIncludedRecords`, which denies at the push site. Corrected
|
|
777
|
+
// rather than deleted -- this comment read "a hidden owner is still a
|
|
778
|
+
// member here", which is the sentence the identical copy in
|
|
779
|
+
// `buildResponse` carried and which #233 falsified in both places. They
|
|
780
|
+
// are still two questions and neither closes the other: a record can be
|
|
781
|
+
// a member while its own linkage is filtered.
|
|
766
782
|
return buildResponse(record.toJSON?.({ fields: modelFields, baseUrl, linkage }), request.query?.include, record, {
|
|
767
783
|
links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}` },
|
|
768
784
|
baseUrl,
|
package/package.json
CHANGED
package/src/orm-request.ts
CHANGED
|
@@ -76,8 +76,18 @@
|
|
|
76
76
|
* signal of is WHICH related record is being asked about -- `recordId` is
|
|
77
77
|
* `null` there and `request.params` names a record of a different model. See
|
|
78
78
|
* `AccessContext.recordId` in ./types/orm-types.ts for the full statement of
|
|
79
|
-
* that limit.
|
|
80
|
-
*
|
|
79
|
+
* that limit.
|
|
80
|
+
*
|
|
81
|
+
* AND THE `?include=` HALF IS NOW OUT OF DATE TOO, CORRECTED THE SAME WAY. It
|
|
82
|
+
* read: "`?include=` is still unfiltered and is abofs/stonyx-orm#233 / #235."
|
|
83
|
+
* Both have landed. #235 filters what a record already in `included` may NAME,
|
|
84
|
+
* and #233 filters MEMBERSHIP at the traversal's push site -- see
|
|
85
|
+
* `traverseIncludePath` below. The ask is the same shape as the
|
|
86
|
+
* related-resource one and carries the same limit: `recordId` is `null`, so a
|
|
87
|
+
* deny expressible only as a request-scoped `return false` -- which is how the
|
|
88
|
+
* shipped sample spells `/archived` -- still cannot fire on this path. That
|
|
89
|
+
* residual is abofs/stonyx-orm#243's, not #233's, and it is measured
|
|
90
|
+
* byte-identical on `dev`.
|
|
81
91
|
*
|
|
82
92
|
* SUPERSEDED 2026-09-01 BY abofs/stonyx-orm#236/#237, AND KEPT FOR THE
|
|
83
93
|
* CONSTRAINT IT STATES RATHER THAN AS A DESCRIPTION OF THE CODE. The context
|
|
@@ -839,11 +849,17 @@ export default class OrmRequest extends Request {
|
|
|
839
849
|
// is the SAME object the primary document is serialized with -- one
|
|
840
850
|
// verdict per type for the whole response, sideload included.
|
|
841
851
|
//
|
|
842
|
-
// The boundary
|
|
843
|
-
//
|
|
852
|
+
// The boundary, so the next reader does not have to derive it: this
|
|
853
|
+
// closes what a record already in `included` may NAME. WHETHER a
|
|
844
854
|
// resource appears in `included` at all is MEMBERSHIP and it is
|
|
845
|
-
// abofs/stonyx-orm#233's
|
|
846
|
-
//
|
|
855
|
+
// abofs/stonyx-orm#233's. THAT IS NOW CLOSED TOO, and the same `linkage`
|
|
856
|
+
// object closes it: `buildResponse` hands this filter to
|
|
857
|
+
// `collectIncludedRecords`, which denies at the push site. Corrected
|
|
858
|
+
// rather than deleted -- this comment read "a hidden owner is still a
|
|
859
|
+
// member here", which is the sentence the identical copy in
|
|
860
|
+
// `buildResponse` carried and which #233 falsified in both places. They
|
|
861
|
+
// are still two questions and neither closes the other: a record can be
|
|
862
|
+
// a member while its own linkage is filtered.
|
|
847
863
|
return buildResponse(record.toJSON?.({ fields: modelFields, baseUrl, linkage }), request.query?.include, record, {
|
|
848
864
|
links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}` },
|
|
849
865
|
baseUrl,
|