@stonyx/orm 0.3.2-alpha.62 → 0.3.2-alpha.63
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 +141 -133
- package/dist/manage-record.js +234 -9
- package/dist/orm-request.d.ts +25 -41
- package/dist/orm-request.js +83 -46
- package/dist/standalone-db.js +17 -5
- package/dist/utils.d.ts +44 -0
- package/dist/utils.js +47 -0
- package/package.json +1 -1
- package/src/manage-record.ts +253 -9
- package/src/orm-request.ts +85 -46
- package/src/standalone-db.ts +17 -6
- package/src/utils.ts +50 -0
package/README.md
CHANGED
|
@@ -315,24 +315,15 @@ 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
|
-
>
|
|
320
|
-
>
|
|
321
|
-
> at the mount at all, so all five variants are **unconstructible** against it
|
|
322
|
-
> rather than merely handled.
|
|
318
|
+
> after the previous was fixed, by five different people. The sample below does
|
|
319
|
+
> not parse anything: it reads `request.baseUrl`, the mount Express actually
|
|
320
|
+
> matched.
|
|
323
321
|
>
|
|
324
322
|
> That is still a stopgap. **The real fix is
|
|
325
323
|
> [#202](https://github.com/abofs/stonyx-orm/issues/202)** — `access()` should
|
|
326
324
|
> receive the model, the operation and the record, so there is nothing to
|
|
327
325
|
> identify. Until it lands, prefer the array shape (`['read']`) or `false` where
|
|
328
326
|
> 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
|
-
>
|
|
336
327
|
> The same warning is repeated at the top of `src/orm-request.ts`, which ships;
|
|
337
328
|
> the longer write-up in `docs/usage-patterns.md` does **not** ship, so this
|
|
338
329
|
> README and that source header are the two copies a consumer sees.
|
|
@@ -346,39 +337,27 @@ Access classes define models and provide custom filtering/authorization logic.
|
|
|
346
337
|
export default class GlobalAccess {
|
|
347
338
|
models = ['owner', 'animal'];
|
|
348
339
|
|
|
349
|
-
access(request
|
|
350
|
-
// `
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
// cannot identify DENIES rather than falling through to the CRUD grant at
|
|
365
|
-
// the bottom. An unidentifiable input must never be the permissive path.
|
|
366
|
-
if (typeof model !== 'string' || model === '') return false;
|
|
367
|
-
|
|
368
|
-
if (model === 'owner') {
|
|
369
|
-
// The context names WHICH MODEL and WHICH VERB — not which route. Six
|
|
370
|
-
// distinct owner surfaces produce one identical context, so a rule that
|
|
371
|
-
// depends on the SUB-PATH still needs argument one. `request.path` is
|
|
372
|
-
// mount-relative and query-free, and it is the one read of the raw
|
|
373
|
-
// request the README sanctions. Lower-cased because the router matched
|
|
374
|
-
// case-insensitively, so a case-sensitive rule here would be stricter
|
|
375
|
-
// than the router and could be stepped around. false → 403 for the whole
|
|
376
|
-
// request.
|
|
377
|
-
//
|
|
378
|
-
// THIS DENY CANNOT BE EXPRESSED FROM THE CONTEXT ALONE. Migrating it away
|
|
379
|
-
// does not remove a rule, it turns a deny into an ALLOW, silently.
|
|
380
|
-
const path = String(request.path ?? '').toLowerCase();
|
|
340
|
+
access(request) {
|
|
341
|
+
// `request.baseUrl` is the mount Express matched — `/owners`, or
|
|
342
|
+
// `/api/owners` under ORM_REST_ROUTE=/api. Never parse `originalUrl`: it is
|
|
343
|
+
// the raw request target and can be absolute-form.
|
|
344
|
+
const mount = request.baseUrl;
|
|
345
|
+
|
|
346
|
+
// FAIL CLOSED. If Express did not tell us what it matched we are not behind
|
|
347
|
+
// the mount we think we are, and an unidentifiable request denies rather
|
|
348
|
+
// than falling through to the CRUD grant at the bottom.
|
|
349
|
+
if (typeof mount !== 'string' || mount === '') return false;
|
|
350
|
+
|
|
351
|
+
// Lower-cased because the router matched case-INSENSITIVELY, and a matcher
|
|
352
|
+
// stricter than the router that dispatched the request can be stepped
|
|
353
|
+
// around. The PATH only — record ids stay at their real case below.
|
|
354
|
+
const collection = mount.toLowerCase();
|
|
381
355
|
|
|
356
|
+
// `request.path` is mount-relative and query-free, so sub-path rules need no
|
|
357
|
+
// prefix arithmetic either. false → 403 for the whole request.
|
|
358
|
+
const path = String(request.path ?? '').toLowerCase();
|
|
359
|
+
|
|
360
|
+
if (collection.endsWith('/owners')) {
|
|
382
361
|
if (path === '/archived' || path.startsWith('/archived/')) return false;
|
|
383
362
|
|
|
384
363
|
// Returning a function plugs it in as a per-record filter, and it is
|
|
@@ -394,7 +373,7 @@ export default class GlobalAccess {
|
|
|
394
373
|
// inert. Deliberately NO `?? record.owner` fallback: accepting the raw
|
|
395
374
|
// shape as well as the resolved one would absorb a resolution regression
|
|
396
375
|
// silently, which is exactly what blinded this fixture before.
|
|
397
|
-
if (
|
|
376
|
+
if (collection.endsWith('/animals')) return record => record.owner?.id !== 'restricted';
|
|
398
377
|
|
|
399
378
|
// Allows full access to all calls that don't match any of the above conditions
|
|
400
379
|
return ['read', 'create', 'update', 'delete'];
|
|
@@ -544,10 +523,9 @@ sample, `getAccess('owner') === getAccess('animal')`.
|
|
|
544
523
|
#### Passing the context makes a model-correct answer *possible*
|
|
545
524
|
|
|
546
525
|
It does not make the answer model-correct on its own. **The resolved predicate
|
|
547
|
-
has to read the context.**
|
|
548
|
-
|
|
549
|
-
**animals
|
|
550
|
-
[#222](https://github.com/abofs/stonyx-orm/issues/222) answered:
|
|
526
|
+
has to read the context.** Measured against the access class shipped with this
|
|
527
|
+
repo, on a request Express dispatched to `GET /owners/angela`, asked about
|
|
528
|
+
**animals**:
|
|
551
529
|
|
|
552
530
|
```
|
|
553
531
|
getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
@@ -555,26 +533,18 @@ getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
|
555
533
|
```
|
|
556
534
|
|
|
557
535
|
That is the **owners** filter, and it returns `true` for animal 21 — the record
|
|
558
|
-
hidden on every animal surface. Under a mount
|
|
559
|
-
|
|
560
|
-
`['read', 'create', 'update', 'delete']`, a full CRUD grant.
|
|
561
|
-
|
|
562
|
-
the
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
and
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
getAccess('animal')(ownersRequest, { model: 'animal', operation: 'read' })
|
|
571
|
-
-> record => record.owner?.id !== 'restricted'
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
A single-argument predicate remains the default in every consumer tree, and a
|
|
575
|
-
caller has no supported way to tell which kind it resolved. The boot-time arity
|
|
576
|
-
warning that surfaces one is
|
|
577
|
-
[#221](https://github.com/abofs/stonyx-orm/issues/221).
|
|
536
|
+
hidden on every animal surface. Under a mount that predicate recognizes neither
|
|
537
|
+
way it is worse still: it falls through to
|
|
538
|
+
`['read', 'create', 'update', 'delete']`, a full CRUD grant.
|
|
539
|
+
|
|
540
|
+
Either way the context was supplied and the answer is not the animal answer, and
|
|
541
|
+
it is wrong in the direction that **grants**. That predicate is single-argument
|
|
542
|
+
and identifies its collection from the request, so it answered about the
|
|
543
|
+
collection the request is *addressed to* while being asked about another one.
|
|
544
|
+
Every predicate in this repo, and in every consumer tree, is single-argument on
|
|
545
|
+
the day this ships, and a caller has no supported way to tell which kind it
|
|
546
|
+
resolved. The boot-time arity warning that would surface it is
|
|
547
|
+
[#213](https://github.com/abofs/stonyx-orm/issues/213).
|
|
578
548
|
|
|
579
549
|
So: pass the context, and do not treat a resolved predicate's answer as
|
|
580
550
|
model-specific until that predicate has been migrated to read it.
|
|
@@ -611,7 +581,7 @@ a write to a *different* collection can still re-parent one. See
|
|
|
611
581
|
| `GET /:models/:id/relationships/{relationship}` | `404` — same |
|
|
612
582
|
| `PATCH /:models/:id` | `404`, no attribute is applied |
|
|
613
583
|
| `DELETE /:models/:id` | `404`, the record is not removed and no SQL `DELETE` is issued |
|
|
614
|
-
| `POST /:models` | `403`, and a record **this request inserted** is rolled back — see [Known limitations](#known-limitations) for the
|
|
584
|
+
| `POST /:models` | `403`, and a record **this request inserted** is rolled back — see [Known limitations](#known-limitations) for why the rollback is conditional |
|
|
615
585
|
|
|
616
586
|
**Denied record-level requests return 404, not 403.** This is deliberate and it
|
|
617
587
|
is the property most easily "improved" away. 403 would confirm that the record
|
|
@@ -645,23 +615,35 @@ chooses the id and learns whether the create succeeded — so under a filter the
|
|
|
645
615
|
caller does not choose the id. The refusal happens before any store lookup, so
|
|
646
616
|
neither the status nor the response time depends on whether the id exists.
|
|
647
617
|
|
|
648
|
-
Let the server assign the id and read it back from the response
|
|
649
|
-
|
|
650
|
-
|
|
618
|
+
Let the server assign the id and read it back from the response — and read it
|
|
619
|
+
back rather than predicting it, because the value it returns is documented but
|
|
620
|
+
not stable across model kinds: a numeric-id model gets `max + 1` (or, at the
|
|
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.
|
|
651
641
|
|
|
652
642
|
### Identifying the collection
|
|
653
643
|
|
|
654
|
-
**Do not reconstruct the request path
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
[the access context](#the-access-context-second-argument): it is fixed at mount
|
|
658
|
-
time, no request can influence it, and there is nothing left to parse.
|
|
659
|
-
|
|
660
|
-
**Everything below is the record of what happened when this sample did parse
|
|
661
|
-
it.** It is kept as history, not as a recipe — none of these matching strategies
|
|
662
|
-
should be written into a new predicate. Every version of this sample that tried
|
|
663
|
-
to identify the collection from the request target failed **open**, and each
|
|
664
|
-
variant was found only after the previous one was fixed:
|
|
644
|
+
**Do not reconstruct the request path.** Every version of this sample that tried
|
|
645
|
+
to has failed **open**, and each variant was found only after the previous one
|
|
646
|
+
was fixed:
|
|
665
647
|
|
|
666
648
|
| # | Variant | Why it fails open |
|
|
667
649
|
|---|---|---|
|
|
@@ -671,19 +653,13 @@ variant was found only after the previous one was fixed:
|
|
|
671
653
|
| 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. |
|
|
672
654
|
| 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. |
|
|
673
655
|
|
|
674
|
-
**The fix is not a sixth rule
|
|
675
|
-
to stop identifying the collection at all.
|
|
656
|
+
**The fix is not a sixth rule.** It is to stop parsing:
|
|
676
657
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
target (variant 5). It was still a transport artifact standing in for a
|
|
683
|
-
structural fact, and it is **no longer what the sample does**: the sample reads
|
|
684
|
-
`model`, so all five variants are unconstructible against it rather than
|
|
685
|
-
handled. The table below is retained as the measured evidence behind the five
|
|
686
|
-
variants, not because any of these values should be matched on:
|
|
658
|
+
**Use `request.baseUrl`.** It is the mount Express *actually matched* when it
|
|
659
|
+
dispatched the request. It carries no query string (variant 2), it is not
|
|
660
|
+
mount-relative (variant 1), it already contains the configured `ORM_REST_ROUTE`
|
|
661
|
+
prefix (variant 4 — there is nothing left to derive, so `/apiowners` is
|
|
662
|
+
unconstructible), and it is unaffected by an absolute-form target (variant 5).
|
|
687
663
|
|
|
688
664
|
| request | `request.url` | `request.originalUrl` | `request.baseUrl` | `request.path` |
|
|
689
665
|
|---|---|---|---|---|
|
|
@@ -694,23 +670,21 @@ variants, not because any of these values should be matched on:
|
|
|
694
670
|
| `GET http://anything.example/owners/angela` | `http://anything.example/angela` | `http://anything.example/owners/angela` | `/owners` | `/angela` |
|
|
695
671
|
| `GET /api/animals/22` (`ORM_REST_ROUTE=/api`) | `/22` | `/api/animals/22` | `/api/animals` | `/22` |
|
|
696
672
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
an absent `model` rather than falling through. An input you cannot identify must
|
|
713
|
-
**deny**.
|
|
673
|
+
Two rules remain, and they are the whole list:
|
|
674
|
+
|
|
675
|
+
**1. Compare lower-cased.** `baseUrl` is the text the caller sent, not the
|
|
676
|
+
registered mount — `GET /OwNeRs/angela` yields `/OwNeRs`. The router matched it
|
|
677
|
+
case-insensitively, so a case-sensitive comparison here is stricter than the
|
|
678
|
+
router and can be walked past. Lower-case the **mount and path only**; record ids
|
|
679
|
+
are case-sensitive and must be compared at their real case.
|
|
680
|
+
|
|
681
|
+
**2. Fail closed when `baseUrl` is absent.** `String(request.originalUrl ?? '')`
|
|
682
|
+
was added to stop a `TypeError`, and it traded fail-closed for fail-**open**: an
|
|
683
|
+
empty string matches no collection, so `access()` fell through to the permission
|
|
684
|
+
array and granted full CRUD. An input you cannot identify must **deny**.
|
|
685
|
+
|
|
686
|
+
Use `request.path` — mount-relative and query-free — if you need to distinguish
|
|
687
|
+
sub-paths beneath the mount, as the `/archived` deny above does.
|
|
714
688
|
|
|
715
689
|
### Known limitations
|
|
716
690
|
|
|
@@ -733,14 +707,11 @@ an absent `model` rather than falling through. An input you cannot identify must
|
|
|
733
707
|
see [The access context](#the-access-context-second-argument):
|
|
734
708
|
`Orm.instance.getAccess(modelName)` makes another model's predicate
|
|
735
709
|
**reachable**, and `context.model` makes a **model-correct answer possible** —
|
|
736
|
-
possible, not guaranteed: the resolved predicate has to read the context
|
|
737
|
-
|
|
738
|
-
([#
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
addressed to, and the boot-time warning that surfaces one is
|
|
742
|
-
[#221](https://github.com/abofs/stonyx-orm/issues/221). **The mechanism
|
|
743
|
-
exists; the ORM does not yet use it on this path.** The re-parenting write above is still
|
|
710
|
+
possible, not guaranteed: the resolved predicate has to read the context, and
|
|
711
|
+
every predicate in tree is still single-argument
|
|
712
|
+
([#213](https://github.com/abofs/stonyx-orm/issues/213)), so today it answers
|
|
713
|
+
about the collection the request is addressed to. **The mechanism exists; the
|
|
714
|
+
ORM does not yet use it on this path.** The re-parenting write above is still
|
|
744
715
|
**not refused** — that enforcement is
|
|
745
716
|
[#196](https://github.com/abofs/stonyx-orm/issues/196) and
|
|
746
717
|
[#207](https://github.com/abofs/stonyx-orm/issues/207), which were blocked on
|
|
@@ -792,9 +763,10 @@ an absent `model` rather than falling through. An input you cannot identify must
|
|
|
792
763
|
transform output differs from its lookup key is the same defect. Filtered
|
|
793
764
|
collections are unaffected — breaking change 3 refuses any client-supplied id
|
|
794
765
|
— so this reaches consumers with **no** function-style filter. Tracked as
|
|
795
|
-
[#205](https://github.com/abofs/stonyx-orm/issues/205)
|
|
796
|
-
[#203](https://github.com/abofs/stonyx-orm/issues/203)
|
|
797
|
-
|
|
766
|
+
[#205](https://github.com/abofs/stonyx-orm/issues/205). Its sibling
|
|
767
|
+
[#203](https://github.com/abofs/stonyx-orm/issues/203) — a **server-assigned**
|
|
768
|
+
id landing on an occupied slot — is **fixed**; see breaking change 8. #205 is
|
|
769
|
+
the client-supplied half and is still open.
|
|
798
770
|
- **`context.record` is `undefined` for an after-`create` hook when a string-id
|
|
799
771
|
model is given a numeric-looking id.** The post-create lookup uses the same id
|
|
800
772
|
coercion as every other surface, which resolves `'9107'` to the number `9107`,
|
|
@@ -804,15 +776,18 @@ an absent `model` rather than falling through. An input you cannot identify must
|
|
|
804
776
|
[#209](https://github.com/abofs/stonyx-orm/issues/209).
|
|
805
777
|
- **A denied `POST` rolls back only a record it *inserted*.** The rollback
|
|
806
778
|
requires the store to have grown, because removing by id alone is a write
|
|
807
|
-
primitive keyed by a caller-supplied value.
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
last-*inserted* + 1,
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
779
|
+
primitive keyed by a caller-supplied value. **The reachability condition this
|
|
780
|
+
bullet used to state is gone**: it was
|
|
781
|
+
[#203](https://github.com/abofs/stonyx-orm/issues/203) — `assignRecordId`
|
|
782
|
+
returned last-*inserted* + 1, so a server-assigned id could land on an
|
|
783
|
+
occupied slot and `createRecord` would update it in place — and #203 is fixed
|
|
784
|
+
(breaking change 8). A server-assigned create can no longer overwrite, so on a
|
|
785
|
+
collection whose only id channel is `createHandler` this guard has no
|
|
786
|
+
observable effect today. It is kept because a caller-supplied id reaching
|
|
787
|
+
`createRecord` from another route — a relationship write,
|
|
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.
|
|
816
791
|
|
|
817
792
|
### Breaking changes
|
|
818
793
|
|
|
@@ -873,6 +848,39 @@ they are recorded here.
|
|
|
873
848
|
population breaking changes 3 and 4 explicitly exempt. If you were relying on
|
|
874
849
|
a hex-shaped or whitespace-padded id creating a second record, it never did.
|
|
875
850
|
|
|
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
|
+
|
|
876
884
|
### Include Parameter (Sideloading Relationships)
|
|
877
885
|
|
|
878
886
|
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.
|