@stonyx/orm 0.3.2-alpha.63 → 0.3.2-alpha.65
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 +71 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -629,15 +629,50 @@ record-level routes are ungated, so the id was the only thing standing between
|
|
|
629
629
|
an unauthenticated caller and `GET`/`PATCH`/`DELETE` on a record. That was never
|
|
630
630
|
a control and must not become one; configure `access`.
|
|
631
631
|
|
|
632
|
-
**And the id itself is an occupancy signal
|
|
633
|
-
store, not the caller's filtered view — it
|
|
634
|
-
it returns is a function of records the
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
632
|
+
**And the id itself is an occupancy signal — on both model kinds.**
|
|
633
|
+
`assignRecordId` reads the whole store, not the caller's filtered view — it
|
|
634
|
+
never sees `state.filter` — so the id it returns is a function of records the
|
|
635
|
+
caller may not be permitted to read. **This applies to numeric-id collections
|
|
636
|
+
as well as string-id ones**, and the conditions differ, so read both:
|
|
637
|
+
|
|
638
|
+
- **String-id collections, always.** The assigned `n` is the smallest positive
|
|
639
|
+
integer whose landing key is free, which tells the caller that every key
|
|
640
|
+
below it is taken, hidden or not.
|
|
641
|
+
- **Numeric-id collections, once one record sits at the numeric ceiling.** The
|
|
642
|
+
normal answer is `max + 1`, which discloses only the maximum. But `max + 1`
|
|
643
|
+
is not representable at or above 2^53, so the walk restarts from `1` (see
|
|
644
|
+
breaking change 8) and the assigned id becomes the smallest free integer —
|
|
645
|
+
the same occupancy predicate, now over arbitrary low keys. Each subsequent
|
|
646
|
+
no-id `POST` names the next free one, so a caller can enumerate the holes in
|
|
647
|
+
a range it cannot read.
|
|
648
|
+
|
|
649
|
+
**A ceiling record reaches a filter-protected collection even though `POST`
|
|
650
|
+
refuses caller ids on one.** Breaking change 3 makes
|
|
651
|
+
`POST /animals {"id": 9007199254740992}` answer `403`, but the same id lands
|
|
652
|
+
through a *relationship write on another collection* —
|
|
653
|
+
`POST /owners` carrying `attributes: { pets: [{ "id": 9007199254740992 }] }`
|
|
654
|
+
creates the animal under that key
|
|
655
|
+
([#207](https://github.com/abofs/stonyx-orm/issues/207), the same channel the
|
|
656
|
+
**Known limitations** re-parenting note describes). So the precondition is
|
|
657
|
+
reachable by an unauthenticated caller on exactly the collections `access`
|
|
658
|
+
exists to protect. Measured on the sample fixture, with every animal hidden by
|
|
659
|
+
the `/animals` predicate and keys 4 and 7 deleted:
|
|
660
|
+
|
|
661
|
+
```
|
|
662
|
+
GET /animals -> 200 [] (nothing visible)
|
|
663
|
+
GET /animals/4 -> 404 (free — indistinguishable from hidden)
|
|
664
|
+
POST /animals {"id":4} -> 403 (breaking change 3)
|
|
665
|
+
POST /owners {... pets:[{"id":9007199254740992}]} -> 200 (#207 plants the ceiling record)
|
|
666
|
+
POST /animals (no id) -> 200 id=4 <- names a hole in the hidden range
|
|
667
|
+
POST /animals (no id) -> 200 id=7 <- and the other one
|
|
668
|
+
POST /animals (no id) -> 200 id=13
|
|
669
|
+
POST /animals (no id) -> 200 id=14
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Closing this requires the assignment to be filter-aware, which is a change to
|
|
673
|
+
the `access` contract rather than a fix; it is stated here rather than left to
|
|
674
|
+
be discovered. Callers with no function-style filter are unaffected — there are
|
|
675
|
+
no hidden records to disclose.
|
|
641
676
|
|
|
642
677
|
### Identifying the collection
|
|
643
678
|
|
|
@@ -848,8 +883,9 @@ they are recorded here.
|
|
|
848
883
|
population breaking changes 3 and 4 explicitly exempt. If you were relying on
|
|
849
884
|
a hex-shaped or whitespace-padded id creating a second record, it never did.
|
|
850
885
|
|
|
851
|
-
8. **Server-assigned ids change value on string-id models,
|
|
852
|
-
|
|
886
|
+
8. **Server-assigned ids change value on string-id models, numeric ids stop
|
|
887
|
+
being monotonic at the numeric ceiling, and the create route gains a
|
|
888
|
+
`409`.** Three consumer-visible changes from
|
|
853
889
|
[#203](https://github.com/abofs/stonyx-orm/issues/203).
|
|
854
890
|
|
|
855
891
|
**The value.** A `POST` with no `id` against a model declaring
|
|
@@ -862,6 +898,7 @@ they are recorded here.
|
|
|
862
898
|
id models (`id = attr('number')`, the default) are unaffected in shape: they
|
|
863
899
|
still get an integer, but it is now the **maximum** existing id plus one
|
|
864
900
|
rather than the last-inserted id plus one, which is the defect #203 is about.
|
|
901
|
+
They are **not** unaffected in *sequence* — see the monotonicity half below.
|
|
865
902
|
|
|
866
903
|
The value is deliberately **not** numeric-looking, and that is not cosmetic.
|
|
867
904
|
Every id-bearing surface resolves a numeric-looking string id to a **number**
|
|
@@ -872,6 +909,29 @@ they are recorded here.
|
|
|
872
909
|
`context.record === undefined`
|
|
873
910
|
([#209](https://github.com/abofs/stonyx-orm/issues/209)).
|
|
874
911
|
|
|
912
|
+
**Numeric ids are no longer monotonic, and deleted ids can be re-issued.**
|
|
913
|
+
The precondition is narrow but it is reachable, and there is no signal when
|
|
914
|
+
it is met: **one record filed at or above 2^53** (`9007199254740992`). `max
|
|
915
|
+
+ 1` is not representable there, so assignment restarts from `1` and walks
|
|
916
|
+
up to the lowest free key — which means the id of a *deleted* record is
|
|
917
|
+
handed to the next `POST`. Both `dev` and every prior release were strictly
|
|
918
|
+
monotonic and never re-issued a numeric id, so a consumer that relied on
|
|
919
|
+
that — audit rows, cursors, cached authorization decisions, external
|
|
920
|
+
references keyed on the id — now has a stale reference that silently points
|
|
921
|
+
at a **different record, created by a different caller**, rather than at a
|
|
922
|
+
deleted one. Nothing fails; the reference simply resolves to the wrong
|
|
923
|
+
record.
|
|
924
|
+
|
|
925
|
+
The restart is deliberate and is not itself optional: without it, one record
|
|
926
|
+
at the ceiling made every subsequent server-assigned create on that
|
|
927
|
+
collection fail permanently. Re-use is the cost of keeping the collection
|
|
928
|
+
writable. **If you need monotonic ids, assign them yourself** rather than
|
|
929
|
+
letting the server assign, and note that a ceiling record can be planted by
|
|
930
|
+
an unauthenticated caller — see *And the id itself is an occupancy signal*
|
|
931
|
+
under [Filter functions](#filter-functions) for the reachability path.
|
|
932
|
+
String-id models are unaffected by this half: their keys are
|
|
933
|
+
`<model>-<n>` and were never monotonic over an integer sequence.
|
|
934
|
+
|
|
875
935
|
**The status.** `POST /{collection}` can now answer `409` for a reason other
|
|
876
936
|
than a duplicate id: the server could not derive a free id. That requires a
|
|
877
937
|
**non-injective** id transform — one that maps distinct candidates onto the
|