Skip to content

Commit

Permalink
fix(backend): GraphQL queries with input filter (#217)
Browse files Browse the repository at this point in the history
* fix(backend): GraphQL queries with input filter
* Improved escaping/normalizing search terms for string/text/readonly fields.
* Invalid characters from integer/float/number fields with string search terms are now removed.
* Added support for range filter `..` to function without an upper bound. When for example `2..` is input then `field ge 2` or `field gt 2` is send to the backend, depending on the `defaultFilterRangeOperator`
* Added support for range filter `..` to function without a lower bound. When for example `..2` is input `field le 2` or `field lt 2` is send, depending on the `defaultFilterRangeOperator`
  • Loading branch information
ghiscoding committed Jan 5, 2021
1 parent e35f116 commit ff7f1e5
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 19 deletions.
159 changes: 152 additions & 7 deletions packages/graphql/src/services/__tests__/graphql.service.spec.ts
Expand Up @@ -28,6 +28,7 @@ function removeSpaces(text: string) {

const gridOptionMock = {
enablePagination: true,
defaultFilterRangeOperator: OperatorType.rangeInclusive,
backendServiceApi: {
service: undefined,
options: { datasetName: '' },
Expand Down Expand Up @@ -851,7 +852,7 @@ describe('GraphqlService', () => {
expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query with search having a range of exclusive numbers when the search value contains 2 (..) to represent a range of numbers', () => {
it('should return a query with search having a range of exclusive numbers when the search value contains 2 dots (..) to represent a range of numbers', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:GE, value:"2"}, {field:duration, operator:LE, value:"33"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumn = { id: 'duration', field: 'duration' } as Column;
const mockColumnFilters = {
Expand All @@ -865,6 +866,62 @@ describe('GraphqlService', () => {
expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query to filter a search value between an inclusive range of numbers using the 2 dots (..) separator, the "RangeInclusive" operator and the range has an unbounded end', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:GE, value:"5"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumnDuration = { id: 'duration', field: 'duration', type: FieldType.number } as Column;
const mockColumnFilters = {
duration: { columnId: 'duration', columnDef: mockColumnDuration, searchTerms: ['5..'], operator: 'RangeInclusive' },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query to filter a search value between an inclusive range of numbers using the 2 dots (..) separator, the "RangeInclusive" operator and the range has an unbounded begin', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:LE, value:"5"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumnDuration = { id: 'duration', field: 'duration', type: FieldType.number } as Column;
const mockColumnFilters = {
duration: { columnId: 'duration', columnDef: mockColumnDuration, searchTerms: ['..5'], operator: 'RangeInclusive' },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query to filter a search value between an inclusive range of numbers using the 2 dots (..) separator, the "RangeExclusive" operator and the range has an unbounded end', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:GT, value:"5"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumnDuration = { id: 'duration', field: 'duration', type: FieldType.number } as Column;
const mockColumnFilters = {
duration: { columnId: 'duration', columnDef: mockColumnDuration, searchTerms: ['5..'], operator: 'RangeExclusive' },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query to filter a search value between an inclusive range of numbers using the 2 dots (..) separator, the "RangeExclusive" operator and the range has an unbounded begin', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:LT, value:"5"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumnDuration = { id: 'duration', field: 'duration', type: FieldType.number } as Column;
const mockColumnFilters = {
duration: { columnId: 'duration', columnDef: mockColumnDuration, searchTerms: ['..5'], operator: 'RangeExclusive' },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query with search having a range of inclusive numbers when 2 searchTerms numbers are provided and the operator is "RangeInclusive"', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:GE, value:2}, {field:duration, operator:LE, value:33}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumn = { id: 'duration', field: 'duration' } as Column;
Expand All @@ -879,7 +936,7 @@ describe('GraphqlService', () => {
expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query with search having a range of exclusive dates when the search value contains 2 (..) to represent a range of dates', () => {
it('should return a query with search having a range of exclusive dates when the search value contains 2 dots (..) to represent a range of dates', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:startDate, operator:GE, value:"2001-01-01"}, {field:startDate, operator:LE, value:"2001-01-31"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumn = { id: 'startDate', field: 'startDate' } as Column;
const mockColumnFilters = {
Expand Down Expand Up @@ -907,6 +964,38 @@ describe('GraphqlService', () => {
expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query with a date equal when only 1 searchTerms is provided and even if the operator is set to a range', () => {
const expectation = `query{users(first:10,offset:0,filterBy:[{field:company,operator:Contains,value:"abc"},{field:updatedDate,operator:EQ,value:"2001-01-20"}]){totalCount,nodes{id,company,gender,name}}}`;
const mockColumnCompany = { id: 'company', field: 'company' } as Column;
const mockColumnUpdated = { id: 'updatedDate', field: 'updatedDate', type: FieldType.date } as Column;
const mockColumnFilters = {
company: { columnId: 'company', columnDef: mockColumnCompany, searchTerms: ['abc'], operator: 'Contains' },
updatedDate: { columnId: 'updatedDate', columnDef: mockColumnUpdated, searchTerms: ['2001-01-20'], operator: 'RangeExclusive' },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query without any date filtering when searchTerms is an empty array', () => {
const expectation = `query{users(first:10,offset:0,filterBy:[{field:company,operator:Contains,value:"abc"}]){totalCount,nodes{id,company,gender,name}}}`;
const mockColumnCompany = { id: 'company', field: 'company' } as Column;
const mockColumnUpdated = { id: 'updatedDate', field: 'updatedDate', type: FieldType.date } as Column;
const mockColumnFilters = {
company: { columnId: 'company', columnDef: mockColumnCompany, searchTerms: ['abc'], operator: 'Contains' },
updatedDate: { columnId: 'updatedDate', columnDef: mockColumnUpdated, searchTerms: [], operator: 'RangeExclusive' },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query with a CSV string when the filter operator is IN ', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:gender, operator:IN, value:"female,male"}]) { totalCount,nodes{ id,company,gender,name } }}`;
const mockColumn = { id: 'gender', field: 'gender' } as Column;
Expand Down Expand Up @@ -1057,11 +1146,11 @@ describe('GraphqlService', () => {

describe('presets', () => {
beforeEach(() => {
const columns = [{ id: 'company', field: 'company' }, { id: 'gender', field: 'gender' }, { id: 'duration', field: 'duration' }, { id: 'startDate', field: 'startDate' }];
const columns = [{ id: 'company', field: 'company' }, { id: 'gender', field: 'gender' }, { id: 'duration', field: 'duration', type: FieldType.number }, { id: 'startDate', field: 'startDate' }];
jest.spyOn(gridStub, 'getColumns').mockReturnValue(columns);
});

it('should return a query with search having a range of exclusive numbers when the search value contains 2 (..) to represent a range of numbers', () => {
it('should return a query with search having a range of exclusive numbers when the search value contains 2 dots (..) to represent a range of numbers', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:GE, value:"2"}, {field:duration, operator:LE, value:"33"}]) {
totalCount,nodes{ id,company,gender,duration,startDate } }}`;
const presetFilters = [
Expand All @@ -1077,8 +1166,8 @@ describe('GraphqlService', () => {
expect(currentFilters).toEqual(presetFilters);
});

it('should return a query with a filter with range of numbers with decimals when the preset is a filter range with 3 dots (..) separator', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:GE, value:"0.5"}, {field:duration, operator:LE, value:".88"}]) { totalCount,nodes{ id,company,gender,duration,startDate } }}`;
it('should return a query with a filter with range of numbers with decimals when the preset is a filter range with 2 dots (..) separator and range ends with a fraction', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:duration, operator:GE, value:"0.5"}, {field:duration, operator:LE, value:"0.88"}]) { totalCount,nodes{ id,company,gender,duration,startDate } }}`;
const presetFilters = [
{ columnId: 'duration', searchTerms: ['0.5...88'] },
] as CurrentFilter[];
Expand Down Expand Up @@ -1122,7 +1211,7 @@ describe('GraphqlService', () => {
expect(currentFilters).toEqual(presetFilters);
});

it('should return a query with search having a range of exclusive dates when the search value contains 2 (..) to represent a range of dates', () => {
it('should return a query with search having a range of exclusive dates when the search value contains 2 dots (..) to represent a range of dates', () => {
const expectation = `query{users(first:10, offset:0, filterBy:[{field:startDate, operator:GE, value:"2001-01-01"}, {field:startDate, operator:LE, value:"2001-01-31"}]) { totalCount,nodes{ id,company,gender,duration,startDate } }}`;
const presetFilters = [
{ columnId: 'startDate', searchTerms: ['2001-01-01..2001-01-31'] },
Expand Down Expand Up @@ -1166,6 +1255,62 @@ describe('GraphqlService', () => {
expect(removeSpaces(query)).toBe(removeSpaces(expectation));
expect(currentFilters).toEqual(presetFilters);
});

it('should return a query to filter a search value with a fraction of a number that is missing a leading 0', () => {
const expectation = `query{users(first:10,offset:0,filterBy:[{field:duration,operator:EQ,value:"0.22"}]){totalCount,nodes{id,company,gender,duration,startDate}}}`;
const mockColumnDuration = { id: 'duration', field: 'duration', type: FieldType.number } as Column;
const mockColumnFilters = {
duration: { columnId: 'duration', columnDef: mockColumnDuration, searchTerms: ['.22'] },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query without invalid characters to filter a search value that does contains invalid characters', () => {
const expectation = `query{users(first:10,offset:0,filterBy:[{field:duration,operator:EQ,value:"-22"}]){totalCount,nodes{id,company,gender,duration,startDate}}}`;
const mockColumnDuration = { id: 'duration', field: 'duration', type: FieldType.float } as Column;
const mockColumnFilters = {
duration: { columnId: 'duration', columnDef: mockColumnDuration, searchTerms: ['-2a2'] },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query without invalid characters to filter a search value with an integer that contains invalid characters', () => {
const expectation = `query{users(first:10,offset:0,filterBy:[{field:duration,operator:EQ,value:"22"}]){totalCount,nodes{id,company,gender,duration,startDate}}}`;
const mockColumnDuration = { id: 'duration', field: 'duration', type: FieldType.integer } as Column;
const mockColumnFilters = {
duration: { columnId: 'duration', columnDef: mockColumnDuration, searchTerms: ['22;'] },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});

it('should return a query without invalid characters to filter a search value with a number that only has a minus characters', () => {
const expectation = `query{users(first:10,offset:0,filterBy:[{field:duration,operator:EQ,value:"0"}]){totalCount,nodes{id,company,gender,duration,startDate}}}`;
const mockColumnDuration = { id: 'duration', field: 'duration', type: FieldType.number } as Column;
const mockColumnFilters = {
duration: { columnId: 'duration', columnDef: mockColumnDuration, searchTerms: ['-'] },
} as ColumnFilters;

service.init(serviceOptions, paginationOptions, gridStub);
service.updateFilters(mockColumnFilters, false);
const query = service.buildQuery();

expect(removeSpaces(query)).toBe(removeSpaces(expectation));
});
});

describe('updateSorters method', () => {
Expand Down

0 comments on commit ff7f1e5

Please sign in to comment.