@stonyx/orm 0.3.2-alpha.106 → 0.3.2-alpha.108
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 +97 -8
- package/dist/index.d.ts +1 -0
- package/dist/normalize-record-id.d.ts +16 -2
- package/dist/normalize-record-id.js +16 -2
- package/dist/orm-request.d.ts +16 -2
- package/dist/orm-request.js +19 -9
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/normalize-record-id.ts +16 -2
- package/src/orm-request.ts +39 -11
package/README.md
CHANGED
|
@@ -381,8 +381,33 @@ to keep in sync — abofs/stonyx-orm#270. It is `undefined` on collection routes
|
|
|
381
381
|
which is how you tell a collection request from a record request. `params.id` is
|
|
382
382
|
left untouched, so anything that needs the raw client text still has it.
|
|
383
383
|
|
|
384
|
-
|
|
385
|
-
|
|
384
|
+
**Version floor: `request.recordId` requires the release that carries
|
|
385
|
+
abofs/stonyx-orm#270.** Measured on the published tarballs: `0.3.2-alpha.106`
|
|
386
|
+
(this change's alpha) has `request.recordId` and exports `normalizeRecordId`;
|
|
387
|
+
`0.3.2-beta.231`, the newest build on the `beta` tag at the time of writing, has
|
|
388
|
+
neither, and `latest` is `0.3.1`. **On any earlier build the samples above fail
|
|
389
|
+
open, completely** — measured against `origin/dev`: `recordId` is `undefined` on
|
|
390
|
+
every route, so `if (recordId === undefined) return record => …` fires on the
|
|
391
|
+
*record* route, and a function return authorizes the request outright.
|
|
392
|
+
`GET /owners/angela` and `GET /animals/7` both returned 200 and served the
|
|
393
|
+
protected record, and `DELETE` on both returned 204 and destroyed it.
|
|
394
|
+
|
|
395
|
+
If you cannot pin the version, make the samples fail *closed* instead — one line,
|
|
396
|
+
a no-op on a new build and a refusal on an old one:
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
access(request) {
|
|
400
|
+
// Old builds do not attach recordId. Refuse rather than fall through to the
|
|
401
|
+
// collection branch, which would authorize the record route outright.
|
|
402
|
+
if (!('recordId' in request)) return false;
|
|
403
|
+
|
|
404
|
+
const { recordId } = request;
|
|
405
|
+
// …
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
The same normalisation is exported for the places `access()` does not reach —
|
|
410
|
+
hooks, custom handlers, your own lookups:
|
|
386
411
|
|
|
387
412
|
```javascript
|
|
388
413
|
import { normalizeRecordId } from '@stonyx/orm';
|
|
@@ -393,6 +418,15 @@ normalizeRecordId('0x7'); // 7 — the same value the ORM resolves by
|
|
|
393
418
|
Do not re-implement it. A hand-written copy is correct only for as long as it
|
|
394
419
|
happens to match, and nothing holds the two together.
|
|
395
420
|
|
|
421
|
+
**`normalizeRecordId(undefined)` is `''`, not `undefined`.** The two values this
|
|
422
|
+
section documents side by side do not agree, and the difference is load-bearing:
|
|
423
|
+
`request.recordId` is `undefined` on a collection route, while
|
|
424
|
+
`normalizeRecordId` returns `''` for any falsy id — including `undefined` — because
|
|
425
|
+
`store.get(key, undefined)` returns the whole model Map rather than a record
|
|
426
|
+
(abofs/stonyx-orm#167). So `normalizeRecordId(context.params.id) === undefined`
|
|
427
|
+
is **never** true and a collection branch written that way never runs. Branch on
|
|
428
|
+
`request.recordId === undefined`, or compare against `''`.
|
|
429
|
+
|
|
396
430
|
```javascript
|
|
397
431
|
export default class AnimalAccess {
|
|
398
432
|
models = ['animal'];
|
|
@@ -633,9 +667,13 @@ beforeHook('create', 'animal', (context) => {
|
|
|
633
667
|
}
|
|
634
668
|
});
|
|
635
669
|
|
|
636
|
-
// Return an object to send a custom response
|
|
670
|
+
// Return an object to send a custom response.
|
|
671
|
+
// Look the record up by `context.request.recordId` — the value the ORM
|
|
672
|
+
// resolved the record by. `context.params.id` is the raw client text and is a
|
|
673
|
+
// different value for every alias of the same id, so a lookup keyed on it
|
|
674
|
+
// finds nothing on a numeric-id model (abofs/stonyx-orm#270).
|
|
637
675
|
beforeHook('delete', 'animal', (context) => {
|
|
638
|
-
const animal = store.get('animal', context.
|
|
676
|
+
const animal = store.get('animal', context.request.recordId);
|
|
639
677
|
if (animal.protected) {
|
|
640
678
|
return { errors: [{ detail: 'Cannot delete protected animals' }] };
|
|
641
679
|
}
|
|
@@ -676,9 +714,12 @@ afterHook('update', 'animal', async (context) => {
|
|
|
676
714
|
}
|
|
677
715
|
});
|
|
678
716
|
|
|
679
|
-
// Cache invalidation
|
|
717
|
+
// Cache invalidation.
|
|
718
|
+
// Keyed on `recordId`, not `params.id`: `/animals/7` and `/animals/007` are
|
|
719
|
+
// one record but two strings, so a raw-text key invalidates two entries and
|
|
720
|
+
// misses the one the write used.
|
|
680
721
|
afterHook('delete', 'animal', async (context) => {
|
|
681
|
-
await cache.invalidate(`owner:${context.
|
|
722
|
+
await cache.invalidate(`owner:${context.request.recordId}:pets`);
|
|
682
723
|
});
|
|
683
724
|
```
|
|
684
725
|
|
|
@@ -719,10 +760,16 @@ afterHook('delete', 'animal', async (context) => {
|
|
|
719
760
|
#### Authorization
|
|
720
761
|
|
|
721
762
|
```javascript
|
|
722
|
-
// Additional access control - halt with 403 if unauthorized
|
|
763
|
+
// Additional access control - halt with 403 if unauthorized.
|
|
764
|
+
//
|
|
765
|
+
// `context.request.recordId` is the id the ORM resolved the record by — the
|
|
766
|
+
// same value `access()` is handed. Authorizing on `context.params.id` instead
|
|
767
|
+
// looks up the raw client text: on a numeric-id model that lookup returns
|
|
768
|
+
// `undefined` for EVERY spelling, `animal.owner` throws, and the check never
|
|
769
|
+
// runs. Measured; abofs/stonyx-orm#270.
|
|
723
770
|
beforeHook('delete', 'animal', (context) => {
|
|
724
771
|
const user = context.state.currentUser;
|
|
725
|
-
const animal = store.get('animal', context.
|
|
772
|
+
const animal = store.get('animal', context.request.recordId);
|
|
726
773
|
|
|
727
774
|
if (animal.owner !== user.id && !user.isAdmin) {
|
|
728
775
|
return 403; // Forbidden
|
|
@@ -923,6 +970,48 @@ test('validation hook rejects negative age', async () => {
|
|
|
923
970
|
| `afterHook` | Register an after hook for post-operation logic. |
|
|
924
971
|
| `clearHook` | Clear hooks for a specific operation:model. |
|
|
925
972
|
| `clearAllHooks` | Clear all registered hooks (useful for testing). |
|
|
973
|
+
| `normalizeRecordId` | Turn a raw URL id into the value the ORM resolves the record by. |
|
|
974
|
+
|
|
975
|
+
### `normalizeRecordId(id)`
|
|
976
|
+
|
|
977
|
+
```ts
|
|
978
|
+
normalizeRecordId(id?: string | null): string | number
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
The **one** implementation of URL-id normalisation in the package
|
|
982
|
+
(abofs/stonyx-orm#270). `access()` is already handed its result as
|
|
983
|
+
`request.recordId`; import it for the places `access()` does not reach — hooks,
|
|
984
|
+
custom handlers, your own lookups. Synchronous, and it must stay synchronous:
|
|
985
|
+
`auth()` is invoked without `await`.
|
|
986
|
+
|
|
987
|
+
| Input | Returns | Note |
|
|
988
|
+
| --- | --- | --- |
|
|
989
|
+
| `'7'`, `'007'`, `'7.0'`, `'7.9'`, `'7e0'`, `'0x7'`, `'+7'`, `' 7'` | `7` (number) | `parseInt` with **no radix**; passing a radix of 10 would make this `0` |
|
|
990
|
+
| `'angela'`, `'ANGELA'` | the same string, case included | a non-numeric id is passed through untouched |
|
|
991
|
+
| `'0'`, `'00'`, `'-0'`, `'0x0'` | `0` (number) | falsy, and a legitimate record id |
|
|
992
|
+
| `' '`, `'\t'`, `'\n'`, `'\u00a0'` | `NaN` | whitespace-only ids are numeric to `isNaN` but parse to nothing |
|
|
993
|
+
| `''`, `null`, `undefined` | `''` (empty string) | **not** `undefined` — see the trap below |
|
|
994
|
+
|
|
995
|
+
**The trap.** `request.recordId` is `undefined` on a collection route;
|
|
996
|
+
`normalizeRecordId(undefined)` is `''`. They are different values and a
|
|
997
|
+
collection check written against the wrong one silently never fires:
|
|
998
|
+
|
|
999
|
+
```javascript
|
|
1000
|
+
// WRONG — never true, so this branch never runs
|
|
1001
|
+
if (normalizeRecordId(context.params.id) === undefined) { /* … */ }
|
|
1002
|
+
|
|
1003
|
+
// Right — the ORM attaches undefined for "this route carries no :id"
|
|
1004
|
+
if (context.request.recordId === undefined) { /* collection route */ }
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
The `''` is deliberate: `store.get(key, undefined)` returns the whole model Map
|
|
1008
|
+
rather than a record (abofs/stonyx-orm#167), so the resolution path depends on
|
|
1009
|
+
the empty string this function returns today.
|
|
1010
|
+
|
|
1011
|
+
**A normalised id is not a promise that a record exists.** `0` and `NaN` are both
|
|
1012
|
+
possible returns and neither addresses a record you can rely on — `NaN` is not
|
|
1013
|
+
even `===` itself, so `recordId === NaN` can never be written as a guard. Treat
|
|
1014
|
+
`recordId` as "the key the lookup will use", not as "a record is there".
|
|
926
1015
|
|
|
927
1016
|
## Project Structure
|
|
928
1017
|
|
package/dist/index.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export { Model, View, Serializer };
|
|
|
13
13
|
export { attr, belongsTo, hasMany, createRecord, updateRecord };
|
|
14
14
|
export { count, avg, sum, min, max };
|
|
15
15
|
export { default as normalizeRecordId } from './normalize-record-id.js';
|
|
16
|
+
export type { OrmRequest$ as OrmAccessRequest } from './orm-request.js';
|
|
16
17
|
export { beforeHook, afterHook, clearHook, clearAllHooks } from './hooks.js';
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* The ONE place a URL id is turned into the value a record is resolved by —
|
|
3
3
|
* abofs/stonyx-orm#270.
|
|
4
4
|
*
|
|
5
|
-
* Before this existed
|
|
5
|
+
* Before this existed this coercion was written out seven times: twice in
|
|
6
6
|
* README.md, once in docs/usage-patterns.md, and four times inside the
|
|
7
|
-
* framework.
|
|
7
|
+
* framework. Seven copies, not seven identical copies — the three persistence
|
|
8
|
+
* ones (abofs/stonyx-orm#282) omit the `if (!id) return ''` guard below, which
|
|
9
|
+
* is the point: nothing held them together, so they had already drifted.
|
|
10
|
+
* The framework's copy — `getId()`, module-private in
|
|
8
11
|
* orm-request.ts, unreachable through the package `exports` map — was the one
|
|
9
12
|
* that decided which record a request addressed, and a consumer's `access()`
|
|
10
13
|
* predicate had no way to obtain it. So the framework resolved the record by
|
|
@@ -47,6 +50,17 @@
|
|
|
47
50
|
* @stonyx/rest-server, so a promise here would be handed to `access()` as the
|
|
48
51
|
* record id.
|
|
49
52
|
*
|
|
53
|
+
* FALSY AND `NaN` RETURNS ARE LOAD-BEARING ELSEWHERE — abofs/stonyx-orm#287.
|
|
54
|
+
* `''` is returned for any falsy id because `store.get(key, undefined)` returns
|
|
55
|
+
* the whole model Map rather than a record (abofs/stonyx-orm#167). But `''` is
|
|
56
|
+
* not the only falsy return: `'0'`, `'00'`, `'-0'` and `'0x0'` all normalise to
|
|
57
|
+
* `0`, and `' '`, `'\t'`, `'\n'`, `'\u00a0'` all normalise to `NaN` (their
|
|
58
|
+
* `Number()` is `0`, so the `isNaN` guard does not fire and `parseInt` runs).
|
|
59
|
+
* `store.remove(key, id)` branches on truthiness, so those spellings reach a
|
|
60
|
+
* fall-through this function does not own. Tracked as #287; every row is pinned
|
|
61
|
+
* in test/unit/normalize-record-id-test.ts so a cleanup here cannot move the
|
|
62
|
+
* boundary #287 is measured against.
|
|
63
|
+
*
|
|
50
64
|
* @param id the raw, already-URL-decoded id text from `request.params.id`
|
|
51
65
|
* @returns the value the ORM resolves the record by
|
|
52
66
|
*/
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
* The ONE place a URL id is turned into the value a record is resolved by —
|
|
18
18
|
* abofs/stonyx-orm#270.
|
|
19
19
|
*
|
|
20
|
-
* Before this existed
|
|
20
|
+
* Before this existed this coercion was written out seven times: twice in
|
|
21
21
|
* README.md, once in docs/usage-patterns.md, and four times inside the
|
|
22
|
-
* framework.
|
|
22
|
+
* framework. Seven copies, not seven identical copies — the three persistence
|
|
23
|
+
* ones (abofs/stonyx-orm#282) omit the `if (!id) return ''` guard below, which
|
|
24
|
+
* is the point: nothing held them together, so they had already drifted.
|
|
25
|
+
* The framework's copy — `getId()`, module-private in
|
|
23
26
|
* orm-request.ts, unreachable through the package `exports` map — was the one
|
|
24
27
|
* that decided which record a request addressed, and a consumer's `access()`
|
|
25
28
|
* predicate had no way to obtain it. So the framework resolved the record by
|
|
@@ -62,6 +65,17 @@
|
|
|
62
65
|
* @stonyx/rest-server, so a promise here would be handed to `access()` as the
|
|
63
66
|
* record id.
|
|
64
67
|
*
|
|
68
|
+
* FALSY AND `NaN` RETURNS ARE LOAD-BEARING ELSEWHERE — abofs/stonyx-orm#287.
|
|
69
|
+
* `''` is returned for any falsy id because `store.get(key, undefined)` returns
|
|
70
|
+
* the whole model Map rather than a record (abofs/stonyx-orm#167). But `''` is
|
|
71
|
+
* not the only falsy return: `'0'`, `'00'`, `'-0'` and `'0x0'` all normalise to
|
|
72
|
+
* `0`, and `' '`, `'\t'`, `'\n'`, `'\u00a0'` all normalise to `NaN` (their
|
|
73
|
+
* `Number()` is `0`, so the `isNaN` guard does not fire and `parseInt` runs).
|
|
74
|
+
* `store.remove(key, id)` branches on truthiness, so those spellings reach a
|
|
75
|
+
* fall-through this function does not own. Tracked as #287; every row is pinned
|
|
76
|
+
* in test/unit/normalize-record-id-test.ts so a cleanup here cannot move the
|
|
77
|
+
* boundary #287 is measured against.
|
|
78
|
+
*
|
|
65
79
|
* @param id the raw, already-URL-decoded id text from `request.params.id`
|
|
66
80
|
* @returns the value the ORM resolves the record by
|
|
67
81
|
*/
|
package/dist/orm-request.d.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { Request } from '@stonyx/rest-server';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The request object a consumer's `access()` predicate receives.
|
|
4
|
+
*
|
|
5
|
+
* Exported because `recordId` is public API — README's `access()` samples
|
|
6
|
+
* destructure it — and a public runtime field with an unreachable type asks the
|
|
7
|
+
* consumer to re-declare something the framework already knows, which is
|
|
8
|
+
* abofs/stonyx-orm#270's own defect shape one layer over into the type surface.
|
|
9
|
+
* `HookContext` (src/hooks.ts) is this repo's precedent for exporting the
|
|
10
|
+
* interface a consumer is handed. Re-exported from the root barrel as
|
|
11
|
+
* `OrmAccessRequest` (src/index.ts) — NOT as `OrmRequest`, which is already the
|
|
12
|
+
* default-exported CLASS in this file and means something else. The
|
|
13
|
+
* `./orm-request` subpath is not in the `exports` map, so the barrel is the
|
|
14
|
+
* only reachable spelling.
|
|
15
|
+
*/
|
|
16
|
+
export interface OrmRequest$ extends Request {
|
|
3
17
|
protocol?: string;
|
|
4
18
|
baseUrl?: string;
|
|
5
19
|
method: string;
|
|
6
20
|
params: {
|
|
7
21
|
[key: string]: string;
|
|
8
22
|
};
|
|
9
|
-
recordId?: string | number;
|
|
23
|
+
recordId?: string | number | undefined;
|
|
10
24
|
body?: {
|
|
11
25
|
[key: string]: unknown;
|
|
12
26
|
};
|
package/dist/orm-request.js
CHANGED
|
@@ -66,11 +66,21 @@ function getBaseUrl(request, pluralizedModel) {
|
|
|
66
66
|
const prefix = mountPath.endsWith(modelSegment) ? mountPath.slice(0, -modelSegment.length) : '';
|
|
67
67
|
return `${protocol}://${host}${prefix}`;
|
|
68
68
|
}
|
|
69
|
-
// Kept as a name because twelve call sites read
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
69
|
+
// Kept as a name because twelve `getId(...)` call sites read it. It is now a
|
|
70
|
+
// thin delegate: there is exactly ONE normalisation of a URL id in the repo,
|
|
71
|
+
// and it is the exported one a consumer can import.
|
|
72
|
+
//
|
|
73
|
+
// "of a URL id" is the load-bearing qualifier, and it is the same one
|
|
74
|
+
// src/normalize-record-id.ts:18 carries. Three copies of the coercion survive
|
|
75
|
+
// at this head — src/orm-request.ts (the create-response path),
|
|
76
|
+
// src/postgres/postgres-db.ts and src/mysql/mysql-db.ts — but each normalises a
|
|
77
|
+
// RESPONSE id (`response?.data?.id`), not a URL id, and each omits this
|
|
78
|
+
// function's `if (!id) return ''` guard. They are tracked as
|
|
79
|
+
// abofs/stonyx-orm#282 and enumerated by name in AC-5's allowlist
|
|
80
|
+
// (test/integration/readme-sample-test.ts).
|
|
81
|
+
//
|
|
82
|
+
// A second implementation of the URL-id normalisation here is the defect
|
|
83
|
+
// abofs/stonyx-orm#270 exists to remove — see src/normalize-record-id.ts.
|
|
74
84
|
function getId(params) {
|
|
75
85
|
return normalizeRecordId(params.id);
|
|
76
86
|
}
|
|
@@ -493,10 +503,10 @@ export default class OrmRequest extends Request {
|
|
|
493
503
|
// handlers; this line is the contract.
|
|
494
504
|
//
|
|
495
505
|
// `request.params` is deliberately NOT mutated. Twelve `getId(...)` call
|
|
496
|
-
// sites
|
|
497
|
-
//
|
|
498
|
-
// string to number underneath them is a silent behaviour
|
|
499
|
-
// this issue is not about.
|
|
506
|
+
// sites read it, and `_withHooks` assigns `params: request.params` onto the
|
|
507
|
+
// hook context, so every consumer hook reads the same object. Changing
|
|
508
|
+
// `params.id` from string to number underneath them is a silent behaviour
|
|
509
|
+
// change on paths this issue is not about.
|
|
500
510
|
//
|
|
501
511
|
// Synchronous by necessity: @stonyx/rest-server calls auth() without
|
|
502
512
|
// awaiting it.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export { Model, View, Serializer }; // base classes
|
|
|
31
31
|
export { attr, belongsTo, hasMany, createRecord, updateRecord }; // helpers
|
|
32
32
|
export { count, avg, sum, min, max }; // aggregate helpers
|
|
33
33
|
export { default as normalizeRecordId } from './normalize-record-id.js'; // canonical URL-id -> record-id resolution (#270)
|
|
34
|
+
export type { OrmRequest$ as OrmAccessRequest } from './orm-request.js'; // the request access() is handed, incl. recordId (#270)
|
|
34
35
|
export { beforeHook, afterHook, clearHook, clearAllHooks } from './hooks.js'; // middleware hooks
|
|
35
36
|
|
|
36
37
|
// Store API:
|
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
* The ONE place a URL id is turned into the value a record is resolved by —
|
|
19
19
|
* abofs/stonyx-orm#270.
|
|
20
20
|
*
|
|
21
|
-
* Before this existed
|
|
21
|
+
* Before this existed this coercion was written out seven times: twice in
|
|
22
22
|
* README.md, once in docs/usage-patterns.md, and four times inside the
|
|
23
|
-
* framework.
|
|
23
|
+
* framework. Seven copies, not seven identical copies — the three persistence
|
|
24
|
+
* ones (abofs/stonyx-orm#282) omit the `if (!id) return ''` guard below, which
|
|
25
|
+
* is the point: nothing held them together, so they had already drifted.
|
|
26
|
+
* The framework's copy — `getId()`, module-private in
|
|
24
27
|
* orm-request.ts, unreachable through the package `exports` map — was the one
|
|
25
28
|
* that decided which record a request addressed, and a consumer's `access()`
|
|
26
29
|
* predicate had no way to obtain it. So the framework resolved the record by
|
|
@@ -63,6 +66,17 @@
|
|
|
63
66
|
* @stonyx/rest-server, so a promise here would be handed to `access()` as the
|
|
64
67
|
* record id.
|
|
65
68
|
*
|
|
69
|
+
* FALSY AND `NaN` RETURNS ARE LOAD-BEARING ELSEWHERE — abofs/stonyx-orm#287.
|
|
70
|
+
* `''` is returned for any falsy id because `store.get(key, undefined)` returns
|
|
71
|
+
* the whole model Map rather than a record (abofs/stonyx-orm#167). But `''` is
|
|
72
|
+
* not the only falsy return: `'0'`, `'00'`, `'-0'` and `'0x0'` all normalise to
|
|
73
|
+
* `0`, and `' '`, `'\t'`, `'\n'`, `'\u00a0'` all normalise to `NaN` (their
|
|
74
|
+
* `Number()` is `0`, so the `isNaN` guard does not fire and `parseInt` runs).
|
|
75
|
+
* `store.remove(key, id)` branches on truthiness, so those spellings reach a
|
|
76
|
+
* fall-through this function does not own. Tracked as #287; every row is pinned
|
|
77
|
+
* in test/unit/normalize-record-id-test.ts so a cleanup here cannot move the
|
|
78
|
+
* boundary #287 is measured against.
|
|
79
|
+
*
|
|
66
80
|
* @param id the raw, already-URL-decoded id text from `request.params.id`
|
|
67
81
|
* @returns the value the ORM resolves the record by
|
|
68
82
|
*/
|
package/src/orm-request.ts
CHANGED
|
@@ -9,7 +9,21 @@ import type { OrmRecord } from './types/orm-types.js';
|
|
|
9
9
|
import { isOrmRecord } from './utils.js';
|
|
10
10
|
import normalizeRecordId from './normalize-record-id.js';
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* The request object a consumer's `access()` predicate receives.
|
|
14
|
+
*
|
|
15
|
+
* Exported because `recordId` is public API — README's `access()` samples
|
|
16
|
+
* destructure it — and a public runtime field with an unreachable type asks the
|
|
17
|
+
* consumer to re-declare something the framework already knows, which is
|
|
18
|
+
* abofs/stonyx-orm#270's own defect shape one layer over into the type surface.
|
|
19
|
+
* `HookContext` (src/hooks.ts) is this repo's precedent for exporting the
|
|
20
|
+
* interface a consumer is handed. Re-exported from the root barrel as
|
|
21
|
+
* `OrmAccessRequest` (src/index.ts) — NOT as `OrmRequest`, which is already the
|
|
22
|
+
* default-exported CLASS in this file and means something else. The
|
|
23
|
+
* `./orm-request` subpath is not in the `exports` map, so the barrel is the
|
|
24
|
+
* only reachable spelling.
|
|
25
|
+
*/
|
|
26
|
+
export interface OrmRequest$ extends Request {
|
|
13
27
|
protocol?: string;
|
|
14
28
|
// Express sets this to the path the router was mounted at, e.g. '/api/animals'
|
|
15
29
|
// when orm.restServer.route is '/api'. Optional because non-Express callers
|
|
@@ -20,7 +34,11 @@ interface OrmRequest$ extends Request {
|
|
|
20
34
|
// Attached by auth() before access() runs — abofs/stonyx-orm#270. This is the
|
|
21
35
|
// value the ORM resolves the record by; `params.id` remains the raw client
|
|
22
36
|
// text it was parsed from.
|
|
23
|
-
|
|
37
|
+
//
|
|
38
|
+
// `undefined` is part of the contract, not an absence: it is how a collection
|
|
39
|
+
// route is told from a record route, and both documented samples branch on
|
|
40
|
+
// it. Spelled out rather than left to `?:` for that reason.
|
|
41
|
+
recordId?: string | number | undefined;
|
|
24
42
|
body?: { [key: string]: unknown };
|
|
25
43
|
query?: { [key: string]: string };
|
|
26
44
|
get(header: string): string;
|
|
@@ -112,11 +130,21 @@ function getBaseUrl(request: OrmRequest$, pluralizedModel: string): string {
|
|
|
112
130
|
return `${protocol}://${host}${prefix}`;
|
|
113
131
|
}
|
|
114
132
|
|
|
115
|
-
// Kept as a name because twelve call sites read
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
133
|
+
// Kept as a name because twelve `getId(...)` call sites read it. It is now a
|
|
134
|
+
// thin delegate: there is exactly ONE normalisation of a URL id in the repo,
|
|
135
|
+
// and it is the exported one a consumer can import.
|
|
136
|
+
//
|
|
137
|
+
// "of a URL id" is the load-bearing qualifier, and it is the same one
|
|
138
|
+
// src/normalize-record-id.ts:18 carries. Three copies of the coercion survive
|
|
139
|
+
// at this head — src/orm-request.ts (the create-response path),
|
|
140
|
+
// src/postgres/postgres-db.ts and src/mysql/mysql-db.ts — but each normalises a
|
|
141
|
+
// RESPONSE id (`response?.data?.id`), not a URL id, and each omits this
|
|
142
|
+
// function's `if (!id) return ''` guard. They are tracked as
|
|
143
|
+
// abofs/stonyx-orm#282 and enumerated by name in AC-5's allowlist
|
|
144
|
+
// (test/integration/readme-sample-test.ts).
|
|
145
|
+
//
|
|
146
|
+
// A second implementation of the URL-id normalisation here is the defect
|
|
147
|
+
// abofs/stonyx-orm#270 exists to remove — see src/normalize-record-id.ts.
|
|
120
148
|
function getId(params: { id?: string; [key: string]: unknown }): string | number {
|
|
121
149
|
return normalizeRecordId(params.id);
|
|
122
150
|
}
|
|
@@ -615,10 +643,10 @@ export default class OrmRequest extends Request {
|
|
|
615
643
|
// handlers; this line is the contract.
|
|
616
644
|
//
|
|
617
645
|
// `request.params` is deliberately NOT mutated. Twelve `getId(...)` call
|
|
618
|
-
// sites
|
|
619
|
-
//
|
|
620
|
-
// string to number underneath them is a silent behaviour
|
|
621
|
-
// this issue is not about.
|
|
646
|
+
// sites read it, and `_withHooks` assigns `params: request.params` onto the
|
|
647
|
+
// hook context, so every consumer hook reads the same object. Changing
|
|
648
|
+
// `params.id` from string to number underneath them is a silent behaviour
|
|
649
|
+
// change on paths this issue is not about.
|
|
622
650
|
//
|
|
623
651
|
// Synchronous by necessity: @stonyx/rest-server calls auth() without
|
|
624
652
|
// awaiting it.
|