@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 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
- [#233](https://github.com/abofs/stonyx-orm/issues/233) (`included`
990
- membership) and [#246](https://github.com/abofs/stonyx-orm/issues/246) (the
991
- absence of `attributes.<fk>` on a `POST` response), all open. **Audit your
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 still not filtered the relationship routes now
1050
- are.** *Re-specified by [#232](https://github.com/abofs/stonyx-orm/issues/232);
1051
- the sentence this replaces said all three surfaces were unfiltered, and two of
1052
- them no longer are.* `GET /animals/1/owner` and
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` still does not**: it
1055
- serializes the related record without resolving that class, so a filter on
1056
- `/owners` does not hide an owner reached through `?include=` on `/animals`.
1057
- There are **two** open questions here and they are owned separately —
1058
- [#233](https://github.com/abofs/stonyx-orm/issues/233), the remaining child of
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). Neither closes the other, and following only #233 will not lead you
1064
- to the second. Membership whether the related resource is served at all — is
1065
- a different question from which ids a document may *name*, immediately
1066
- below.
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 hidden on its own route is not a member, and **the
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 included record gets full `toJSON()` representation
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 one of the two questions, not both.** What
1742
- a record already in `included` may **name** in its own
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* and is still unfiltered
1747
- ([#233](https://github.com/abofs/stonyx-orm/issues/233)): a record that is
1748
- 404 on its own routes is still served as an `included` resource, attributes
1749
- and all. See [Consumer Contracts](#consumer-contracts).
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
 
@@ -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. `?include=` is still unfiltered and is abofs/stonyx-orm#233 /
80
- * #235.
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
@@ -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. `?include=` is still unfiltered and is abofs/stonyx-orm#233 /
80
- * #235.
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 that remains, so the next reader does not have to derive
762
- // it: this closes what a record already in `included` may NAME. WHETHER a
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 -- a hidden owner is still a member here.
765
- // Neither question closes the other.
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
@@ -4,7 +4,7 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.3.2-alpha.88",
7
+ "version": "0.3.2-alpha.89",
8
8
  "description": "",
9
9
  "main": "dist/index.js",
10
10
  "type": "module",
@@ -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. `?include=` is still unfiltered and is abofs/stonyx-orm#233 /
80
- * #235.
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 that remains, so the next reader does not have to derive
843
- // it: this closes what a record already in `included` may NAME. WHETHER a
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 -- a hidden owner is still a member here.
846
- // Neither question closes the other.
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,