@stonyx/orm 0.3.2-beta.166 → 0.3.2-beta.168
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 +36 -0
- package/dist/orm-request.d.ts +1 -0
- package/dist/orm-request.js +24 -7
- package/package.json +3 -2
- package/src/orm-request.ts +30 -7
package/README.md
CHANGED
|
@@ -113,6 +113,12 @@ export default {
|
|
|
113
113
|
};
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
> **`route` and JSON:API `links`.** Generated endpoints are mounted under `route`, and every
|
|
117
|
+
> `links.self` / `links.related` the ORM emits is an absolute URL that includes it. With
|
|
118
|
+
> `ORM_REST_ROUTE='/api'` the animal collection is served at `/api/animals` and advertises
|
|
119
|
+
> `http://host/api/animals`, so published links are followable as-is — do not prepend the
|
|
120
|
+
> mount yourself.
|
|
121
|
+
|
|
116
122
|
Then run the application via the Stonyx CLI, which auto-initializes all modules including the ORM:
|
|
117
123
|
|
|
118
124
|
```bash
|
|
@@ -322,6 +328,36 @@ export default class GlobalAccess {
|
|
|
322
328
|
}
|
|
323
329
|
```
|
|
324
330
|
|
|
331
|
+
### Upgrading: behaviour changes
|
|
332
|
+
|
|
333
|
+
**Advertised `links.self` / `links.related` now carry the REST mount route.**
|
|
334
|
+
Consumer-visible for any deployment where `orm.restServer.route` is not the
|
|
335
|
+
default `'/'`.
|
|
336
|
+
|
|
337
|
+
Measured on this repo's mounted-route harness at `ORM_REST_ROUTE='/api'`, resource
|
|
338
|
+
`links.self` in the response to `GET /api/animals/1`:
|
|
339
|
+
|
|
340
|
+
| | published `links.self` | fetching that URL |
|
|
341
|
+
|---|---|---|
|
|
342
|
+
| before | `http://host/animals/1` | **404** |
|
|
343
|
+
| after | `http://host/api/animals/1` | **200** |
|
|
344
|
+
|
|
345
|
+
The ORM previously built links from the request origin alone, so at any non-default
|
|
346
|
+
mount every URL it advertised pointed at a route that did not exist
|
|
347
|
+
(abofs/stonyx-orm#254). Links are now built from the path the routes are actually
|
|
348
|
+
mounted at, and are followable verbatim.
|
|
349
|
+
|
|
350
|
+
**⚠️ Breaking if you carry a prepending workaround.** The usual workaround for #254
|
|
351
|
+
was for the client to prepend the mount to every link the ORM published. That now
|
|
352
|
+
double-prefixes: prepending `/api` to the new `http://host/api/animals/1` yields
|
|
353
|
+
`http://host/api/api/animals/1`, measured **404**. Remove the prepending. There is no
|
|
354
|
+
configuration flag that restores the old link shape.
|
|
355
|
+
|
|
356
|
+
**Unaffected.** Deployments on the default `ORM_REST_ROUTE='/'` see byte-identical
|
|
357
|
+
output — the prefix is empty, and this is pinned by a byte-identity test against a
|
|
358
|
+
golden fixture captured *before* the fix. Response structure, field names and the
|
|
359
|
+
public API are unchanged; only the URL value inside `links` changes.
|
|
360
|
+
|
|
325
361
|
### Include Parameter (Sideloading Relationships)
|
|
326
362
|
|
|
327
363
|
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.
|
package/dist/orm-request.d.ts
CHANGED
package/dist/orm-request.js
CHANGED
|
@@ -42,11 +42,28 @@ function getModelRelationships(modelName) {
|
|
|
42
42
|
}
|
|
43
43
|
return relationships;
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Build the absolute base URL that every advertised link hangs off — origin
|
|
47
|
+
* plus the prefix the ORM's routes are actually mounted at.
|
|
48
|
+
*
|
|
49
|
+
* The prefix is derived from the *request*, not from
|
|
50
|
+
* `config.orm.restServer.route`. Express sets `request.baseUrl` to the real
|
|
51
|
+
* mountpath registered by `RestServer.mountRoute`, which for this module is
|
|
52
|
+
* always `<prefix>/<pluralizedModel>` (see setup-rest-server.ts). Stripping the
|
|
53
|
+
* trailing model segment therefore yields the prefix by construction, and the
|
|
54
|
+
* link builder cannot drift from the mount registrar the way a second,
|
|
55
|
+
* independent normalisation of `route` would.
|
|
56
|
+
*
|
|
57
|
+
* When `request.baseUrl` is absent or does not end in the model segment the
|
|
58
|
+
* prefix is empty, which reproduces the previous origin-only behaviour.
|
|
59
|
+
*/
|
|
60
|
+
function getBaseUrl(request, pluralizedModel) {
|
|
47
61
|
const protocol = request.protocol || 'http';
|
|
48
62
|
const host = request.get('host');
|
|
49
|
-
|
|
63
|
+
const modelSegment = `/${pluralizedModel}`;
|
|
64
|
+
const mountPath = request.baseUrl ?? '';
|
|
65
|
+
const prefix = mountPath.endsWith(modelSegment) ? mountPath.slice(0, -modelSegment.length) : '';
|
|
66
|
+
return `${protocol}://${host}${prefix}`;
|
|
50
67
|
}
|
|
51
68
|
function getId(params) {
|
|
52
69
|
const id = params.id;
|
|
@@ -209,7 +226,7 @@ export default class OrmRequest extends Request {
|
|
|
209
226
|
recordsToReturn = recordsToReturn.filter(accessFilter);
|
|
210
227
|
if (queryFilterPredicate)
|
|
211
228
|
recordsToReturn = recordsToReturn.filter(queryFilterPredicate);
|
|
212
|
-
const baseUrl = getBaseUrl(request);
|
|
229
|
+
const baseUrl = getBaseUrl(request, pluralizedModel);
|
|
213
230
|
const data = recordsToReturn.map(record => record.toJSON?.({ fields: modelFields, baseUrl }));
|
|
214
231
|
return buildResponse(data, request.query?.include, recordsToReturn, {
|
|
215
232
|
links: { self: `${baseUrl}/${pluralizedModel}` },
|
|
@@ -222,7 +239,7 @@ export default class OrmRequest extends Request {
|
|
|
222
239
|
return 404;
|
|
223
240
|
const fieldsMap = parseFields(request.query);
|
|
224
241
|
const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
|
|
225
|
-
const baseUrl = getBaseUrl(request);
|
|
242
|
+
const baseUrl = getBaseUrl(request, pluralizedModel);
|
|
226
243
|
return buildResponse(record.toJSON?.({ fields: modelFields, baseUrl }), request.query?.include, record, {
|
|
227
244
|
links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}` },
|
|
228
245
|
baseUrl
|
|
@@ -397,7 +414,7 @@ export default class OrmRequest extends Request {
|
|
|
397
414
|
if (!record)
|
|
398
415
|
return 404;
|
|
399
416
|
const relatedData = record.__relationships[relationshipName];
|
|
400
|
-
const baseUrl = getBaseUrl(request);
|
|
417
|
+
const baseUrl = getBaseUrl(request, pluralizedModel);
|
|
401
418
|
let data;
|
|
402
419
|
if (info.isArray) {
|
|
403
420
|
// hasMany - return array
|
|
@@ -419,7 +436,7 @@ export default class OrmRequest extends Request {
|
|
|
419
436
|
if (!record)
|
|
420
437
|
return 404;
|
|
421
438
|
const relatedData = record.__relationships[relationshipName];
|
|
422
|
-
const baseUrl = getBaseUrl(request);
|
|
439
|
+
const baseUrl = getBaseUrl(request, pluralizedModel);
|
|
423
440
|
let data;
|
|
424
441
|
if (info.isArray) {
|
|
425
442
|
// hasMany - return array of linkage objects
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.3.2-beta.
|
|
7
|
+
"version": "0.3.2-beta.168",
|
|
8
8
|
"description": "",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"type": "module",
|
|
@@ -103,7 +103,8 @@
|
|
|
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'",
|
|
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",
|
|
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'",
|
|
107
108
|
"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'"
|
|
108
109
|
}
|
|
109
110
|
}
|
package/src/orm-request.ts
CHANGED
|
@@ -10,6 +10,10 @@ import { isOrmRecord } from './utils.js';
|
|
|
10
10
|
|
|
11
11
|
interface OrmRequest$ extends Request {
|
|
12
12
|
protocol?: string;
|
|
13
|
+
// Express sets this to the path the router was mounted at, e.g. '/api/animals'
|
|
14
|
+
// when orm.restServer.route is '/api'. Optional because non-Express callers
|
|
15
|
+
// (unit tests, programmatic handler invocation) do not supply it.
|
|
16
|
+
baseUrl?: string;
|
|
13
17
|
method: string;
|
|
14
18
|
params: { [key: string]: string };
|
|
15
19
|
body?: { [key: string]: unknown };
|
|
@@ -77,11 +81,30 @@ function getModelRelationships(modelName: string): { [key: string]: Relationship
|
|
|
77
81
|
return relationships;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Build the absolute base URL that every advertised link hangs off — origin
|
|
86
|
+
* plus the prefix the ORM's routes are actually mounted at.
|
|
87
|
+
*
|
|
88
|
+
* The prefix is derived from the *request*, not from
|
|
89
|
+
* `config.orm.restServer.route`. Express sets `request.baseUrl` to the real
|
|
90
|
+
* mountpath registered by `RestServer.mountRoute`, which for this module is
|
|
91
|
+
* always `<prefix>/<pluralizedModel>` (see setup-rest-server.ts). Stripping the
|
|
92
|
+
* trailing model segment therefore yields the prefix by construction, and the
|
|
93
|
+
* link builder cannot drift from the mount registrar the way a second,
|
|
94
|
+
* independent normalisation of `route` would.
|
|
95
|
+
*
|
|
96
|
+
* When `request.baseUrl` is absent or does not end in the model segment the
|
|
97
|
+
* prefix is empty, which reproduces the previous origin-only behaviour.
|
|
98
|
+
*/
|
|
99
|
+
function getBaseUrl(request: OrmRequest$, pluralizedModel: string): string {
|
|
82
100
|
const protocol = request.protocol || 'http';
|
|
83
101
|
const host = request.get('host');
|
|
84
|
-
|
|
102
|
+
|
|
103
|
+
const modelSegment = `/${pluralizedModel}`;
|
|
104
|
+
const mountPath = request.baseUrl ?? '';
|
|
105
|
+
const prefix = mountPath.endsWith(modelSegment) ? mountPath.slice(0, -modelSegment.length) : '';
|
|
106
|
+
|
|
107
|
+
return `${protocol}://${host}${prefix}`;
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
function getId(params: { id?: string; [key: string]: unknown }): string | number {
|
|
@@ -278,7 +301,7 @@ export default class OrmRequest extends Request {
|
|
|
278
301
|
if (accessFilter) recordsToReturn = recordsToReturn.filter(accessFilter as (record: OrmRecord) => boolean);
|
|
279
302
|
if (queryFilterPredicate) recordsToReturn = recordsToReturn.filter(queryFilterPredicate as (record: OrmRecord) => boolean);
|
|
280
303
|
|
|
281
|
-
const baseUrl = getBaseUrl(request);
|
|
304
|
+
const baseUrl = getBaseUrl(request, pluralizedModel);
|
|
282
305
|
const data = recordsToReturn.map(record => record.toJSON?.({ fields: modelFields, baseUrl }));
|
|
283
306
|
|
|
284
307
|
return buildResponse(data, request.query?.include, recordsToReturn, {
|
|
@@ -294,7 +317,7 @@ export default class OrmRequest extends Request {
|
|
|
294
317
|
const fieldsMap = parseFields(request.query);
|
|
295
318
|
const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
|
|
296
319
|
|
|
297
|
-
const baseUrl = getBaseUrl(request);
|
|
320
|
+
const baseUrl = getBaseUrl(request, pluralizedModel);
|
|
298
321
|
return buildResponse(record.toJSON?.({ fields: modelFields, baseUrl }), request.query?.include, record, {
|
|
299
322
|
links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}` },
|
|
300
323
|
baseUrl
|
|
@@ -502,7 +525,7 @@ export default class OrmRequest extends Request {
|
|
|
502
525
|
if (!record) return 404;
|
|
503
526
|
|
|
504
527
|
const relatedData = record.__relationships[relationshipName];
|
|
505
|
-
const baseUrl = getBaseUrl(request);
|
|
528
|
+
const baseUrl = getBaseUrl(request, pluralizedModel);
|
|
506
529
|
|
|
507
530
|
let data: unknown;
|
|
508
531
|
if (info.isArray) {
|
|
@@ -526,7 +549,7 @@ export default class OrmRequest extends Request {
|
|
|
526
549
|
if (!record) return 404;
|
|
527
550
|
|
|
528
551
|
const relatedData = record.__relationships[relationshipName];
|
|
529
|
-
const baseUrl = getBaseUrl(request);
|
|
552
|
+
const baseUrl = getBaseUrl(request, pluralizedModel);
|
|
530
553
|
|
|
531
554
|
let data: unknown;
|
|
532
555
|
if (info.isArray) {
|