@stonyx/orm 0.3.2-alpha.93 → 0.3.2-alpha.95
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 +77 -3
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -318,16 +318,90 @@ await setupRestServer('/', './access');
|
|
|
318
318
|
Access classes define models and provide custom filtering/authorization logic:
|
|
319
319
|
|
|
320
320
|
```js
|
|
321
|
-
export default class
|
|
322
|
-
models = ['owner'
|
|
321
|
+
export default class OwnerAccess {
|
|
322
|
+
models = ['owner'];
|
|
323
323
|
|
|
324
324
|
access(request) {
|
|
325
|
-
|
|
325
|
+
// `access` runs after route matching, so `request.params` is populated and
|
|
326
|
+
// `id` has already been URL-decoded. Authorize on it, never on a URL.
|
|
327
|
+
const { id } = request.params;
|
|
328
|
+
|
|
329
|
+
// `id` is still raw client text. Normalise it the way the record lookup
|
|
330
|
+
// does, or your predicate and the lookup disagree — see "Numeric ids" below.
|
|
331
|
+
// No radix on parseInt: that is deliberate, and it must stay that way.
|
|
332
|
+
const recordId = id === undefined ? undefined : (isNaN(id) ? id : parseInt(id));
|
|
333
|
+
|
|
334
|
+
// Returning false explicitly denies access to this record
|
|
335
|
+
if (recordId === 'angela') return false;
|
|
336
|
+
|
|
337
|
+
// No `id` means the collection route. Returning a function plugs it in to
|
|
338
|
+
// the response object as a filter. NOTE: a function return authorizes the
|
|
339
|
+
// request outright — the operations list below is not consulted — so this
|
|
340
|
+
// branch permits POST /owners as well as reads.
|
|
341
|
+
if (recordId === undefined) return record => record.id !== 'angela';
|
|
342
|
+
|
|
343
|
+
// Returning a list of operations allows full access to everything else
|
|
344
|
+
return ['read', 'create', 'update', 'delete'];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Do not authorize on the request URL.** `request.url` is rewritten relative to
|
|
350
|
+
the mount point, so inside the REST server it is `/angela`, not `/owners/angela`
|
|
351
|
+
— a suffix comparison against it never matches and the request falls through to
|
|
352
|
+
whatever the method returns next. `request.originalUrl` keeps the full path but
|
|
353
|
+
is still the raw text the client sent, so it varies with query strings
|
|
354
|
+
(`/owners?x=1`), trailing slashes (`/owners/angela/`), casing (`/OwNeRs/angela`,
|
|
355
|
+
which Express routes to the same handler) and percent-encoding
|
|
356
|
+
(`/owners/%61ngela`). Each of those is a plain address-bar request, and each one
|
|
357
|
+
slips past a URL predicate. `request.params.id` is identical for all of them.
|
|
358
|
+
|
|
359
|
+
**One access class per model when the rules are model-specific.** `access()`
|
|
360
|
+
receives only the request, and the request does not carry the model name in any
|
|
361
|
+
form that is safe to match on — `request.baseUrl` is the mount text as the
|
|
362
|
+
client spelled it (`/OWNERS`), not the model. A class may still list several
|
|
363
|
+
models in `models` when they share one rule.
|
|
364
|
+
|
|
365
|
+
**Numeric ids: normalise before you compare.** `request.params.id` is raw text
|
|
366
|
+
from the client. When it looks numeric the ORM coerces it — `isNaN(id) ? id :
|
|
367
|
+
parseInt(id)` — *before* it resolves the record, so `7`, `007`, `7.0`, `7.9`,
|
|
368
|
+
`7e0`, `0x7`, `+7`, `%207` (a leading space), `%09 7` (a tab) and `7%0A` (a
|
|
369
|
+
trailing newline) all address record `7`, while a `===` against the raw text
|
|
370
|
+
matches only the one spelling you wrote down. Every other spelling falls through
|
|
371
|
+
to whatever your method returns next — which, in the shape above, is a full CRUD
|
|
372
|
+
grant. All of them are plain address-bar requests.
|
|
373
|
+
|
|
374
|
+
Two details are load-bearing. `parseInt` is called with **no radix**, so `0x7`
|
|
375
|
+
is `7` and not `0`; writing `parseInt(id, 10)` in your predicate re-opens the
|
|
376
|
+
hex spelling. And the coercion applies only when the id looks numeric, so a
|
|
377
|
+
model with string ids (like `owner` above) is unaffected — which is exactly why
|
|
378
|
+
this is easy to miss. Normalise the same way the lookup does:
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
export default class AnimalAccess {
|
|
382
|
+
models = ['animal'];
|
|
383
|
+
|
|
384
|
+
access(request) {
|
|
385
|
+
const { id } = request.params;
|
|
386
|
+
|
|
387
|
+
// Agrees with the lookup for every spelling of 7 above. Compare the
|
|
388
|
+
// coerced value, which for a numeric-id model is a number, not a string.
|
|
389
|
+
const recordId = id === undefined ? undefined : (isNaN(id) ? id : parseInt(id));
|
|
390
|
+
|
|
391
|
+
if (recordId === 7) return false;
|
|
392
|
+
|
|
393
|
+
if (recordId === undefined) return record => record.id !== 7;
|
|
394
|
+
|
|
326
395
|
return ['read', 'create', 'update', 'delete'];
|
|
327
396
|
}
|
|
328
397
|
}
|
|
329
398
|
```
|
|
330
399
|
|
|
400
|
+
The sample above is executed verbatim against a live server by
|
|
401
|
+
`test/integration/readme-access/`, which reads it out of this file: the request
|
|
402
|
+
`DELETE /owners/angela` is asserted to return 403 with the record intact, along
|
|
403
|
+
with each of the spellings named above.
|
|
404
|
+
|
|
331
405
|
### Upgrading: behaviour changes
|
|
332
406
|
|
|
333
407
|
**Advertised `links.self` / `links.related` now carry the REST mount route.**
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.3.2-alpha.
|
|
7
|
+
"version": "0.3.2-alpha.95",
|
|
8
8
|
"description": "",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"type": "module",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
},
|
|
62
62
|
"homepage": "https://github.com/abofs/stonyx-orm#readme",
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@stonyx/cron": "0.2.1-beta.
|
|
65
|
-
"@stonyx/events": "0.1.1-beta.
|
|
64
|
+
"@stonyx/cron": "0.2.1-beta.98",
|
|
65
|
+
"@stonyx/events": "0.1.1-beta.54",
|
|
66
66
|
"@stonyx/utils": "0.2.3-beta.26",
|
|
67
67
|
"stonyx": "0.2.3-beta.81"
|
|
68
68
|
},
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@stonyx/rest-server": "0.2.1-beta.
|
|
94
|
+
"@stonyx/rest-server": "0.2.1-beta.100",
|
|
95
95
|
"@types/node": "^25.6.0",
|
|
96
96
|
"mysql2": "^3.20.0",
|
|
97
97
|
"pg": "^8.20.0",
|
|
@@ -103,8 +103,10 @@
|
|
|
103
103
|
"scripts": {
|
|
104
104
|
"build": "tsc",
|
|
105
105
|
"build:test": "tsc -p tsconfig.test.json",
|
|
106
|
-
"test": "pnpm build && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts' && ORM_TEST_ROUTE=/ pnpm test:mounted && ORM_TEST_ROUTE=/api pnpm test:mounted && ORM_TEST_ROUTE=api pnpm test:mounted && ORM_TEST_ROUTE=/api/v1 pnpm test:mounted && ORM_TEST_ROUTE=/api/ pnpm test:mounted",
|
|
106
|
+
"test": "pnpm build && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts' && ORM_TEST_ROUTE=/ pnpm test:mounted && ORM_TEST_ROUTE=/api pnpm test:mounted && ORM_TEST_ROUTE=api pnpm test:mounted && ORM_TEST_ROUTE=/api/v1 pnpm test:mounted && ORM_TEST_ROUTE=/api/ pnpm test:mounted && pnpm test:readme && pnpm test:reference",
|
|
107
107
|
"test:mounted": "node --import tsx/esm --import ./test/integration/mounted-route/setup.ts node_modules/qunit/bin/qunit.js 'test/integration/mounted-route/links-mounted.ts' 'test/zz-exit-test.ts'",
|
|
108
|
+
"test:readme": "node --import tsx/esm --import ./test/integration/readme-access/setup.ts node_modules/qunit/bin/qunit.js 'test/integration/readme-access/readme-sample.ts' 'test/zz-exit-test.ts'",
|
|
109
|
+
"test:reference": "node --import tsx/esm --import ./test/integration/reference-access/setup.ts node_modules/qunit/bin/qunit.js 'test/integration/reference-access/reference-sample.ts' 'test/zz-exit-test.ts'",
|
|
108
110
|
"test:dynamodb": "pnpm build && node --import tsx/esm --import ./test/integration/dynamodb/setup.ts node_modules/qunit/bin/qunit.js 'test/integration/dynamodb/**/*-test.ts'"
|
|
109
111
|
}
|
|
110
112
|
}
|