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..7d4d382ca --- /dev/null +++ b/addons/api/addon/services/resource-filter-store.js @@ -0,0 +1,87 @@ +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 + + #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 + */ + equals(key, value) { + return `"/item/${key}" == "${value}"`; + } + + /** + * @param {string[]} clauses + */ + and(clauses) { + return clauses.join(' and '); + } + + /** + * @param {string[]} clauses + */ + or(clauses) { + return clauses.join(' or '); + } + + /** + * @param {string} expression + */ + parenthetical(expression) { + return `(${expression})`; + } +} + +export default class ResourceFilterStoreService extends Service { + @service store; + + queryBy(modelName, filterObject = {}, storeQuery = {}) { + console.log(filterObject); + 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/addon/services/resource-filtering-store.js b/addons/api/addon/services/resource-filtering-store.js deleted file mode 100644 index 4f7537c10..000000000 --- a/addons/api/addon/services/resource-filtering-store.js +++ /dev/null @@ -1,53 +0,0 @@ -import Service from '@ember/service'; -import { inject as service } from '@ember/service'; - -class ResourceFilters { - buildQuery(resourceFilters) { - const filters = this._buildFilters(resourceFilters); - return this._joinFilters(filters, { inclusive: false }); - } - - /** - * Iterate through filter config to generate array of filter strings - * @param {object} config - */ - _buildFilters(config) { - let filters = []; - for (const key in config) { - filters.push(this._generateQuery(`/item/${key}`, config[`${key}`])); - } - return filters; - } - - /** - * For a single filter, build filter query - * @param {string} selector - * @param {*} values - */ - _generateQuery(selector, values) { - // Coerce single string filters into an array for easy mapping - if (!Array.isArray(values)) values = [values]; - const filters = values.map((value) => `"${selector}" == "${value}"`); - return this._joinFilters(filters, { inclusive: true }); - } - - /** - * Combine filter strings - * @param {array} filters - * @param {object} inclusive - */ - _joinFilters(filters, { inclusive }) { - return filters.join(inclusive ? ' or ' : ' and '); - } -} - -export default class ResourceFilteringStoreService extends Service { - @service store; - - queryBy(modelName, resourceParams, resourceFilters) { - const filterQuery = new ResourceFilters().buildQuery(resourceFilters); - // Apply filters to store query when available - if (filterQuery) resourceParams.filter = filterQuery; - return this.store.query(modelName, resourceParams); - } -} 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/app/services/resource-filtering-store.js b/addons/api/app/services/resource-filtering-store.js deleted file mode 100644 index 13fe8a0c8..000000000 --- a/addons/api/app/services/resource-filtering-store.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'api/services/resource-filtering-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/addons/api/tests/unit/services/resource-filtering-store-test.js b/addons/api/tests/unit/services/resource-filtering-store-test.js deleted file mode 100644 index a7cc8ad3e..000000000 --- a/addons/api/tests/unit/services/resource-filtering-store-test.js +++ /dev/null @@ -1,29 +0,0 @@ -import { module, test, skip } from 'qunit'; -import { setupTest } from 'ember-qunit'; - -module('Unit | Service | resource-filtering-store', function (hooks) { - setupTest(hooks); - let service, store; - hooks.beforeEach(function () { - service = this.owner.lookup('service:resource-filtering-store'); - store = this.owner.lookup('service:store'); - - store.createRecord('session', { - id: 's_1', - scope: { - id: '123', - }, - }); - }); - - // TODO: Replace this with your real tests. - test('it exists', function (assert) { - assert.ok(service); - }); - - // TODO: Is it possible to capture mirage API requests in this test? - skip('it supports query filters', function (assert) { - service.queryBy('session', {}, { scope_id: '123' }); - assert.ok(service); - }); -}); 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 });