@stonyx/orm 0.3.2-alpha.73 → 0.3.2-alpha.75
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 +74 -14
- package/dist/orm-request.js +83 -9
- package/package.json +1 -1
- package/src/orm-request.ts +84 -9
package/README.md
CHANGED
|
@@ -452,7 +452,11 @@ export default class GlobalAccess {
|
|
|
452
452
|
// inert. Deliberately NO `?? record.owner` fallback: accepting the raw
|
|
453
453
|
// shape as well as the resolved one would absorb a resolution regression
|
|
454
454
|
// silently, which is exactly what blinded this fixture before.
|
|
455
|
-
|
|
455
|
+
// `record.id !== 18` hides one animal whose OWNER is permitted. It is the
|
|
456
|
+
// fixture that makes the `hasMany` half of the relationship-route rules
|
|
457
|
+
// observable: gina is served, animal 18 is not, and every surface that
|
|
458
|
+
// names gina's pets has to drop it.
|
|
459
|
+
if (model === 'animal') return record => record.owner?.id !== 'restricted' && record.id !== 18;
|
|
456
460
|
|
|
457
461
|
// Allows full access to all calls that don't match any of the above conditions
|
|
458
462
|
return ['read', 'create', 'update', 'delete'];
|
|
@@ -921,15 +925,67 @@ per-record filter. An input you cannot identify must **deny**.
|
|
|
921
925
|
operation and which record the request addresses. The five variants above are
|
|
922
926
|
the five ways that has been observed to fail open so far. Tracked as
|
|
923
927
|
[#202](https://github.com/abofs/stonyx-orm/issues/202).
|
|
924
|
-
- **
|
|
925
|
-
|
|
926
|
-
`GET /
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
`
|
|
931
|
-
|
|
932
|
-
|
|
928
|
+
- **The two relationship route families now resolve the *related* model's own
|
|
929
|
+
access class — `GET /:models/:id/{relationship}` and
|
|
930
|
+
`GET /:models/:id/relationships/{relationship}`**
|
|
931
|
+
([#232](https://github.com/abofs/stonyx-orm/issues/232)). This is
|
|
932
|
+
**membership**: the related resource is the route's *primary* data, so the
|
|
933
|
+
filter decides whether it is served at all, not merely which ids a document
|
|
934
|
+
may name. A denied `hasMany` member is **dropped from the array** — the result
|
|
935
|
+
is shaped exactly like a genuinely empty relationship, `links` intact, no
|
|
936
|
+
`errors` member, same status. A denied `belongsTo` target answers **404**, the
|
|
937
|
+
same status the route already answered for a denied parent and for a parent
|
|
938
|
+
that does not exist. The `/relationships/` family built its `{type, id}` by
|
|
939
|
+
hand rather than through `toJSON()`, which is why the linkage filter shipped in
|
|
940
|
+
[#234](https://github.com/abofs/stonyx-orm/issues/234) did not reach it.
|
|
941
|
+
|
|
942
|
+
Before this, both families served a record hidden on every one of its own
|
|
943
|
+
surfaces, in full, from another model's route, at **zero query parameters**.
|
|
944
|
+
The severe case is a model **claimed by no access class**: `getAccess()`
|
|
945
|
+
returns `undefined`, no route is mounted for it at all, and it was still
|
|
946
|
+
readable as a related resource — a collection the consumer deliberately never
|
|
947
|
+
exposed.
|
|
948
|
+
|
|
949
|
+
**Residual on the `belongsTo` shape, stated rather than left to be found:** a
|
|
950
|
+
denied target answers 404 while a genuinely *absent* one answers 200 with
|
|
951
|
+
`data: null`, so those two cases are distinguishable. That asymmetry is
|
|
952
|
+
inherited — a denied *parent* has always answered 404 while an existing parent
|
|
953
|
+
with an empty relationship answers 200 — and changing it is a change to this
|
|
954
|
+
module's whole spelling of denial, not to these two routes.
|
|
955
|
+
|
|
956
|
+
**Per-record denies for a related resource are not expressible.** A predicate resolved for a
|
|
957
|
+
related resource on these routes receives `recordId: null` and a `request`
|
|
958
|
+
whose `params` name a record of a **different model**. So the inputs it has
|
|
959
|
+
are the model name, the operation and the request — and **a rule that needs to
|
|
960
|
+
know *which* related record it is being asked about cannot be written**.
|
|
961
|
+
Model-level denies (`return false` for a model) work. Request-level denies (a
|
|
962
|
+
rule reading a header, a tenant, the method) work. The per-record **filter**
|
|
963
|
+
shape works too — `access()` may return a function, and that function receives
|
|
964
|
+
the whole record, id included. What does not work is branching on the record's
|
|
965
|
+
identity *before* returning, because `access()` is not told it.
|
|
966
|
+
|
|
967
|
+
This is not an oversight and it is not closed here. The verdict is resolved
|
|
968
|
+
**once per type**, cached, before any record has been examined — a `hasMany`
|
|
969
|
+
related-resource route returns many records of one type, so seeding `recordId`
|
|
970
|
+
from a record would let the first one decide the context for all of them. The
|
|
971
|
+
rule the framework holds to is: **`recordId` may name a record only where the
|
|
972
|
+
route addresses exactly one record of the model being asked about.** That is
|
|
973
|
+
true for `GET /owners/{id}`, false for linkage, and false for a `hasMany`
|
|
974
|
+
related-resource route.
|
|
975
|
+
|
|
976
|
+
- **`?include=` records are still not filtered — the relationship routes now
|
|
977
|
+
are.** *Re-specified by [#232](https://github.com/abofs/stonyx-orm/issues/232);
|
|
978
|
+
the sentence this replaces said all three surfaces were unfiltered, and two of
|
|
979
|
+
them no longer are.* `GET /animals/1/owner` and
|
|
980
|
+
`GET /animals/1/relationships/owner` resolve the related model's own access
|
|
981
|
+
class (see the bullet above). **`?include=owner` still does not**: it
|
|
982
|
+
serializes the related record without resolving that class, so a filter on
|
|
983
|
+
`/owners` does not hide an owner reached through `?include=` on `/animals`.
|
|
984
|
+
Tracked as [#233](https://github.com/abofs/stonyx-orm/issues/233), the
|
|
985
|
+
remaining child of [#196](https://github.com/abofs/stonyx-orm/issues/196).
|
|
986
|
+
This is **membership** — whether the related resource is served at all — and
|
|
987
|
+
it is a different question from which ids a document may *name*, immediately
|
|
988
|
+
below.
|
|
933
989
|
- **Relationship linkage is filtered on the four request-bound read surfaces,
|
|
934
990
|
and only there.** A document's `relationships.*.data` used to publish the id
|
|
935
991
|
of every related record unconditionally, so a record hidden on every one of
|
|
@@ -988,10 +1044,14 @@ per-record filter. An input you cannot identify must **deny**.
|
|
|
988
1044
|
angela**, seconds apart, with no query string and no relationship route. Any
|
|
989
1045
|
caller who can read a record can also write it and be handed the id the read
|
|
990
1046
|
withheld.
|
|
991
|
-
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1047
|
+
- ~~**`GET /:models/:id/relationships/{relationship}`**~~ — **covered as of
|
|
1048
|
+
[#232](https://github.com/abofs/stonyx-orm/issues/232)**, together with
|
|
1049
|
+
`GET /:models/:id/{relationship}`. Left in place rather than deleted because
|
|
1050
|
+
this list is what a reader consults to find out what is *not* covered, and a
|
|
1051
|
+
silently vanishing entry reads as an entry that was never there. Its
|
|
1052
|
+
*primary data* is linkage, so filtering it was a **membership** decision —
|
|
1053
|
+
which is why it belonged to #232 and not to
|
|
1054
|
+
[#234](https://github.com/abofs/stonyx-orm/issues/234).
|
|
995
1055
|
- **A bare `toJSON()` still emits unfiltered linkage, and that is deliberate.**
|
|
996
1056
|
`Record.toJSON()` **applies** a verdict; it never **resolves** one. It has no
|
|
997
1057
|
request, and the documented `access()` contract permits a predicate to read
|
package/dist/orm-request.js
CHANGED
|
@@ -1245,21 +1245,77 @@ export default class OrmRequest extends Request {
|
|
|
1245
1245
|
return 404;
|
|
1246
1246
|
const relatedData = record.__relationships[relationshipName];
|
|
1247
1247
|
const baseUrl = getBaseUrl(request);
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
// related record is
|
|
1248
|
+
// ONE FILTER, TWO JOBS, AND abofs/stonyx-orm#232 IS THE SECOND ONE.
|
|
1249
|
+
//
|
|
1250
|
+
// As LINKAGE (#234) it decides which ids the emitted documents may NAME
|
|
1251
|
+
// in their own `relationships.*.data`. As MEMBERSHIP (this issue) it
|
|
1252
|
+
// decides whether the related record is served here AT ALL -- the
|
|
1253
|
+
// related resource is PRIMARY data on this route, so there is no
|
|
1254
|
+
// linkage-consistency question to answer separately.
|
|
1255
|
+
//
|
|
1256
|
+
// Until #232 this route filtered only the PARENT, so a record its own
|
|
1257
|
+
// model's predicate hides was served in full from another model's
|
|
1258
|
+
// route, at ZERO query parameters. Measured on dev @ 8dda5d6:
|
|
1259
|
+
//
|
|
1260
|
+
// GET /owners/angela -> 404
|
|
1261
|
+
// GET /animals/1/owner -> 200, owner:angela, full attributes
|
|
1262
|
+
// GET /traits/2/tag -> 200, a model NO access class
|
|
1263
|
+
// claims, on a collection that has
|
|
1264
|
+
// no mounted route at all
|
|
1265
|
+
//
|
|
1266
|
+
// ARGUMENT ONE IS THE LIVE REQUEST, NOT A DERIVED ONE. A fabricated
|
|
1267
|
+
// request addressing the RELATED resource was the original design and
|
|
1268
|
+
// it is dropped: #241 removed the shipped fixture's read of argument
|
|
1269
|
+
// one, so a fabricated value changes nothing it could observe, and
|
|
1270
|
+
// test/unit/linkage-verdict-test.ts:484 pins the predicate as receiving
|
|
1271
|
+
// the live request BY IDENTITY. `createLinkageFilter` is also a
|
|
1272
|
+
// published public export (src/index.ts) whose resolution granularity
|
|
1273
|
+
// is per TYPE; supplying a per-RECORD request would mean widening it,
|
|
1274
|
+
// which takes a consumer `access()` from ~2 calls to ~7 on a plain
|
|
1275
|
+
// `GET /animals`. That is a separate, consumer-visible story.
|
|
1276
|
+
//
|
|
1277
|
+
// THE RESIDUAL THAT FOLLOWS FROM THAT IS DISCLOSED, NOT PAPERED OVER.
|
|
1278
|
+
// `recordId` is `null` here and the request names a record of a
|
|
1279
|
+
// DIFFERENT model, so a consumer predicate can express a model-level or
|
|
1280
|
+
// a request-level deny for a related resource, but NOT a per-record
|
|
1281
|
+
// one. README.md and docs/usage-patterns.md say so; a ledger assertion
|
|
1282
|
+
// in test/unit/relationship-route-access-test.ts keeps them saying it.
|
|
1253
1283
|
const linkage = createLinkageFilter(request);
|
|
1284
|
+
// FAIL CLOSED ON A RECORD WHOSE TYPE CANNOT BE NAMED. `isLinkable` is
|
|
1285
|
+
// keyed on the model name; without one there is no predicate to ask,
|
|
1286
|
+
// and an unidentifiable input must never be the permissive path.
|
|
1287
|
+
const isLinkable = (r) => {
|
|
1288
|
+
const type = r.__model?.__name;
|
|
1289
|
+
return typeof type === 'string' && type !== '' && linkage(type, r);
|
|
1290
|
+
};
|
|
1254
1291
|
let data;
|
|
1255
1292
|
if (info.isArray) {
|
|
1256
|
-
// hasMany - return array
|
|
1293
|
+
// hasMany - return array, MINUS the members this caller may not see.
|
|
1294
|
+
// Dropped, never errored: the result is byte-identical to a genuinely
|
|
1295
|
+
// empty relationship, so this route is not an existence oracle.
|
|
1257
1296
|
const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
|
|
1258
|
-
data = related.map(r => r.toJSON?.({ baseUrl, linkage }));
|
|
1297
|
+
data = related.filter(isLinkable).map(r => r.toJSON?.({ baseUrl, linkage }));
|
|
1259
1298
|
}
|
|
1260
1299
|
else {
|
|
1261
|
-
// belongsTo - return single or null
|
|
1262
|
-
|
|
1300
|
+
// belongsTo - return single or null. A DENIED target is 404, the same
|
|
1301
|
+
// status this route already returns for a denied PARENT and for a
|
|
1302
|
+
// parent that does not exist, and the same status the related
|
|
1303
|
+
// record's own route answers.
|
|
1304
|
+
//
|
|
1305
|
+
// KNOWN RESIDUAL, stated here because it is the one place a reader
|
|
1306
|
+
// will look: 404 is distinguishable from a relationship that is
|
|
1307
|
+
// genuinely EMPTY, which answers 200 with `data: null`. So a caller
|
|
1308
|
+
// can tell "there is a target you may not see" from "there is no
|
|
1309
|
+
// target". That asymmetry is inherited -- a denied PARENT has always
|
|
1310
|
+
// answered 404 while an existing parent with an empty relationship
|
|
1311
|
+
// answers 200 -- and closing it is a change to the module's whole
|
|
1312
|
+
// denial spelling, not to this route.
|
|
1313
|
+
if (!isOrmRecord(relatedData))
|
|
1314
|
+
data = null;
|
|
1315
|
+
else if (!isLinkable(relatedData))
|
|
1316
|
+
return 404;
|
|
1317
|
+
else
|
|
1318
|
+
data = relatedData.toJSON?.({ baseUrl, linkage });
|
|
1263
1319
|
}
|
|
1264
1320
|
return {
|
|
1265
1321
|
links: { self: `${baseUrl}/${pluralizedModel}/${request.params.id}/${dasherizedName}` },
|
|
@@ -1275,17 +1331,35 @@ export default class OrmRequest extends Request {
|
|
|
1275
1331
|
return 404;
|
|
1276
1332
|
const relatedData = record.__relationships[relationshipName];
|
|
1277
1333
|
const baseUrl = getBaseUrl(request);
|
|
1334
|
+
// THE ONLY ONE OF THE FOUR READ SURFACES THAT DOES NOT GO THROUGH
|
|
1335
|
+
// `toJSON()`. It builds `{ type, id }` BY HAND, which is why #234's
|
|
1336
|
+
// linkage filter never reached it and why this half belongs to
|
|
1337
|
+
// abofs/stonyx-orm#232 rather than to #234: on this route the linkage
|
|
1338
|
+
// IS the primary data of an opt-in request, so filtering it changes the
|
|
1339
|
+
// route's MEMBERSHIP semantics, not the ids named inside somebody
|
|
1340
|
+
// else's document.
|
|
1341
|
+
//
|
|
1342
|
+
// Same filter, same argument-one decision, same residual as
|
|
1343
|
+
// `/:id/{relationship}` above -- read the block there.
|
|
1344
|
+
const linkage = createLinkageFilter(request);
|
|
1345
|
+
const isLinkable = (r) => {
|
|
1346
|
+
const type = r.__model?.__name;
|
|
1347
|
+
return typeof type === 'string' && type !== '' && linkage(type, r);
|
|
1348
|
+
};
|
|
1278
1349
|
let data;
|
|
1279
1350
|
if (info.isArray) {
|
|
1280
1351
|
// hasMany - return array of linkage objects
|
|
1281
1352
|
const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
|
|
1282
1353
|
data = related
|
|
1283
1354
|
.filter((r) => Boolean(r.__model))
|
|
1355
|
+
.filter(isLinkable)
|
|
1284
1356
|
.map(r => ({ type: r.__model.__name, id: r.id }));
|
|
1285
1357
|
}
|
|
1286
1358
|
else {
|
|
1287
1359
|
// belongsTo - return single linkage or null
|
|
1288
1360
|
if (isOrmRecord(relatedData) && relatedData.__model) {
|
|
1361
|
+
if (!isLinkable(relatedData))
|
|
1362
|
+
return 404;
|
|
1289
1363
|
data = { type: relatedData.__model.__name, id: relatedData.id };
|
|
1290
1364
|
}
|
|
1291
1365
|
else {
|
package/package.json
CHANGED
package/src/orm-request.ts
CHANGED
|
@@ -1364,21 +1364,76 @@ export default class OrmRequest extends Request {
|
|
|
1364
1364
|
const relatedData = record.__relationships[relationshipName];
|
|
1365
1365
|
const baseUrl = getBaseUrl(request);
|
|
1366
1366
|
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
// related record is
|
|
1367
|
+
// ONE FILTER, TWO JOBS, AND abofs/stonyx-orm#232 IS THE SECOND ONE.
|
|
1368
|
+
//
|
|
1369
|
+
// As LINKAGE (#234) it decides which ids the emitted documents may NAME
|
|
1370
|
+
// in their own `relationships.*.data`. As MEMBERSHIP (this issue) it
|
|
1371
|
+
// decides whether the related record is served here AT ALL -- the
|
|
1372
|
+
// related resource is PRIMARY data on this route, so there is no
|
|
1373
|
+
// linkage-consistency question to answer separately.
|
|
1374
|
+
//
|
|
1375
|
+
// Until #232 this route filtered only the PARENT, so a record its own
|
|
1376
|
+
// model's predicate hides was served in full from another model's
|
|
1377
|
+
// route, at ZERO query parameters. Measured on dev @ 8dda5d6:
|
|
1378
|
+
//
|
|
1379
|
+
// GET /owners/angela -> 404
|
|
1380
|
+
// GET /animals/1/owner -> 200, owner:angela, full attributes
|
|
1381
|
+
// GET /traits/2/tag -> 200, a model NO access class
|
|
1382
|
+
// claims, on a collection that has
|
|
1383
|
+
// no mounted route at all
|
|
1384
|
+
//
|
|
1385
|
+
// ARGUMENT ONE IS THE LIVE REQUEST, NOT A DERIVED ONE. A fabricated
|
|
1386
|
+
// request addressing the RELATED resource was the original design and
|
|
1387
|
+
// it is dropped: #241 removed the shipped fixture's read of argument
|
|
1388
|
+
// one, so a fabricated value changes nothing it could observe, and
|
|
1389
|
+
// test/unit/linkage-verdict-test.ts:484 pins the predicate as receiving
|
|
1390
|
+
// the live request BY IDENTITY. `createLinkageFilter` is also a
|
|
1391
|
+
// published public export (src/index.ts) whose resolution granularity
|
|
1392
|
+
// is per TYPE; supplying a per-RECORD request would mean widening it,
|
|
1393
|
+
// which takes a consumer `access()` from ~2 calls to ~7 on a plain
|
|
1394
|
+
// `GET /animals`. That is a separate, consumer-visible story.
|
|
1395
|
+
//
|
|
1396
|
+
// THE RESIDUAL THAT FOLLOWS FROM THAT IS DISCLOSED, NOT PAPERED OVER.
|
|
1397
|
+
// `recordId` is `null` here and the request names a record of a
|
|
1398
|
+
// DIFFERENT model, so a consumer predicate can express a model-level or
|
|
1399
|
+
// a request-level deny for a related resource, but NOT a per-record
|
|
1400
|
+
// one. README.md and docs/usage-patterns.md say so; a ledger assertion
|
|
1401
|
+
// in test/unit/relationship-route-access-test.ts keeps them saying it.
|
|
1372
1402
|
const linkage = createLinkageFilter(request);
|
|
1373
1403
|
|
|
1404
|
+
// FAIL CLOSED ON A RECORD WHOSE TYPE CANNOT BE NAMED. `isLinkable` is
|
|
1405
|
+
// keyed on the model name; without one there is no predicate to ask,
|
|
1406
|
+
// and an unidentifiable input must never be the permissive path.
|
|
1407
|
+
const isLinkable = (r: OrmRecord) => {
|
|
1408
|
+
const type = (r as { __model?: { __name?: string } }).__model?.__name;
|
|
1409
|
+
|
|
1410
|
+
return typeof type === 'string' && type !== '' && linkage(type, r);
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1374
1413
|
let data: unknown;
|
|
1375
1414
|
if (info.isArray) {
|
|
1376
|
-
// hasMany - return array
|
|
1415
|
+
// hasMany - return array, MINUS the members this caller may not see.
|
|
1416
|
+
// Dropped, never errored: the result is byte-identical to a genuinely
|
|
1417
|
+
// empty relationship, so this route is not an existence oracle.
|
|
1377
1418
|
const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
|
|
1378
|
-
data = related.map(r => r.toJSON?.({ baseUrl, linkage }));
|
|
1419
|
+
data = related.filter(isLinkable).map(r => r.toJSON?.({ baseUrl, linkage }));
|
|
1379
1420
|
} else {
|
|
1380
|
-
// belongsTo - return single or null
|
|
1381
|
-
|
|
1421
|
+
// belongsTo - return single or null. A DENIED target is 404, the same
|
|
1422
|
+
// status this route already returns for a denied PARENT and for a
|
|
1423
|
+
// parent that does not exist, and the same status the related
|
|
1424
|
+
// record's own route answers.
|
|
1425
|
+
//
|
|
1426
|
+
// KNOWN RESIDUAL, stated here because it is the one place a reader
|
|
1427
|
+
// will look: 404 is distinguishable from a relationship that is
|
|
1428
|
+
// genuinely EMPTY, which answers 200 with `data: null`. So a caller
|
|
1429
|
+
// can tell "there is a target you may not see" from "there is no
|
|
1430
|
+
// target". That asymmetry is inherited -- a denied PARENT has always
|
|
1431
|
+
// answered 404 while an existing parent with an empty relationship
|
|
1432
|
+
// answers 200 -- and closing it is a change to the module's whole
|
|
1433
|
+
// denial spelling, not to this route.
|
|
1434
|
+
if (!isOrmRecord(relatedData)) data = null;
|
|
1435
|
+
else if (!isLinkable(relatedData)) return 404;
|
|
1436
|
+
else data = relatedData.toJSON?.({ baseUrl, linkage });
|
|
1382
1437
|
}
|
|
1383
1438
|
|
|
1384
1439
|
return {
|
|
@@ -1396,16 +1451,36 @@ export default class OrmRequest extends Request {
|
|
|
1396
1451
|
const relatedData = record.__relationships[relationshipName];
|
|
1397
1452
|
const baseUrl = getBaseUrl(request);
|
|
1398
1453
|
|
|
1454
|
+
// THE ONLY ONE OF THE FOUR READ SURFACES THAT DOES NOT GO THROUGH
|
|
1455
|
+
// `toJSON()`. It builds `{ type, id }` BY HAND, which is why #234's
|
|
1456
|
+
// linkage filter never reached it and why this half belongs to
|
|
1457
|
+
// abofs/stonyx-orm#232 rather than to #234: on this route the linkage
|
|
1458
|
+
// IS the primary data of an opt-in request, so filtering it changes the
|
|
1459
|
+
// route's MEMBERSHIP semantics, not the ids named inside somebody
|
|
1460
|
+
// else's document.
|
|
1461
|
+
//
|
|
1462
|
+
// Same filter, same argument-one decision, same residual as
|
|
1463
|
+
// `/:id/{relationship}` above -- read the block there.
|
|
1464
|
+
const linkage = createLinkageFilter(request);
|
|
1465
|
+
const isLinkable = (r: OrmRecord) => {
|
|
1466
|
+
const type = (r as { __model?: { __name?: string } }).__model?.__name;
|
|
1467
|
+
|
|
1468
|
+
return typeof type === 'string' && type !== '' && linkage(type, r);
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1399
1471
|
let data: unknown;
|
|
1400
1472
|
if (info.isArray) {
|
|
1401
1473
|
// hasMany - return array of linkage objects
|
|
1402
1474
|
const related = Array.isArray(relatedData) ? relatedData.filter(isOrmRecord) : [];
|
|
1403
1475
|
data = related
|
|
1404
1476
|
.filter((r): r is OrmRecord & { __model: { __name: string } } => Boolean(r.__model))
|
|
1477
|
+
.filter(isLinkable)
|
|
1405
1478
|
.map(r => ({ type: r.__model.__name, id: r.id }));
|
|
1406
1479
|
} else {
|
|
1407
1480
|
// belongsTo - return single linkage or null
|
|
1408
1481
|
if (isOrmRecord(relatedData) && relatedData.__model) {
|
|
1482
|
+
if (!isLinkable(relatedData)) return 404;
|
|
1483
|
+
|
|
1409
1484
|
data = { type: relatedData.__model.__name, id: relatedData.id };
|
|
1410
1485
|
} else {
|
|
1411
1486
|
data = null;
|