Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transparently ensure foreign key target field in inclusion resolvers #5592

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/filter/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/filter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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']);
});
});
75 changes: 75 additions & 0 deletions packages/filter/src/ensure-fields.ts
Original file line number Diff line number Diff line change
@@ -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<T extends object = AnyObject>(
targetFields: (keyof T)[],
filter: Filter<T>,
) {
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<T extends object = AnyObject>(
fields: (keyof T)[],
filter?: Filter<T>,
) {
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;
}
1 change: 1 addition & 0 deletions packages/filter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
*/

export * from './query';
export * from './ensure-fields';
2 changes: 1 addition & 1 deletion packages/filter/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ export class WhereBuilder<MT extends object = AnyObject> {
}

/**
* 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
Expand Down
Loading