@stonyx/orm 0.3.2-alpha.47 → 0.3.2-alpha.48
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 +12 -1
- package/dist/orm-request.js +90 -9
- package/package.json +2 -2
- package/src/orm-request.ts +86 -9
package/README.md
CHANGED
|
@@ -316,12 +316,23 @@ export default class GlobalAccess {
|
|
|
316
316
|
models = ['owner', 'animal'];
|
|
317
317
|
|
|
318
318
|
access(request) {
|
|
319
|
-
|
|
319
|
+
// false → 403 for the whole request
|
|
320
|
+
if (request.url.startsWith('/admin')) return false;
|
|
321
|
+
|
|
322
|
+
// A function is a per-record filter. It is enforced on the collection, on
|
|
323
|
+
// GET/PATCH/DELETE by id, and on both relationship route families. Match on
|
|
324
|
+
// the url prefix so record routes get the predicate too. Rejected records
|
|
325
|
+
// are 404 on record routes and 403 on POST.
|
|
326
|
+
if (request.url.startsWith('/owners')) return record => record.id !== 'angela';
|
|
327
|
+
|
|
320
328
|
return ['read', 'create', 'update', 'delete'];
|
|
321
329
|
}
|
|
322
330
|
}
|
|
323
331
|
```
|
|
324
332
|
|
|
333
|
+
> `include` (sideloading) does not yet apply the included model's own access
|
|
334
|
+
> filter — see [#196](https://github.com/abofs/stonyx-orm/issues/196).
|
|
335
|
+
|
|
325
336
|
### Include Parameter (Sideloading Relationships)
|
|
326
337
|
|
|
327
338
|
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.js
CHANGED
|
@@ -187,6 +187,23 @@ function createFilterPredicate(filters) {
|
|
|
187
187
|
return String(current) === value;
|
|
188
188
|
});
|
|
189
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* A function-style `access` return is a per-record predicate, and it is only
|
|
192
|
+
* meaningful if every surface that can hand a record to a caller consults it.
|
|
193
|
+
* Before #190 exactly one of seven did.
|
|
194
|
+
*
|
|
195
|
+
* Enforcement is deliberately post-fetch. `access` returns an opaque JS
|
|
196
|
+
* predicate and `store.findAll(model, conditions)` accepts only an equality
|
|
197
|
+
* conditions object that the SQL drivers translate to a WHERE clause, so
|
|
198
|
+
* query-layer enforcement would require a breaking change to the published
|
|
199
|
+
* `access` contract. That belongs in #197, not in a security patch. Six of the
|
|
200
|
+
* seven surfaces fetch by primary key anyway, so this costs exactly one row.
|
|
201
|
+
*/
|
|
202
|
+
function isDenied(filter, record) {
|
|
203
|
+
if (typeof filter !== 'function')
|
|
204
|
+
return false;
|
|
205
|
+
return !filter(record);
|
|
206
|
+
}
|
|
190
207
|
export default class OrmRequest extends Request {
|
|
191
208
|
model;
|
|
192
209
|
access;
|
|
@@ -216,10 +233,15 @@ export default class OrmRequest extends Request {
|
|
|
216
233
|
baseUrl
|
|
217
234
|
});
|
|
218
235
|
};
|
|
219
|
-
const getSingleHandler = async (request) => {
|
|
236
|
+
const getSingleHandler = async (request, { filter }) => {
|
|
220
237
|
const record = await store.find(model, getId(request.params));
|
|
221
238
|
if (!record)
|
|
222
239
|
return 404;
|
|
240
|
+
// 404, never 403: the status for "exists but filtered out" must be
|
|
241
|
+
// identical to "does not exist", or the fix trades an authorization
|
|
242
|
+
// bypass for a narrower existence oracle.
|
|
243
|
+
if (isDenied(filter, record))
|
|
244
|
+
return 404;
|
|
223
245
|
const fieldsMap = parseFields(request.query);
|
|
224
246
|
const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
|
|
225
247
|
const baseUrl = getBaseUrl(request);
|
|
@@ -228,7 +250,7 @@ export default class OrmRequest extends Request {
|
|
|
228
250
|
baseUrl
|
|
229
251
|
});
|
|
230
252
|
};
|
|
231
|
-
const createHandler = async ({ body, query }) => {
|
|
253
|
+
const createHandler = async ({ body, query }, { filter }) => {
|
|
232
254
|
const { type, id, attributes, relationships: rels } = (body?.data || {});
|
|
233
255
|
if (!type)
|
|
234
256
|
return 400; // Bad request
|
|
@@ -252,12 +274,30 @@ export default class OrmRequest extends Request {
|
|
|
252
274
|
const record = isOrmRecord(created) ? created : null;
|
|
253
275
|
if (!record)
|
|
254
276
|
return 500;
|
|
277
|
+
// 403 here, NOT 404. The oracle argument does not apply to create: there
|
|
278
|
+
// is no pre-existing record whose existence could leak, the caller
|
|
279
|
+
// supplied the attributes, and 404 on a mounted collection route is
|
|
280
|
+
// indistinguishable from "model not mounted" -- a genuinely different
|
|
281
|
+
// failure a developer needs to diagnose.
|
|
282
|
+
//
|
|
283
|
+
// The rollback is not optional. createRecord writes to the store BEFORE
|
|
284
|
+
// the predicate can run, so returning 403 alone would leave the record
|
|
285
|
+
// behind: a worse bug than the bypass being fixed.
|
|
286
|
+
if (isDenied(filter, record)) {
|
|
287
|
+
store.remove(model, record.id, { _skipAutoPersist: true });
|
|
288
|
+
return 403;
|
|
289
|
+
}
|
|
255
290
|
return { data: record.toJSON?.({ fields: modelFields }) };
|
|
256
291
|
};
|
|
257
|
-
const updateHandler = async ({ body, params }) => {
|
|
292
|
+
const updateHandler = async ({ body, params }, { filter }) => {
|
|
258
293
|
const found = await store.find(model, getId(params));
|
|
259
294
|
if (!found || !isOrmRecord(found))
|
|
260
295
|
return 404;
|
|
296
|
+
// Checked BEFORE any attribute is applied. 404 rather than 403 for the
|
|
297
|
+
// same reason as GET /:id -- 403 would disclose both that the record
|
|
298
|
+
// exists and that this caller specifically is excluded.
|
|
299
|
+
if (isDenied(filter, found))
|
|
300
|
+
return 404;
|
|
261
301
|
const record = found;
|
|
262
302
|
const { attributes, relationships: rels } = (body?.data || {});
|
|
263
303
|
if (!attributes && !rels)
|
|
@@ -288,7 +328,19 @@ export default class OrmRequest extends Request {
|
|
|
288
328
|
}
|
|
289
329
|
return { data: record.toJSON?.() };
|
|
290
330
|
};
|
|
291
|
-
const deleteHandler = ({ params }) => {
|
|
331
|
+
const deleteHandler = async ({ params }, { filter }) => {
|
|
332
|
+
const record = await store.find(model, getId(params));
|
|
333
|
+
// BEHAVIOUR CHANGE (#190): a DELETE of a record that never existed
|
|
334
|
+
// returned 204 before this change. It now returns 404, matching the
|
|
335
|
+
// denied case below. This is deliberate and load-bearing -- if a denied
|
|
336
|
+
// delete returned 404 while a missing one returned 204, the pair would be
|
|
337
|
+
// a perfect existence oracle and the whole fix would be worthless.
|
|
338
|
+
// Returning 204 for a denied delete was rejected instead: it falsely
|
|
339
|
+
// reports success for a request that changed nothing.
|
|
340
|
+
if (!record)
|
|
341
|
+
return 404;
|
|
342
|
+
if (isDenied(filter, record))
|
|
343
|
+
return 404;
|
|
292
344
|
store.remove(model, getId(params), { _skipAutoPersist: true });
|
|
293
345
|
return 204;
|
|
294
346
|
};
|
|
@@ -353,8 +405,20 @@ export default class OrmRequest extends Request {
|
|
|
353
405
|
context.record = store.get(this.model, getId(request.params));
|
|
354
406
|
}
|
|
355
407
|
// Persist to SQL database for all write operations (create/update/delete)
|
|
408
|
+
//
|
|
409
|
+
// A denied or failed handler returns a bare status integer and MUST NOT
|
|
410
|
+
// reach persist. `response` is passed to sqlDb.persist below, but it is
|
|
411
|
+
// dropped at the driver boundary: _persistDelete(modelName, context) never
|
|
412
|
+
// receives it and guards only on context.recordId -- which _withHooks set
|
|
413
|
+
// above, BEFORE the handler ran. Without this gate a correct 404 still
|
|
414
|
+
// issues DELETE FROM ... WHERE id = ? on every SQL backend.
|
|
415
|
+
//
|
|
416
|
+
// No file-backed test can observe that, because Orm.instance.sqlDb is
|
|
417
|
+
// null in file/directory mode. See the stubbed-sqlDb assertions in
|
|
418
|
+
// test/unit/access-filter-enforcement-test.ts.
|
|
419
|
+
const denied = Number.isInteger(response) && response >= 400;
|
|
356
420
|
const sqlDb = Orm.instance.sqlDb;
|
|
357
|
-
if (sqlDb && WRITE_OPERATIONS.has(operation)) {
|
|
421
|
+
if (sqlDb && WRITE_OPERATIONS.has(operation) && !denied) {
|
|
358
422
|
await sqlDb.persist(operation, this.model, context, response);
|
|
359
423
|
}
|
|
360
424
|
// Add response and relevant records to context
|
|
@@ -392,10 +456,21 @@ export default class OrmRequest extends Request {
|
|
|
392
456
|
// Dasherize the relationship name for URL paths (e.g., accessLinks -> access-links)
|
|
393
457
|
const dasherizedName = camelCaseToKebabCase(relationshipName);
|
|
394
458
|
// Related resource route: GET /:id/{relationship}
|
|
395
|
-
|
|
459
|
+
//
|
|
460
|
+
// These generated routes are not wrapped by _withHooks, which is why they
|
|
461
|
+
// were the least obvious two of the seven unguarded surfaces in #190.
|
|
462
|
+
// They are still dispatched by @stonyx/rest-server as
|
|
463
|
+
// `handler(req, getState(req))`, so `state` -- and therefore the filter
|
|
464
|
+
// planted by auth() -- has always been available here; it was simply
|
|
465
|
+
// never declared or read.
|
|
466
|
+
routes[`/:id/${dasherizedName}`] = async (request, { filter } = {}) => {
|
|
396
467
|
const record = await store.find(model, getId(request.params));
|
|
397
468
|
if (!record)
|
|
398
469
|
return 404;
|
|
470
|
+
// Filtering the PARENT: a caller who may not see the record may not see
|
|
471
|
+
// what it is related to either.
|
|
472
|
+
if (isDenied(filter, record))
|
|
473
|
+
return 404;
|
|
399
474
|
const relatedData = record.__relationships[relationshipName];
|
|
400
475
|
const baseUrl = getBaseUrl(request);
|
|
401
476
|
let data;
|
|
@@ -414,10 +489,12 @@ export default class OrmRequest extends Request {
|
|
|
414
489
|
};
|
|
415
490
|
};
|
|
416
491
|
// Relationship linkage route: GET /:id/relationships/{relationship}
|
|
417
|
-
routes[`/:id/relationships/${dasherizedName}`] = async (request) => {
|
|
492
|
+
routes[`/:id/relationships/${dasherizedName}`] = async (request, { filter } = {}) => {
|
|
418
493
|
const record = await store.find(model, getId(request.params));
|
|
419
494
|
if (!record)
|
|
420
495
|
return 404;
|
|
496
|
+
if (isDenied(filter, record))
|
|
497
|
+
return 404;
|
|
421
498
|
const relatedData = record.__relationships[relationshipName];
|
|
422
499
|
const baseUrl = getBaseUrl(request);
|
|
423
500
|
let data;
|
|
@@ -447,18 +524,22 @@ export default class OrmRequest extends Request {
|
|
|
447
524
|
};
|
|
448
525
|
}
|
|
449
526
|
// Catch-all for invalid relationship names on related resource route
|
|
450
|
-
routes[`/:id/:relationship`] = async (request) => {
|
|
527
|
+
routes[`/:id/:relationship`] = async (request, { filter } = {}) => {
|
|
451
528
|
const record = await store.find(model, getId(request.params));
|
|
452
529
|
if (!record)
|
|
453
530
|
return 404;
|
|
531
|
+
if (isDenied(filter, record))
|
|
532
|
+
return 404;
|
|
454
533
|
// If we reach here, relationship doesn't exist (valid ones were registered above)
|
|
455
534
|
return 404;
|
|
456
535
|
};
|
|
457
536
|
// Catch-all for invalid relationship names on relationship linkage route
|
|
458
|
-
routes[`/:id/relationships/:relationship`] = async (request) => {
|
|
537
|
+
routes[`/:id/relationships/:relationship`] = async (request, { filter } = {}) => {
|
|
459
538
|
const record = await store.find(model, getId(request.params));
|
|
460
539
|
if (!record)
|
|
461
540
|
return 404;
|
|
541
|
+
if (isDenied(filter, record))
|
|
542
|
+
return 404;
|
|
462
543
|
return 404;
|
|
463
544
|
};
|
|
464
545
|
return routes;
|
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.48",
|
|
8
8
|
"description": "",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"type": "module",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"homepage": "https://github.com/abofs/stonyx-orm#readme",
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@stonyx/cron": "0.2.1-beta.
|
|
64
|
+
"@stonyx/cron": "0.2.1-beta.81",
|
|
65
65
|
"@stonyx/events": "0.1.1-beta.52",
|
|
66
66
|
"@stonyx/utils": "0.2.3-beta.26",
|
|
67
67
|
"stonyx": "0.2.3-beta.76"
|
package/src/orm-request.ts
CHANGED
|
@@ -251,6 +251,24 @@ function createFilterPredicate(filters: Filter[]): ((record: { [key: string]: un
|
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* A function-style `access` return is a per-record predicate, and it is only
|
|
256
|
+
* meaningful if every surface that can hand a record to a caller consults it.
|
|
257
|
+
* Before #190 exactly one of seven did.
|
|
258
|
+
*
|
|
259
|
+
* Enforcement is deliberately post-fetch. `access` returns an opaque JS
|
|
260
|
+
* predicate and `store.findAll(model, conditions)` accepts only an equality
|
|
261
|
+
* conditions object that the SQL drivers translate to a WHERE clause, so
|
|
262
|
+
* query-layer enforcement would require a breaking change to the published
|
|
263
|
+
* `access` contract. That belongs in #197, not in a security patch. Six of the
|
|
264
|
+
* seven surfaces fetch by primary key anyway, so this costs exactly one row.
|
|
265
|
+
*/
|
|
266
|
+
function isDenied(filter: unknown, record: unknown): boolean {
|
|
267
|
+
if (typeof filter !== 'function') return false;
|
|
268
|
+
|
|
269
|
+
return !(filter as (record: unknown) => boolean)(record);
|
|
270
|
+
}
|
|
271
|
+
|
|
254
272
|
export default class OrmRequest extends Request {
|
|
255
273
|
model: string;
|
|
256
274
|
access: (request: unknown) => AccessMethod;
|
|
@@ -287,9 +305,13 @@ export default class OrmRequest extends Request {
|
|
|
287
305
|
});
|
|
288
306
|
};
|
|
289
307
|
|
|
290
|
-
const getSingleHandler: HandlerFn = async (request) => {
|
|
308
|
+
const getSingleHandler: HandlerFn = async (request, { filter }) => {
|
|
291
309
|
const record = await store.find(model, getId(request.params)) as OrmRecord | undefined;
|
|
292
310
|
if (!record) return 404;
|
|
311
|
+
// 404, never 403: the status for "exists but filtered out" must be
|
|
312
|
+
// identical to "does not exist", or the fix trades an authorization
|
|
313
|
+
// bypass for a narrower existence oracle.
|
|
314
|
+
if (isDenied(filter, record)) return 404;
|
|
293
315
|
|
|
294
316
|
const fieldsMap = parseFields(request.query);
|
|
295
317
|
const modelFields = fieldsMap.get(pluralizedModel) || fieldsMap.get(model);
|
|
@@ -301,7 +323,7 @@ export default class OrmRequest extends Request {
|
|
|
301
323
|
});
|
|
302
324
|
};
|
|
303
325
|
|
|
304
|
-
const createHandler: HandlerFn = async ({ body, query }) => {
|
|
326
|
+
const createHandler: HandlerFn = async ({ body, query }, { filter }) => {
|
|
305
327
|
const { type, id, attributes, relationships: rels } = (body?.data || {}) as {
|
|
306
328
|
type?: string;
|
|
307
329
|
id?: string | number;
|
|
@@ -334,12 +356,30 @@ export default class OrmRequest extends Request {
|
|
|
334
356
|
const record = isOrmRecord(created) ? created : null;
|
|
335
357
|
if (!record) return 500;
|
|
336
358
|
|
|
359
|
+
// 403 here, NOT 404. The oracle argument does not apply to create: there
|
|
360
|
+
// is no pre-existing record whose existence could leak, the caller
|
|
361
|
+
// supplied the attributes, and 404 on a mounted collection route is
|
|
362
|
+
// indistinguishable from "model not mounted" -- a genuinely different
|
|
363
|
+
// failure a developer needs to diagnose.
|
|
364
|
+
//
|
|
365
|
+
// The rollback is not optional. createRecord writes to the store BEFORE
|
|
366
|
+
// the predicate can run, so returning 403 alone would leave the record
|
|
367
|
+
// behind: a worse bug than the bypass being fixed.
|
|
368
|
+
if (isDenied(filter, record)) {
|
|
369
|
+
store.remove(model, record.id as string | number, { _skipAutoPersist: true });
|
|
370
|
+
return 403;
|
|
371
|
+
}
|
|
372
|
+
|
|
337
373
|
return { data: record.toJSON?.({ fields: modelFields }) };
|
|
338
374
|
};
|
|
339
375
|
|
|
340
|
-
const updateHandler: HandlerFn = async ({ body, params }) => {
|
|
376
|
+
const updateHandler: HandlerFn = async ({ body, params }, { filter }) => {
|
|
341
377
|
const found = await store.find(model, getId(params));
|
|
342
378
|
if (!found || !isOrmRecord(found)) return 404;
|
|
379
|
+
// Checked BEFORE any attribute is applied. 404 rather than 403 for the
|
|
380
|
+
// same reason as GET /:id -- 403 would disclose both that the record
|
|
381
|
+
// exists and that this caller specifically is excluded.
|
|
382
|
+
if (isDenied(filter, found)) return 404;
|
|
343
383
|
const record = found;
|
|
344
384
|
const { attributes, relationships: rels } = (body?.data || {}) as {
|
|
345
385
|
attributes?: { [key: string]: unknown };
|
|
@@ -375,7 +415,19 @@ export default class OrmRequest extends Request {
|
|
|
375
415
|
return { data: record.toJSON?.() };
|
|
376
416
|
};
|
|
377
417
|
|
|
378
|
-
const deleteHandler: HandlerFn = ({ params }) => {
|
|
418
|
+
const deleteHandler: HandlerFn = async ({ params }, { filter }) => {
|
|
419
|
+
const record = await store.find(model, getId(params));
|
|
420
|
+
|
|
421
|
+
// BEHAVIOUR CHANGE (#190): a DELETE of a record that never existed
|
|
422
|
+
// returned 204 before this change. It now returns 404, matching the
|
|
423
|
+
// denied case below. This is deliberate and load-bearing -- if a denied
|
|
424
|
+
// delete returned 404 while a missing one returned 204, the pair would be
|
|
425
|
+
// a perfect existence oracle and the whole fix would be worthless.
|
|
426
|
+
// Returning 204 for a denied delete was rejected instead: it falsely
|
|
427
|
+
// reports success for a request that changed nothing.
|
|
428
|
+
if (!record) return 404;
|
|
429
|
+
if (isDenied(filter, record)) return 404;
|
|
430
|
+
|
|
379
431
|
store.remove(model, getId(params), { _skipAutoPersist: true });
|
|
380
432
|
return 204;
|
|
381
433
|
};
|
|
@@ -449,8 +501,20 @@ export default class OrmRequest extends Request {
|
|
|
449
501
|
}
|
|
450
502
|
|
|
451
503
|
// Persist to SQL database for all write operations (create/update/delete)
|
|
504
|
+
//
|
|
505
|
+
// A denied or failed handler returns a bare status integer and MUST NOT
|
|
506
|
+
// reach persist. `response` is passed to sqlDb.persist below, but it is
|
|
507
|
+
// dropped at the driver boundary: _persistDelete(modelName, context) never
|
|
508
|
+
// receives it and guards only on context.recordId -- which _withHooks set
|
|
509
|
+
// above, BEFORE the handler ran. Without this gate a correct 404 still
|
|
510
|
+
// issues DELETE FROM ... WHERE id = ? on every SQL backend.
|
|
511
|
+
//
|
|
512
|
+
// No file-backed test can observe that, because Orm.instance.sqlDb is
|
|
513
|
+
// null in file/directory mode. See the stubbed-sqlDb assertions in
|
|
514
|
+
// test/unit/access-filter-enforcement-test.ts.
|
|
515
|
+
const denied = Number.isInteger(response) && (response as number) >= 400;
|
|
452
516
|
const sqlDb = Orm.instance.sqlDb;
|
|
453
|
-
if (sqlDb && WRITE_OPERATIONS.has(operation)) {
|
|
517
|
+
if (sqlDb && WRITE_OPERATIONS.has(operation) && !denied) {
|
|
454
518
|
await sqlDb.persist(operation, this.model, context, response);
|
|
455
519
|
}
|
|
456
520
|
|
|
@@ -497,9 +561,19 @@ export default class OrmRequest extends Request {
|
|
|
497
561
|
const dasherizedName = camelCaseToKebabCase(relationshipName);
|
|
498
562
|
|
|
499
563
|
// Related resource route: GET /:id/{relationship}
|
|
500
|
-
|
|
564
|
+
//
|
|
565
|
+
// These generated routes are not wrapped by _withHooks, which is why they
|
|
566
|
+
// were the least obvious two of the seven unguarded surfaces in #190.
|
|
567
|
+
// They are still dispatched by @stonyx/rest-server as
|
|
568
|
+
// `handler(req, getState(req))`, so `state` -- and therefore the filter
|
|
569
|
+
// planted by auth() -- has always been available here; it was simply
|
|
570
|
+
// never declared or read.
|
|
571
|
+
routes[`/:id/${dasherizedName}`] = async (request: OrmRequest$, { filter }: { [key: string]: unknown } = {}) => {
|
|
501
572
|
const record = await store.find(model, getId(request.params)) as OrmRecord | undefined;
|
|
502
573
|
if (!record) return 404;
|
|
574
|
+
// Filtering the PARENT: a caller who may not see the record may not see
|
|
575
|
+
// what it is related to either.
|
|
576
|
+
if (isDenied(filter, record)) return 404;
|
|
503
577
|
|
|
504
578
|
const relatedData = record.__relationships[relationshipName];
|
|
505
579
|
const baseUrl = getBaseUrl(request);
|
|
@@ -521,9 +595,10 @@ export default class OrmRequest extends Request {
|
|
|
521
595
|
};
|
|
522
596
|
|
|
523
597
|
// Relationship linkage route: GET /:id/relationships/{relationship}
|
|
524
|
-
routes[`/:id/relationships/${dasherizedName}`] = async (request: OrmRequest
|
|
598
|
+
routes[`/:id/relationships/${dasherizedName}`] = async (request: OrmRequest$, { filter }: { [key: string]: unknown } = {}) => {
|
|
525
599
|
const record = await store.find(model, getId(request.params)) as OrmRecord | undefined;
|
|
526
600
|
if (!record) return 404;
|
|
601
|
+
if (isDenied(filter, record)) return 404;
|
|
527
602
|
|
|
528
603
|
const relatedData = record.__relationships[relationshipName];
|
|
529
604
|
const baseUrl = getBaseUrl(request);
|
|
@@ -555,18 +630,20 @@ export default class OrmRequest extends Request {
|
|
|
555
630
|
}
|
|
556
631
|
|
|
557
632
|
// Catch-all for invalid relationship names on related resource route
|
|
558
|
-
routes[`/:id/:relationship`] = async (request: OrmRequest
|
|
633
|
+
routes[`/:id/:relationship`] = async (request: OrmRequest$, { filter }: { [key: string]: unknown } = {}) => {
|
|
559
634
|
const record = await store.find(model, getId(request.params));
|
|
560
635
|
if (!record) return 404;
|
|
636
|
+
if (isDenied(filter, record)) return 404;
|
|
561
637
|
|
|
562
638
|
// If we reach here, relationship doesn't exist (valid ones were registered above)
|
|
563
639
|
return 404;
|
|
564
640
|
};
|
|
565
641
|
|
|
566
642
|
// Catch-all for invalid relationship names on relationship linkage route
|
|
567
|
-
routes[`/:id/relationships/:relationship`] = async (request: OrmRequest
|
|
643
|
+
routes[`/:id/relationships/:relationship`] = async (request: OrmRequest$, { filter }: { [key: string]: unknown } = {}) => {
|
|
568
644
|
const record = await store.find(model, getId(request.params));
|
|
569
645
|
if (!record) return 404;
|
|
646
|
+
if (isDenied(filter, record)) return 404;
|
|
570
647
|
|
|
571
648
|
return 404;
|
|
572
649
|
};
|