diff --git a/addons/api/addon/services/resource-filter-store.js b/addons/api/addon/services/resource-filter-store.js new file mode 100644 index 000000000..94d934359 --- /dev/null +++ b/addons/api/addon/services/resource-filter-store.js @@ -0,0 +1,109 @@ +import Service from '@ember/service'; +import { inject as service } from '@ember/service'; + +/** + * Takes a POJO representing a query and converts it to a boolean expression + * string which is understood by the API. + * + * @example + * const filter = new ResourceFilter({ + * type: 'oidc', + * status: ['active', 'pending'] + * }); + * console.log(filter.queryExpression); + * // ("/item/type" == "oidc") and ("/item/status" == "active" or "/item/status" == "pending") + */ +export class ResourceFilter { + // =attributes + + /** + * @type {?object} + */ + #filterObject; + + /** + * @type {string} + */ + get queryExpression() { + const clauses = Object.keys(this.#filterObject) + .map((key) => { + const value = this.#filterObject[key]; + const valuesArray = Array.isArray(value) ? value : [value]; + return this.parenthetical( + this.or(valuesArray.map((value) => this.equals(key, value))) + ); + }) + .flat(); + return this.and(clauses); + } + + // =methods + + /** + * @param {object} filterObject + */ + constructor(filterObject = {}) { + this.#filterObject = Object.assign({}, filterObject); + } + + /** + * @param {string} key + * @param value + * @return {string} + */ + equals(key, value) { + return `"/item/${key}" == "${value}"`; + } + + /** + * @param {string[]} clauses + * @return {string} + */ + and(clauses) { + return clauses.join(' and '); + } + + /** + * @param {string[]} clauses + * @return {string} + */ + or(clauses) { + return clauses.join(' or '); + } + + /** + * @param {string} expression + * @return {string} + */ + parenthetical(expression) { + return `(${expression})`; + } +} + +/** + * + */ +export default class ResourceFilterStoreService extends Service { + // =services + + @service store; + + // =methods + + /** + * Similar to Ember's built-in `store.query`, except this method accepts + * an additional argument `filterObject` representing a query filter + * (see ResourceFilter class above). + * @param {string} modelName + * @param {object} filterObject + * @param {?object} storeQuery + * @return {Promise} + */ + queryBy(modelName, filterObject = {}, storeQuery = {}) { + const filter = new ResourceFilter(filterObject); + const query = Object.assign({}, storeQuery); + // Apply filters to store query when available + if (filter.queryExpression) query.filter = filter.queryExpression; + return this.store.query(modelName, query); + } +} diff --git a/addons/api/app/services/resource-filter-store.js b/addons/api/app/services/resource-filter-store.js new file mode 100644 index 000000000..984741e20 --- /dev/null +++ b/addons/api/app/services/resource-filter-store.js @@ -0,0 +1 @@ +export { default } from 'api/services/resource-filter-store'; diff --git a/addons/api/tests/unit/services/resource-filter-store-test.js b/addons/api/tests/unit/services/resource-filter-store-test.js new file mode 100644 index 000000000..4902d8c5b --- /dev/null +++ b/addons/api/tests/unit/services/resource-filter-store-test.js @@ -0,0 +1,18 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { ResourceFilter } from 'api/services/resource-filter-store'; + +module('Unit | Service | resource-filter-store', function (hooks) { + setupTest(hooks); + + test('it exports a ResourceFilter class that converts filterObjects to query expressions', function (assert) { + assert.ok(ResourceFilter); + const filter = new ResourceFilter({ + type: 'oidc', + status: ['active', 'pending'], + }); + const expected = + '("/item/type" == "oidc") and ("/item/status" == "active" or "/item/status" == "pending")'; + assert.equal(filter.queryExpression, expected); + }); +}); diff --git a/ui/desktop/app/routes/scopes/scope/projects/sessions.js b/ui/desktop/app/routes/scopes/scope/projects/sessions.js index cd11b0569..799785f5b 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/sessions.js +++ b/ui/desktop/app/routes/scopes/scope/projects/sessions.js @@ -13,6 +13,7 @@ export default class ScopesScopeProjectsSessionsRoute extends Route { @service ipc; @service session; + @service resourceFilterStore; // =attributes @@ -52,8 +53,7 @@ export default class ScopesScopeProjectsSessionsRoute extends Route { const { id: scope_id } = this.modelFor('scopes.scope'); const { user_id } = this.session.data.authenticated; await this.store.query('target', { recursive: true, scope_id }); - return await this.store.query('session', { - filter: `"/item/user_id" == "${user_id}"`, + return await this.resourceFilterStore.queryBy('session', { user_id }, { recursive: true, scope_id });