From 60f81c23a1ce272bce8e122bdb96cad7655bd630 Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Mon, 15 Jun 2026 19:18:52 +0100 Subject: [PATCH] New: Add queryHook for role-agnostic query filtering accessQueryHook is skipped for super users (access filtering), which silently breaks user-driven list filters that must apply to every role. queryHook runs in queryHandler for all users, after requestHook and before pagination, so filter clauses merge into req.apiData.query without affecting counts or the Link header. --- lib/AbstractApiModule.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/AbstractApiModule.js b/lib/AbstractApiModule.js index 0603aa8e..cd33a842 100644 --- a/lib/AbstractApiModule.js +++ b/lib/AbstractApiModule.js @@ -110,12 +110,22 @@ class AbstractApiModule extends AbstractModule { this.accessCheckHook = new Hook() /** * Hook invoked before a query runs, allowing observers to merge access-control clauses into - * `req.apiData.query`. Preferred over `accessCheckHook` for filtering: keeps pagination counts - * and the `Link` header accurate, and avoids any need to top up short pages. + * `req.apiData.query`. Preferred over `accessCheckHook` for access filtering: keeps pagination + * counts and the `Link` header accurate, and avoids any need to top up short pages. * Skipped for super users so they see unfiltered results, matching `checkAccess` behaviour. + * For user-driven query filters that must apply regardless of role, use `queryHook` instead. * @type {Hook} */ this.accessQueryHook = new Hook() + /** + * Hook invoked before a list query runs, allowing observers to merge user-driven filter clauses + * into `req.apiData.query` (e.g. dashboard filters sourced from another collection). Unlike + * `accessQueryHook` this runs for every user, including super users — it expresses what the user + * asked to see, not what they are permitted to see. Runs after `requestHook` and before + * pagination, so counts and the `Link` header stay accurate. + * @type {Hook} + */ + this.queryHook = new Hook({ mutable: true }) await this.setValues() this.validateValues() @@ -495,6 +505,7 @@ class AbstractApiModule extends AbstractModule { Object.keys(req.apiData.query).forEach(key => delete opts[key]) await this.requestHook.invoke(req) + await this.queryHook.invoke(req) if (!req.auth.isSuper) await this.accessQueryHook.invoke(req) await this.setUpPagination(req, res, mongoOpts)