diff --git a/packages/filter/package-lock.json b/packages/filter/package-lock.json index 79e5b83b2996..c5ccbdb64479 100644 --- a/packages/filter/package-lock.json +++ b/packages/filter/package-lock.json @@ -4,12 +4,23 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/lodash": { + "version": "4.14.161", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz", + "integrity": "sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA==", + "dev": true + }, "@types/node": { "version": "10.17.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz", "integrity": "sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA==", "dev": true }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, "tslib": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.2.tgz", diff --git a/packages/filter/package.json b/packages/filter/package.json index e0712e1acff4..e83acdd3f12c 100644 --- a/packages/filter/package.json +++ b/packages/filter/package.json @@ -28,11 +28,13 @@ "!*/__tests__" ], "dependencies": { + "lodash": "^4.17.20", "tslib": "^2.0.2" }, "devDependencies": { "@loopback/build": "^6.2.4", "@loopback/testlab": "^3.2.6", + "@types/lodash": "^4.14.161", "@types/node": "^10.17.35", "typescript": "~4.0.3" }, diff --git a/packages/filter/src/__tests__/acceptance/ensure-fields.acceptance.ts b/packages/filter/src/__tests__/acceptance/ensure-fields.acceptance.ts new file mode 100644 index 000000000000..dfd527eb7bcc --- /dev/null +++ b/packages/filter/src/__tests__/acceptance/ensure-fields.acceptance.ts @@ -0,0 +1,57 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/filter +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect} from '@loopback/testlab'; +import {ensureFields, Filter} from '../..'; + +describe('ensureFields', () => { + it('does not modify a filter when it does not specify fields', () => { + const filter = {} as Filter; + const {filter: newFilter, fieldsAdded} = ensureFields(['a', 'b'], filter); + + expect(newFilter).to.eql({}); + expect(fieldsAdded).to.eql([]); + }); + + it('does not modify a filter when it does not exclude target fields', () => { + const filter = {fields: {a: false, b: false}} as Filter; + const {filter: newFilter, fieldsAdded} = ensureFields(['c'], filter); + + expect(newFilter).to.eql({fields: {a: false, b: false}}); + expect(fieldsAdded).to.eql([]); + }); + + it('does not modify a filter when target fields are not specified', () => { + const filter = {fields: {a: false, b: false}} as Filter; + const {filter: newFilter, fieldsAdded} = ensureFields([], filter); + + expect(newFilter).to.eql({fields: {a: false, b: false}}); + expect(fieldsAdded).to.eql([]); + }); + + it('adds omitted fields', () => { + const filter = {fields: {a: true}} as Filter; + const {filter: newFilter, fieldsAdded} = ensureFields(['b'], filter); + + expect(newFilter).to.eql({fields: {a: true, b: true}}); + expect(fieldsAdded).to.eql(['b']); + }); + + it('adds explicitly disabled fields', () => { + const filter = {fields: {a: true, b: false}} as Filter; + const {filter: newFilter, fieldsAdded} = ensureFields(['b'], filter); + + expect(newFilter).to.eql({fields: {a: true, b: true}}); + expect(fieldsAdded).to.eql(['b']); + }); + + it('removes fields clause when it only excludes fields', () => { + const filter = {fields: {a: false, b: false}} as Filter; + const {filter: newFilter, fieldsAdded} = ensureFields(['b'], filter); + + expect(newFilter).to.eql({}); + expect(fieldsAdded).to.eql(['a', 'b']); + }); +}); diff --git a/packages/filter/src/ensure-fields.ts b/packages/filter/src/ensure-fields.ts new file mode 100644 index 000000000000..5ca02d736830 --- /dev/null +++ b/packages/filter/src/ensure-fields.ts @@ -0,0 +1,75 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/filter +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import _ from 'lodash'; +import {Filter, FilterBuilder} from './query'; +import {AnyObject} from './types'; + +/** + * Ensures that queries which apply the returned filter would always include + * the target fields. To undo this effect later, fields that were disabled + * in the original filter will be added to the pruning mask. + * + * @param targetFields - An array of fields to include + * @param filter - A target filter + * @returns A tuple containing amended filter and pruning mask + */ +export function ensureFields( + targetFields: (keyof T)[], + filter: Filter, +) { + const builder = new FilterBuilder(filter); + const fields = builder.build().fields; + if (!fields || matchesFields(targetFields, filter)) { + return { + filter: builder.build(), + fieldsAdded: [] as (keyof T)[], + }; + } + const isDisablingOnly = _.size(fields) > 0 && !_.some(fields, Boolean); + const fieldsAdded = (isDisablingOnly ? _.keys(fields) : []) as (keyof T)[]; + targetFields.forEach(f => { + if (!fields[f]) { + fieldsAdded.push(f); + builder.fields(f); + } + }); + + const newFilter = builder.build(); + // if the filter only hides the fields, unset the entire fields clause + if (isDisablingOnly) { + delete filter.fields; + } + return { + filter: newFilter, + fieldsAdded: _.uniq(fieldsAdded) as (keyof T)[], + } as const; +} + +/** + * Checks whether fields array passed as an argument is a + * subset of fields picked by a target filter. + * + * @param fields - An array of fields to search in a filter + * @param filter - A target filter + */ +export function matchesFields( + fields: (keyof T)[], + filter?: Filter, +) { + const normalized = new FilterBuilder(filter).build(); + const targetFields = normalized.fields; + if (!targetFields) { + return true; + } + const isDisablingOnly = + _.size(targetFields) > 0 && !_.some(targetFields, Boolean); + for (const f of fields) { + if (!targetFields[f] && (f in targetFields || !isDisablingOnly)) { + return false; + } + } + return true; +} diff --git a/packages/filter/src/index.ts b/packages/filter/src/index.ts index d296d7e02fbf..b4e48c76f9cd 100644 --- a/packages/filter/src/index.ts +++ b/packages/filter/src/index.ts @@ -18,3 +18,4 @@ */ export * from './query'; +export * from './ensure-fields'; diff --git a/packages/filter/src/query.ts b/packages/filter/src/query.ts index 209876ef4eca..b237c706b5ca 100644 --- a/packages/filter/src/query.ts +++ b/packages/filter/src/query.ts @@ -506,7 +506,7 @@ export class WhereBuilder { } /** - * A builder for Filter. It provides fleunt APIs to add clauses such as + * A builder for Filter. It provides fluent APIs to add clauses such as * `fields`, `order`, `where`, `limit`, `offset`, and `include`. * * @example