@stonyx/orm 0.3.2-alpha.63 → 0.3.2-alpha.64
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 +177 -142
- package/dist/manage-record.js +9 -234
- package/dist/orm-request.d.ts +53 -25
- package/dist/orm-request.js +58 -83
- package/dist/standalone-db.js +5 -17
- package/dist/utils.d.ts +0 -44
- package/dist/utils.js +0 -47
- package/package.json +1 -1
- package/src/manage-record.ts +9 -253
- package/src/orm-request.ts +58 -85
- package/src/standalone-db.ts +6 -17
- package/src/utils.ts +0 -50
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.
|
|
319
|
-
> not
|
|
320
|
-
>
|
|
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,65 @@ 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
|
-
// `
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
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 variants 1, 2, 4 and 5 are not
|
|
354
|
+
// constructible against this predicate any more — they are history, not
|
|
355
|
+
// rules to follow. VARIANT 3 IS THE EXCEPTION AND THE CLAIM IS NARROWER
|
|
356
|
+
// THAN IT WAS: a matcher stricter than the router can still be stepped
|
|
357
|
+
// around, because the sub-path rule below is still a string comparison.
|
|
358
|
+
// Case is handled; percent-encoding is not — abofs/stonyx-orm#228.
|
|
359
|
+
//
|
|
360
|
+
// `operation` is destructured to name the whole contract at the point of
|
|
361
|
+
// use. This sample's rules are per-model and per-sub-path rather than
|
|
362
|
+
// per-verb, so it does not branch on it; the permission array at the bottom
|
|
363
|
+
// is where the verb is answered.
|
|
364
|
+
|
|
365
|
+
// FAIL CLOSED ON ARGUMENT TWO. `model` is absent for any caller that
|
|
366
|
+
// resolved this predicate without supplying the context, and a request this
|
|
367
|
+
// function cannot identify DENIES rather than falling through to the CRUD
|
|
368
|
+
// grant at the bottom. An unidentifiable input must never be the permissive
|
|
369
|
+
// path. Argument ONE is guarded at its own read, below — this guard does
|
|
370
|
+
// not cover it.
|
|
371
|
+
if (typeof model !== 'string' || model === '') return false;
|
|
372
|
+
|
|
373
|
+
if (model === 'owner') {
|
|
374
|
+
// The context names WHICH MODEL and WHICH VERB — not which route. Six
|
|
375
|
+
// distinct owner surfaces produce one identical context, so a rule that
|
|
376
|
+
// depends on the SUB-PATH still needs argument one. `request.path` is
|
|
377
|
+
// mount-relative and query-free, and it is the one read of the raw
|
|
378
|
+
// request the README sanctions. false → 403 for the whole request.
|
|
379
|
+
//
|
|
380
|
+
// THIS DENY CANNOT BE EXPRESSED FROM THE CONTEXT ALONE. Migrating it away
|
|
381
|
+
// does not remove a rule, it turns a deny into an ALLOW, silently.
|
|
382
|
+
//
|
|
383
|
+
// FAIL CLOSED ON ARGUMENT ONE TOO. The guard above covers the context;
|
|
384
|
+
// this one covers the request, and since #202 they are two different
|
|
385
|
+
// objects. A caller that resolves this predicate through the documented
|
|
386
|
+
// `Orm.instance.getAccess()` path and hand-assembles a request can supply
|
|
387
|
+
// a perfectly valid context with no usable `path` — and
|
|
388
|
+
// `String(request.path ?? '')` is then `''`, which matches no sub-path
|
|
389
|
+
// rule and falls straight through to the per-record filter below. That is
|
|
390
|
+
// a DENY becoming an ALLOW. An input this function cannot identify DENIES,
|
|
391
|
+
// whichever ARGUMENT it arrived on — which is also why the `?? ''` this
|
|
392
|
+
// file's header condemns does not appear below.
|
|
393
|
+
if (typeof request?.path !== 'string' || request.path === '') return false;
|
|
394
|
+
|
|
395
|
+
// Lower-cased because the router matched case-insensitively, so a
|
|
396
|
+
// case-sensitive rule here would be stricter than the router and could be
|
|
397
|
+
// stepped around.
|
|
398
|
+
//
|
|
399
|
+
// CASE-FOLDING ALONE IS NOT A SUFFICIENT NORMALISATION, and this line is
|
|
400
|
+
// not a recipe for one. Express sets `request.path` from the RAW pathname
|
|
401
|
+
// while the router DECODES `:id`, so `GET /owners/%61rchived` reaches this
|
|
402
|
+
// comparison as `/%61rchived`, walks past the deny, and is dispatched as
|
|
403
|
+
// the record `archived` — abofs/stonyx-orm#228. A matcher must normalise
|
|
404
|
+
// the way the router that dispatched the request does. Record ids are
|
|
405
|
+
// case-sensitive and must be compared at their real case.
|
|
406
|
+
const path = request.path.toLowerCase();
|
|
359
407
|
|
|
360
|
-
if (collection.endsWith('/owners')) {
|
|
361
408
|
if (path === '/archived' || path.startsWith('/archived/')) return false;
|
|
362
409
|
|
|
363
410
|
// Returning a function plugs it in as a per-record filter, and it is
|
|
@@ -373,7 +420,7 @@ export default class GlobalAccess {
|
|
|
373
420
|
// inert. Deliberately NO `?? record.owner` fallback: accepting the raw
|
|
374
421
|
// shape as well as the resolved one would absorb a resolution regression
|
|
375
422
|
// silently, which is exactly what blinded this fixture before.
|
|
376
|
-
if (
|
|
423
|
+
if (model === 'animal') return record => record.owner?.id !== 'restricted';
|
|
377
424
|
|
|
378
425
|
// Allows full access to all calls that don't match any of the above conditions
|
|
379
426
|
return ['read', 'create', 'update', 'delete'];
|
|
@@ -523,9 +570,10 @@ sample, `getAccess('owner') === getAccess('animal')`.
|
|
|
523
570
|
#### Passing the context makes a model-correct answer *possible*
|
|
524
571
|
|
|
525
572
|
It does not make the answer model-correct on its own. **The resolved predicate
|
|
526
|
-
has to read the context.**
|
|
527
|
-
|
|
528
|
-
**animals
|
|
573
|
+
has to read the context.** Against a predicate that ignores it the failure is
|
|
574
|
+
measurable. On a request Express dispatched to `GET /owners/angela`, asked about
|
|
575
|
+
**animals**, the sample as it shipped before
|
|
576
|
+
[#222](https://github.com/abofs/stonyx-orm/issues/222) answered:
|
|
529
577
|
|
|
530
578
|
```
|
|
531
579
|
getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
@@ -533,18 +581,26 @@ getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
|
533
581
|
```
|
|
534
582
|
|
|
535
583
|
That is the **owners** filter, and it returns `true` for animal 21 — the record
|
|
536
|
-
hidden on every animal surface. Under a mount
|
|
537
|
-
way it is worse still: it falls through to
|
|
538
|
-
`['read', 'create', 'update', 'delete']`, a full CRUD grant.
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
584
|
+
hidden on every animal surface. Under a mount such a predicate recognizes
|
|
585
|
+
neither way it is worse still: it falls through to
|
|
586
|
+
`['read', 'create', 'update', 'delete']`, a full CRUD grant. Either way the
|
|
587
|
+
context was supplied and the answer is not the animal answer, and it is wrong in
|
|
588
|
+
the direction that **grants** — because that predicate was single-argument and
|
|
589
|
+
identified its collection from the request, so it answered about the collection
|
|
590
|
+
the request was *addressed to* while being asked about another one.
|
|
591
|
+
|
|
592
|
+
The sample shipped with this repo has since been migrated to read the context,
|
|
593
|
+
and the same call now answers with the **animal** filter:
|
|
594
|
+
|
|
595
|
+
```
|
|
596
|
+
getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
597
|
+
-> record => record.owner?.id !== 'restricted'
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
A single-argument predicate remains the default in every consumer tree, and a
|
|
601
|
+
caller has no supported way to tell which kind it resolved. The boot-time arity
|
|
602
|
+
warning that surfaces one is
|
|
603
|
+
[#221](https://github.com/abofs/stonyx-orm/issues/221).
|
|
548
604
|
|
|
549
605
|
So: pass the context, and do not treat a resolved predicate's answer as
|
|
550
606
|
model-specific until that predicate has been migrated to read it.
|
|
@@ -581,7 +637,7 @@ a write to a *different* collection can still re-parent one. See
|
|
|
581
637
|
| `GET /:models/:id/relationships/{relationship}` | `404` — same |
|
|
582
638
|
| `PATCH /:models/:id` | `404`, no attribute is applied |
|
|
583
639
|
| `DELETE /:models/:id` | `404`, the record is not removed and no SQL `DELETE` is issued |
|
|
584
|
-
| `POST /:models` | `403`, and a record **this request inserted** is rolled back — see [Known limitations](#known-limitations) for
|
|
640
|
+
| `POST /:models` | `403`, and a record **this request inserted** is rolled back — see [Known limitations](#known-limitations) for the case where it did not insert one |
|
|
585
641
|
|
|
586
642
|
**Denied record-level requests return 404, not 403.** This is deliberate and it
|
|
587
643
|
is the property most easily "improved" away. 403 would confirm that the record
|
|
@@ -615,35 +671,23 @@ chooses the id and learns whether the create succeeded — so under a filter the
|
|
|
615
671
|
caller does not choose the id. The refusal happens before any store lookup, so
|
|
616
672
|
neither the status nor the response time depends on whether the id exists.
|
|
617
673
|
|
|
618
|
-
Let the server assign the id and read it back from the response
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
numeric ceiling, the lowest free integer), and a string-id model gets
|
|
622
|
-
`<model>-<n>`. See breaking change 8.
|
|
623
|
-
|
|
624
|
-
**What a server-assigned id is not.** It is not a secret. On a string-id
|
|
625
|
-
collection it is dense and enumerable from `1`, where previously it inherited
|
|
626
|
-
whatever entropy the last-inserted id happened to carry — a UUID-seeded store
|
|
627
|
-
answered a UUID-derived key. If a collection has **no** `access` config its
|
|
628
|
-
record-level routes are ungated, so the id was the only thing standing between
|
|
629
|
-
an unauthenticated caller and `GET`/`PATCH`/`DELETE` on a record. That was never
|
|
630
|
-
a control and must not become one; configure `access`.
|
|
631
|
-
|
|
632
|
-
**And the id itself is an occupancy signal.** `assignRecordId` reads the whole
|
|
633
|
-
store, not the caller's filtered view — it never sees `state.filter` — so the id
|
|
634
|
-
it returns is a function of records the caller may not be permitted to read. On
|
|
635
|
-
a string-id collection the assigned `n` is the smallest positive integer whose
|
|
636
|
-
key is free, which tells the caller that every key below it is taken, hidden or
|
|
637
|
-
not. Closing that requires the assignment to be filter-aware, which is a change
|
|
638
|
-
to the `access` contract rather than a fix; it is stated here rather than left
|
|
639
|
-
to be discovered. Callers with no function-style filter are unaffected — there
|
|
640
|
-
are no hidden records to disclose.
|
|
674
|
+
Let the server assign the id and read it back from the response. Callers with no
|
|
675
|
+
function-style filter are unaffected: `409` on a duplicate id and `200` on a free
|
|
676
|
+
one both behave exactly as before.
|
|
641
677
|
|
|
642
678
|
### Identifying the collection
|
|
643
679
|
|
|
644
|
-
**Do not reconstruct the request path
|
|
645
|
-
|
|
646
|
-
|
|
680
|
+
**Do not reconstruct the request path — and since
|
|
681
|
+
[#202](https://github.com/abofs/stonyx-orm/issues/202) you do not have to
|
|
682
|
+
identify the collection at all.** Read `model` from
|
|
683
|
+
[the access context](#the-access-context-second-argument): it is fixed at mount
|
|
684
|
+
time, no request can influence it, and there is nothing left to parse.
|
|
685
|
+
|
|
686
|
+
**Everything below is the record of what happened when this sample did parse
|
|
687
|
+
it.** It is kept as history, not as a recipe — none of these matching strategies
|
|
688
|
+
should be written into a new predicate. Every version of this sample that tried
|
|
689
|
+
to identify the collection from the request target failed **open**, and each
|
|
690
|
+
variant was found only after the previous one was fixed:
|
|
647
691
|
|
|
648
692
|
| # | Variant | Why it fails open |
|
|
649
693
|
|---|---|---|
|
|
@@ -653,13 +697,24 @@ was fixed:
|
|
|
653
697
|
| 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. |
|
|
654
698
|
| 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. |
|
|
655
699
|
|
|
656
|
-
**The fix is not a sixth rule
|
|
657
|
-
|
|
658
|
-
**
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
700
|
+
**The fix is not a sixth rule, and it is not a better string to match.** It is
|
|
701
|
+
to stop identifying the collection at all. That is a statement about
|
|
702
|
+
**identifying the collection**, and it is not a statement about the sample as a
|
|
703
|
+
whole: the `/archived` sub-path rule *is* still a string match, and
|
|
704
|
+
[#228](https://github.com/abofs/stonyx-orm/issues/228) is a sixth spelling that
|
|
705
|
+
gets past it. Sub-path rules are the residue this fix does not cover, which is
|
|
706
|
+
why they must normalise the way the router does.
|
|
707
|
+
|
|
708
|
+
An intermediate revision read **`request.baseUrl`** — the mount Express
|
|
709
|
+
*actually matched*. That closed all five variants: it carries no query string
|
|
710
|
+
(variant 2), it is not mount-relative (variant 1), it already contains the
|
|
711
|
+
configured `ORM_REST_ROUTE` prefix (variant 4 — there is nothing left to derive,
|
|
712
|
+
so `/apiowners` is unconstructible), and it is unaffected by an absolute-form
|
|
713
|
+
target (variant 5). It was still a transport artifact standing in for a
|
|
714
|
+
structural fact, and it is **no longer what the sample does**: the sample reads
|
|
715
|
+
`model`, so all five variants are unconstructible against it rather than
|
|
716
|
+
handled. The table below is retained as the measured evidence behind the five
|
|
717
|
+
variants, not because any of these values should be matched on:
|
|
663
718
|
|
|
664
719
|
| request | `request.url` | `request.originalUrl` | `request.baseUrl` | `request.path` |
|
|
665
720
|
|---|---|---|---|---|
|
|
@@ -670,21 +725,35 @@ unconstructible), and it is unaffected by an absolute-form target (variant 5).
|
|
|
670
725
|
| `GET http://anything.example/owners/angela` | `http://anything.example/angela` | `http://anything.example/owners/angela` | `/owners` | `/angela` |
|
|
671
726
|
| `GET /api/animals/22` (`ORM_REST_ROUTE=/api`) | `/22` | `/api/animals/22` | `/api/animals` | `/22` |
|
|
672
727
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
728
|
+
**One read of argument one survives, and it must: `request.path`.** It is
|
|
729
|
+
mount-relative and query-free, and it is for rules that distinguish **sub-paths**
|
|
730
|
+
beneath the mount — as the `/archived` deny in the sample above does. The context
|
|
731
|
+
names which model and which verb, **not which route**, so that deny *cannot be
|
|
732
|
+
expressed from the context alone*, and a context-only rewrite would silently turn
|
|
733
|
+
it into an allow.
|
|
734
|
+
|
|
735
|
+
**Normalise the way the router that dispatched the request does — and
|
|
736
|
+
case-folding alone does not.** A matcher stricter than the router can be stepped
|
|
737
|
+
around, so the sample lower-cases before comparing (the router matched
|
|
738
|
+
case-insensitively). That closes the case gap and **it is not the whole rule**:
|
|
739
|
+
Express sets `request.path` from the **raw, undecoded** pathname while the router
|
|
740
|
+
**decodes** `:id`, so `GET /owners/%61rchived` reaches a `path === '/archived'`
|
|
741
|
+
comparison as `/%61rchived`, walks past the deny, and is dispatched as the record
|
|
742
|
+
`archived`. That gap is live in the sample above and is tracked as
|
|
743
|
+
[#228](https://github.com/abofs/stonyx-orm/issues/228) — **do not read the
|
|
744
|
+
`.toLowerCase()` there as a complete normalisation recipe.** Record ids are
|
|
745
|
+
case-sensitive and must be compared at their real case.
|
|
746
|
+
|
|
747
|
+
**Fail closed on anything you cannot identify — on *either* argument.**
|
|
748
|
+
`String(request.originalUrl ?? '')` was once added here to stop a `TypeError`,
|
|
749
|
+
and it traded fail-closed for fail-**open**: an empty string matched no
|
|
750
|
+
collection, so `access()` fell through to the permission array and granted full
|
|
751
|
+
CRUD. The same rule applies to the context — the sample returns `false` for an
|
|
752
|
+
absent `model` rather than falling through. Since #202 the guard and the read can
|
|
753
|
+
sit on **different objects**, and a guard on argument two does not protect a read
|
|
754
|
+
of argument one: the sample therefore also returns `false` when `request.path` is
|
|
755
|
+
absent or is not a string, rather than letting `?? ''` fall through to the
|
|
756
|
+
per-record filter. An input you cannot identify must **deny**.
|
|
688
757
|
|
|
689
758
|
### Known limitations
|
|
690
759
|
|
|
@@ -707,11 +776,14 @@ sub-paths beneath the mount, as the `/archived` deny above does.
|
|
|
707
776
|
see [The access context](#the-access-context-second-argument):
|
|
708
777
|
`Orm.instance.getAccess(modelName)` makes another model's predicate
|
|
709
778
|
**reachable**, and `context.model` makes a **model-correct answer possible** —
|
|
710
|
-
possible, not guaranteed: the resolved predicate has to read the context
|
|
711
|
-
|
|
712
|
-
([#
|
|
713
|
-
|
|
714
|
-
|
|
779
|
+
possible, not guaranteed: the resolved predicate has to read the context. The
|
|
780
|
+
sample shipped with this repo now does
|
|
781
|
+
([#222](https://github.com/abofs/stonyx-orm/issues/222)), so
|
|
782
|
+
`getAccess('animal')` answers with the animal filter; a predicate that ignores
|
|
783
|
+
the second argument still answers about the collection the request is
|
|
784
|
+
addressed to, and the boot-time warning that surfaces one is
|
|
785
|
+
[#221](https://github.com/abofs/stonyx-orm/issues/221). **The mechanism
|
|
786
|
+
exists; the ORM does not yet use it on this path.** The re-parenting write above is still
|
|
715
787
|
**not refused** — that enforcement is
|
|
716
788
|
[#196](https://github.com/abofs/stonyx-orm/issues/196) and
|
|
717
789
|
[#207](https://github.com/abofs/stonyx-orm/issues/207), which were blocked on
|
|
@@ -763,10 +835,9 @@ sub-paths beneath the mount, as the `/archived` deny above does.
|
|
|
763
835
|
transform output differs from its lookup key is the same defect. Filtered
|
|
764
836
|
collections are unaffected — breaking change 3 refuses any client-supplied id
|
|
765
837
|
— so this reaches consumers with **no** function-style filter. Tracked as
|
|
766
|
-
[#205](https://github.com/abofs/stonyx-orm/issues/205)
|
|
767
|
-
[#203](https://github.com/abofs/stonyx-orm/issues/203)
|
|
768
|
-
|
|
769
|
-
the client-supplied half and is still open.
|
|
838
|
+
[#205](https://github.com/abofs/stonyx-orm/issues/205), alongside
|
|
839
|
+
[#203](https://github.com/abofs/stonyx-orm/issues/203), which is the other way
|
|
840
|
+
a create can land on an id nobody named.
|
|
770
841
|
- **`context.record` is `undefined` for an after-`create` hook when a string-id
|
|
771
842
|
model is given a numeric-looking id.** The post-create lookup uses the same id
|
|
772
843
|
coercion as every other surface, which resolves `'9107'` to the number `9107`,
|
|
@@ -776,18 +847,15 @@ sub-paths beneath the mount, as the `/archived` deny above does.
|
|
|
776
847
|
[#209](https://github.com/abofs/stonyx-orm/issues/209).
|
|
777
848
|
- **A denied `POST` rolls back only a record it *inserted*.** The rollback
|
|
778
849
|
requires the store to have grown, because removing by id alone is a write
|
|
779
|
-
primitive keyed by a caller-supplied value.
|
|
780
|
-
|
|
781
|
-
[#203](https://github.com/abofs/stonyx-orm/issues/203) —
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
[#207](https://github.com/abofs/stonyx-orm/issues/207) — puts the condition
|
|
789
|
-
back, and without the guard a denied `403` would delete a record the request
|
|
790
|
-
did not create.
|
|
850
|
+
primitive keyed by a caller-supplied value. When `assignRecordId` lands a
|
|
851
|
+
**server-assigned** id on an occupied slot
|
|
852
|
+
([#203](https://github.com/abofs/stonyx-orm/issues/203) — it returns
|
|
853
|
+
last-*inserted* + 1, not max + 1, so a store whose insertion order is not
|
|
854
|
+
ascending collides), `createRecord` updates that record **in place**: the map
|
|
855
|
+
does not grow, the rollback correctly declines to remove a record this request
|
|
856
|
+
did not create, and the `403` leaves the caller's attributes on someone else's
|
|
857
|
+
record. Narrow — it needs a non-ascending insertion order — but it is the
|
|
858
|
+
reachability condition, so it is stated rather than implied.
|
|
791
859
|
|
|
792
860
|
### Breaking changes
|
|
793
861
|
|
|
@@ -848,39 +916,6 @@ they are recorded here.
|
|
|
848
916
|
population breaking changes 3 and 4 explicitly exempt. If you were relying on
|
|
849
917
|
a hex-shaped or whitespace-padded id creating a second record, it never did.
|
|
850
918
|
|
|
851
|
-
8. **Server-assigned ids change value on string-id models, and the create route
|
|
852
|
-
gains a `409`.** Two consumer-visible changes from
|
|
853
|
-
[#203](https://github.com/abofs/stonyx-orm/issues/203).
|
|
854
|
-
|
|
855
|
-
**The value.** A `POST` with no `id` against a model declaring
|
|
856
|
-
`id = attr('string')` previously produced the *last-inserted* id with `1`
|
|
857
|
-
concatenated onto it — an owner store holding `['gina', 'bob']` answered
|
|
858
|
-
`'bob1'`. It now answers `'owner-1'`: the model name, a hyphen, and the
|
|
859
|
-
lowest positive integer whose landing key is free. **No test in this repo
|
|
860
|
-
pinned the old value**, so a consumer relying on it gets no failing test, no
|
|
861
|
-
deprecation and no other signal — which is why it is recorded here. Numeric
|
|
862
|
-
id models (`id = attr('number')`, the default) are unaffected in shape: they
|
|
863
|
-
still get an integer, but it is now the **maximum** existing id plus one
|
|
864
|
-
rather than the last-inserted id plus one, which is the defect #203 is about.
|
|
865
|
-
|
|
866
|
-
The value is deliberately **not** numeric-looking, and that is not cosmetic.
|
|
867
|
-
Every id-bearing surface resolves a numeric-looking string id to a **number**
|
|
868
|
-
(`GET /owners/1` looks up `1`), while a string-id model files its records
|
|
869
|
-
under the **string** key `'1'`. A server-assigned `'1'` would therefore be a
|
|
870
|
-
record that was created successfully and could not be fetched, updated or
|
|
871
|
-
deleted by id, and whose after-`create` hook received
|
|
872
|
-
`context.record === undefined`
|
|
873
|
-
([#209](https://github.com/abofs/stonyx-orm/issues/209)).
|
|
874
|
-
|
|
875
|
-
**The status.** `POST /{collection}` can now answer `409` for a reason other
|
|
876
|
-
than a duplicate id: the server could not derive a free id. That requires a
|
|
877
|
-
**non-injective** id transform — one that maps distinct candidates onto the
|
|
878
|
-
same store key, such as `boolean`, or anything you registered on
|
|
879
|
-
`Orm.instance.transforms` and named as an id type. It is a configuration
|
|
880
|
-
fault rather than a request fault; the message is logged through
|
|
881
|
-
`stonyx/log`. Previously this case threw out of the handler and express
|
|
882
|
-
answered `500` with a stack trace.
|
|
883
|
-
|
|
884
919
|
### Include Parameter (Sideloading Relationships)
|
|
885
920
|
|
|
886
921
|
The ORM supports JSON API-compliant relationship sideloading via the `include` query parameter. This reduces the need for multiple API requests by embedding related records in a single response.
|